kiwivm

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package kiwivm is a Go client for the KiwiVM REST API used by BandwagonHost / 64clouds VPS instances.

The API is per-instance: every call authenticates with a (veid, api_key) pair that identifies one VPS. There is no account-level endpoint, so managing a fleet means holding one credential pair per box. See package github.com/lroolle/bwg-cli/internal/config for the fleet model built on top of this client.

Read-only is a capability, not a flag

Every endpoint is registered in Ops with a Risk classification. A client built with ReadOnly refuses every non-read operation before any HTTP request is made:

c := kiwivm.New(veid, key, kiwivm.ReadOnly())
_, err := c.Restart(ctx)          // *ReadOnlyError, no network I/O
info, err := c.ServiceInfo(ctx)   // fine

The guard lives here rather than in the CLI so that SDK consumers, the CLI, and the MCP server all inherit it. Client.Can reports whether an operation would be permitted, which is how callers build an honest tool list for an agent instead of advertising tools that will fail.

Errors

KiwiVM answers HTTP 200 with an "error" field on failure. Non-zero values surface as *APIError; use IsAuth, IsLocked and IsRateLimited to branch. Transport failures and 5xx surface as *TransportError, which IsTransient reports true for — those deserve a retry, never a credential change.

Example (ErrorHandling)

Errors are classified so callers can branch without matching text.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/lroolle/bwg-cli/kiwivm"
)

func main() {
	c := kiwivm.New("1347645", "private_key", kiwivm.WithTimeout(10*time.Second))

	info, err := c.ServiceInfo(context.Background())
	switch {
	case kiwivm.IsAuth(err):
		log.Fatal("the veid/api_key pair does not work")
	case kiwivm.IsLocked(err):
		log.Print("the VPS is busy with another task; the error carries progress")
	case kiwivm.IsTransient(err):
		log.Print("temporary — retry unchanged")
	case err != nil:
		log.Fatal(err)
	default:
		fmt.Println(info.Hostname)
	}
}
Example (ReadOnly)

The common case: read the fleet-relevant numbers off one VPS without any possibility of changing it.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/lroolle/bwg-cli/kiwivm"
)

func main() {
	c := kiwivm.New(os.Getenv("BWG_VEID"), os.Getenv("BWG_API_KEY"),
		kiwivm.ReadOnly())

	// A mutation is refused before any HTTP request happens.
	if err := c.Restart(context.Background()); kiwivm.IsReadOnly(err) {
		fmt.Println("restart refused: client is read-only")
	}
}
Output:
restart refused: client is read-only

Index

Examples

Constants

View Source
const (
	// CodeOK is the success value of the "error" field.
	CodeOK = 0
	// CodeMissingParam is returned when a required parameter is absent,
	// including the veid itself.
	CodeMissingParam = 700001
	// CodeAuthFailure is returned for a bad veid/api_key pair. It does
	// NOT distinguish "wrong key" from "wrong veid" — both look the same.
	CodeAuthFailure = 700005
	// CodeLocked is returned while the VPS is busy with another task
	// (snapshot, reinstall, migration).
	CodeLocked = 788888
)

KiwiVM error codes observed in the wild. The API answers HTTP 200 and puts the real outcome in the "error" field, so these are the only reliable signal for branching.

View Source
const DefaultBaseURL = "https://api.64clouds.com/v1"

DefaultBaseURL is the public KiwiVM API root.

View Source
const DefaultTimeout = 45 * time.Second

DefaultTimeout covers the slowest documented call, getLiveServiceInfo, which the API says may take up to 15 seconds.

Variables

View Source
var ErrReadOnly = errors.New("client is read-only")

ErrReadOnly matches any ReadOnlyError via errors.Is.

View Source
var Ops = map[string]Op{

	"start":   {"start", RiskWrite, "Start the VPS", ""},
	"stop":    {"stop", RiskWrite, "Stop the VPS", ""},
	"restart": {"restart", RiskWrite, "Reboot the VPS", ""},
	"kill": {"kill", RiskDestructive, "Force-stop a VPS that will not stop normally",
		"unsaved data in the guest is lost"},

	"getServiceInfo":     {"getServiceInfo", RiskRead, "Plan, location, network and quota for the VPS", ""},
	"getLiveServiceInfo": {"getLiveServiceInfo", RiskRead, "Service info plus live guest status (slow: up to 15s)", ""},
	"getRateLimitStatus": {"getRateLimitStatus", RiskRead, "Remaining API rate-limit points", ""},

	"getAvailableOS": {"getAvailableOS", RiskRead, "Installed OS and installable templates", ""},
	"reinstallOS": {"reinstallOS", RiskDestructive, "Reinstall the operating system",
		"every byte on the VPS disk is erased"},
	"getSshKeys":    {"getSshKeys", RiskRead, "SSH keys in Hypervisor Vault and the billing portal", ""},
	"updateSshKeys": {"updateSshKeys", RiskWrite, "Replace the per-VM SSH keys used by reinstallOS", ""},
	"resetRootPassword": {"resetRootPassword", RiskDestructive, "Generate and set a new root password",
		"the current root password becomes unrecoverable and anything using it is locked out"},

	"getUsageGraphs":   {"getUsageGraphs", RiskRead, "Legacy usage graphs (obsolete; use getRawUsageStats)", ""},
	"getRawUsageStats": {"getRawUsageStats", RiskRead, "Per-interval CPU, network and disk usage samples", ""},
	"getAuditLog":      {"getAuditLog", RiskRead, "KiwiVM control-panel audit log", ""},

	"setHostname": {"setHostname", RiskWrite, "Set the VPS hostname", ""},
	"setPTR":      {"setPTR", RiskWrite, "Set the PTR (rDNS) record for an IP", ""},
	"iso/mount": {"iso/mount", RiskDestructive, "Boot from an ISO image instead of primary storage",
		"changes boot media; a VPS left on a wrong ISO is unreachable until corrected"},
	"iso/unmount": {"iso/unmount", RiskDestructive, "Remove the ISO and boot from primary storage",
		"changes boot media; requires a full shutdown and restart"},

	"basicShell/cd": {"basicShell/cd", RiskRead, "Resolve a directory change inside the VPS", ""},
	"basicShell/exec": {"basicShell/exec", RiskDestructive, "Run a shell command inside the VPS as root (synchronous)",
		"arbitrary root code execution; effects are entirely up to the command"},
	"shellScript/exec": {"shellScript/exec", RiskDestructive, "Run a shell script inside the VPS as root (asynchronous)",
		"arbitrary root code execution; runs detached with no way to recall it"},

	"snapshot/list":         {"snapshot/list", RiskRead, "List snapshots", ""},
	"snapshot/create":       {"snapshot/create", RiskWrite, "Create a snapshot", ""},
	"snapshot/toggleSticky": {"snapshot/toggleSticky", RiskWrite, "Protect a snapshot from automatic purge, or stop protecting it", ""},
	"snapshot/export":       {"snapshot/export", RiskWrite, "Mint a transfer token for a snapshot", ""},
	"snapshot/import":       {"snapshot/import", RiskWrite, "Import a snapshot from another instance", ""},
	"snapshot/delete": {"snapshot/delete", RiskDestructive, "Delete a snapshot",
		"the snapshot cannot be recovered"},
	"snapshot/restore": {"snapshot/restore", RiskDestructive, "Restore a snapshot over the VPS",
		"current VPS data is overwritten by the snapshot"},
	"backup/list":           {"backup/list", RiskRead, "List automatic backups", ""},
	"backup/copyToSnapshot": {"backup/copyToSnapshot", RiskWrite, "Copy an automatic backup into a restorable snapshot", ""},

	"ipv6/add": {"ipv6/add", RiskWrite, "Allocate an IPv6 /64 subnet", ""},
	"ipv6/delete": {"ipv6/delete", RiskDestructive, "Release an IPv6 /64 subnet",
		"the subnet returns to the pool and will not be reissued to you"},
	"privateIp/getAvailableIps": {"privateIp/getAvailableIps", RiskRead, "List assignable private IPv4 addresses", ""},
	"privateIp/assign":          {"privateIp/assign", RiskWrite, "Assign a private IPv4 address", ""},
	"privateIp/delete":          {"privateIp/delete", RiskWrite, "Remove a private IPv4 address", ""},

	"migrate/getLocations": {"migrate/getLocations", RiskRead, "List migration target locations", ""},
	"migrate/start": {"migrate/start", RiskDestructive, "Migrate the VPS to another location",
		"every IPv4 address is replaced; the old addresses are not recoverable"},
	"cloneFromExternalServer": {"cloneFromExternalServer", RiskDestructive, "Clone a remote server into this VPS (OpenVZ only)",
		"the current VPS contents are replaced by the remote server's"},

	"getSuspensionDetails": {"getSuspensionDetails", RiskRead, "Suspensions, abuse points and evidence", ""},
	"getPolicyViolations":  {"getPolicyViolations", RiskRead, "Active policy violations awaiting resolution", ""},
	"unsuspend": {"unsuspend", RiskDestructive, "Clear an abuse case and unsuspend the VPS",
		"consumes a one-time case resolution that cannot be re-opened through the API"},
	"resolvePolicyViolation": {"resolvePolicyViolation", RiskDestructive, "Mark a policy violation resolved",
		"consumes a one-time case resolution that cannot be re-opened through the API"},

	"kiwivm/getNotificationPreferences": {"kiwivm/getNotificationPreferences", RiskRead, "Email notification preferences", ""},
	"kiwivm/setNotificationPreferences": {"kiwivm/setNotificationPreferences", RiskWrite, "Change email notification preferences", ""},
}

Ops is the registry of every endpoint this package can call, keyed by endpoint path. It is the single source of truth for risk: the client gate, the CLI confirmation prompts, the MCP tool list, and the generated docs all read from here.

Functions

func IsAuth

func IsAuth(err error) bool

IsAuth reports whether err is KiwiVM rejecting the credentials. Note that KiwiVM returns the same code for a wrong api_key and a wrong veid, so a true result means "this pair does not work", not "this key is bad".

func IsLocked

func IsLocked(err error) bool

IsLocked reports whether the VPS is busy with another task. The error's Locking field carries progress when KiwiVM supplies it.

func IsMissingParam

func IsMissingParam(err error) bool

IsMissingParam reports whether a required parameter was absent.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether KiwiVM dropped the request for rate pressure. KiwiVM signals this with HTTP 429 rather than an error code; call Client.RateLimitStatus to see the remaining budget.

func IsReadOnly

func IsReadOnly(err error) bool

IsReadOnly reports whether err is a read-only refusal.

func IsTransient

func IsTransient(err error) bool

IsTransient reports whether err is worth retrying unchanged. Rate limiting counts: the budget refills on its own.

Types

type APIError

type APIError struct {
	// Op is the endpoint that failed.
	Op string `json:"op"`
	// Code is the value of the response's "error" field.
	Code int `json:"code"`
	// Message is KiwiVM's human-readable detail, when present.
	Message string `json:"message,omitempty"`
	// Additional is the response's additionalErrorInfo field.
	Additional string `json:"additionalErrorInfo,omitempty"`
	// Locking carries task progress when the VPS is locked.
	Locking *LockingInfo `json:"additionalLockingInfo,omitempty"`
}

APIError is a non-zero "error" field in a KiwiVM response.

func APIErrorFrom

func APIErrorFrom(err error) (*APIError, bool)

APIErrorFrom extracts the *APIError from err, if there is one.

func (*APIError) Error

func (e *APIError) Error() string

type AuditEntry

type AuditEntry struct {
	Timestamp     Int    `json:"timestamp"`
	RequestorIPv4 Int    `json:"requestor_ipv4"`
	Type          Int    `json:"type"`
	Summary       string `json:"summary"`
}

AuditEntry is one KiwiVM control-panel event.

func (AuditEntry) RequestorIP

func (a AuditEntry) RequestorIP() string

RequestorIP renders the requestor address, which KiwiVM encodes as a 32-bit integer rather than a string. Returns "" when out of range.

func (AuditEntry) Time

func (a AuditEntry) Time() time.Time

Time returns the event time.

type AuditLog

type AuditLog struct {
	LogEntries []AuditEntry `json:"log_entries"`
}

AuditLog is the response from getAuditLog.

type AvailableOS

type AvailableOS struct {
	Installed string  `json:"installed"`
	Templates Strings `json:"templates"`
}

AvailableOS is the installed OS plus the installable templates.

type Backup

type Backup struct {
	// Token identifies the backup for backup/copyToSnapshot. KiwiVM
	// usually supplies it as the map key rather than a field, so prefer
	// [BackupList.Sorted], which fills it in.
	Token     string `json:"backupToken"`
	Size      Int    `json:"size"`
	OS        string `json:"os"`
	MD5       string `json:"md5"`
	Timestamp Int    `json:"timestamp"`
}

Backup is one automatic backup.

func (Backup) Time

func (b Backup) Time() time.Time

Time returns when the backup was taken.

type BackupList

type BackupList struct {
	Backups Map[Backup] `json:"backups"`
}

BackupList is the response from backup/list. KiwiVM returns backups as an object keyed by token.

func (*BackupList) Sorted

func (b *BackupList) Sorted() []Backup

Sorted returns the backups newest first, each with Token populated.

type Bandwidth

type Bandwidth struct {
	// Used, Total and Free are bytes with the multiplier applied.
	Used  int64 `json:"used"`
	Total int64 `json:"total"`
	Free  int64 `json:"free"`
	// Percent is Used/Total as 0-100. The multiplier scales both sides
	// equally, so this is the one figure that is right regardless of
	// how the multiplier is interpreted.
	Percent float64 `json:"percent"`
	// Multiplier is the location's bandwidth accounting coefficient.
	Multiplier int64 `json:"multiplier"`
	// ResetsAt is when the counter rolls over; zero if unknown.
	ResetsAt time.Time `json:"resetsAt"`
}

Bandwidth is the monthly transfer picture, multiplier applied.

func (Bandwidth) ResetsIn

func (b Bandwidth) ResetsIn() time.Duration

ResetsIn returns the time until the transfer counter resets, or zero if the reset time is unknown or already past.

type Bool

type Bool bool

Bool is a JSON boolean that KiwiVM may send as 0/1 or "0"/"1".

func (Bool) Bool

func (v Bool) Bool() bool

Bool returns the value as a plain bool.

func (Bool) MarshalJSON

func (v Bool) MarshalJSON() ([]byte, error)

func (*Bool) UnmarshalJSON

func (v *Bool) UnmarshalJSON(b []byte) error

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the KiwiVM API for exactly one VPS. It is safe for concurrent use.

func New

func New(veid, apiKey string, opts ...Option) *Client

New builds a client for one VPS. veid and apiKey come as a pair from the KiwiVM panel; the same api_key with the wrong veid authenticates as neither.

func (*Client) AddIPv6

func (c *Client) AddIPv6(ctx context.Context) (*IPv6Added, error)

AddIPv6 allocates a new IPv6 /64 subnet, up to the plan's limit.

func (*Client) AssignPrivateIP

func (c *Client) AssignPrivateIP(ctx context.Context, ip string) (*PrivateIPsAssigned, error)

AssignPrivateIP assigns a private IPv4 address. An empty ip lets KiwiVM pick one.

func (*Client) AuditLog

func (c *Client) AuditLog(ctx context.Context) (*AuditLog, error)

AuditLog returns the KiwiVM control-panel audit log.

func (*Client) AvailableOS

func (c *Client) AvailableOS(ctx context.Context) (*AvailableOS, error)

AvailableOS returns the installed OS and the installable templates.

func (*Client) AvailablePrivateIPs

func (c *Client) AvailablePrivateIPs(ctx context.Context) (*PrivateIPsAvailable, error)

AvailablePrivateIPs lists private IPv4 addresses free to assign.

func (*Client) Backups

func (c *Client) Backups(ctx context.Context) (*BackupList, error)

Backups lists the automatic backups KiwiVM holds.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the API root in use.

func (*Client) Can

func (c *Client) Can(endpoint string) (bool, error)

Can reports whether the named endpoint would be permitted on this client, and why not when it would not. Callers building a tool list for an agent should use this rather than advertising operations that are certain to fail.

Example

Ops is the registry every surface reads. Building a menu of what a client may do means asking it, not guessing.

package main

import (
	"fmt"

	"github.com/lroolle/bwg-cli/kiwivm"
)

func main() {
	c := kiwivm.New("1347645", "private_key", kiwivm.ReadOnly())

	for _, endpoint := range []string{"getServiceInfo", "snapshot/create", "reinstallOS"} {
		op, _ := kiwivm.LookupOp(endpoint)
		allowed, _ := c.Can(endpoint)
		fmt.Printf("%-16s %-12s allowed=%v\n", endpoint, op.Risk, allowed)
	}
}
Output:
getServiceInfo   read         allowed=true
snapshot/create  write        allowed=false
reinstallOS      destructive  allowed=false

func (*Client) CloneFromExternalServer

func (c *Client) CloneFromExternalServer(ctx context.Context, ip, sshPort, rootPassword string) error

CloneFromExternalServer replaces this VPS with a copy of a remote server reachable over SSH. OpenVZ only.

func (*Client) CopyBackupToSnapshot

func (c *Client) CopyBackupToSnapshot(ctx context.Context, backupToken string) error

CopyBackupToSnapshot turns an automatic backup into a restorable snapshot. It does not restore anything by itself.

func (*Client) CreateSnapshot

func (c *Client) CreateSnapshot(ctx context.Context, description string) (*SnapshotCreated, error)

CreateSnapshot starts a snapshot task. description is optional. KiwiVM locks the VPS for the duration and emails on completion.

func (*Client) DeleteIPv6

func (c *Client) DeleteIPv6(ctx context.Context, subnet string) error

DeleteIPv6 releases an IPv6 /64 subnet back to the pool.

func (*Client) DeletePrivateIP

func (c *Client) DeletePrivateIP(ctx context.Context, ip string) error

DeletePrivateIP removes a private IPv4 address from the VPS.

func (*Client) DeleteSnapshot

func (c *Client) DeleteSnapshot(ctx context.Context, fileName string) error

DeleteSnapshot removes a snapshot by its fileName. It cannot be recovered.

func (*Client) ExportSnapshot

func (c *Client) ExportSnapshot(ctx context.Context, fileName string) (*SnapshotExport, error)

ExportSnapshot mints a token another instance can import with Client.ImportSnapshot.

func (*Client) ImportSnapshot

func (c *Client) ImportSnapshot(ctx context.Context, sourceVeid, sourceToken string) error

ImportSnapshot pulls a snapshot from another instance. Both the source VEID and the token come from Client.ExportSnapshot run against that instance.

func (*Client) IsReadOnly

func (c *Client) IsReadOnly() bool

IsReadOnly reports whether this client refuses non-read operations.

func (*Client) Kill

func (c *Client) Kill(ctx context.Context) error

Kill force-stops a VPS that will not stop normally. Unsaved data in the guest is lost.

func (*Client) LiveServiceInfo

func (c *Client) LiveServiceInfo(ctx context.Context) (*LiveServiceInfo, error)

LiveServiceInfo returns Client.ServiceInfo plus guest-reported state. KiwiVM documents this call as taking up to 15 seconds.

func (*Client) MigrateLocations

func (c *Client) MigrateLocations(ctx context.Context) (*MigrateLocations, error)

MigrateLocations lists the locations this VPS can move to.

func (*Client) MountISO

func (c *Client) MountISO(ctx context.Context, iso string) error

MountISO sets the VPS to boot from an ISO image. The VPS must be fully shut down first and restarted afterwards.

func (*Client) NotificationPreferences

func (c *Client) NotificationPreferences(ctx context.Context) (*NotificationPreferences, error)

NotificationPreferences returns the email notification settings.

func (*Client) PolicyViolations

func (c *Client) PolicyViolations(ctx context.Context) (*PolicyViolations, error)

PolicyViolations returns violations awaiting resolution.

func (*Client) RateLimitStatus

func (c *Client) RateLimitStatus(ctx context.Context) (*RateLimit, error)

RateLimitStatus returns the remaining API budget. It costs a point itself, so polling it in a loop is self-defeating.

func (*Client) Raw

func (c *Client) Raw(ctx context.Context, endpoint string, params url.Values) (json.RawMessage, error)

Raw calls any registered endpoint and returns KiwiVM's response body verbatim, for callers that need a field this package does not model yet. veid and api_key are added automatically.

The risk gate still applies: a read-only client refuses a non-read endpoint here exactly as it does through the typed methods. An unregistered endpoint is rejected, so Raw cannot be used to reach something whose risk nobody has classified.

Non-zero "error" values are returned as *APIError, the same as elsewhere; the body is returned only on success.

func (*Client) RawUsageStats

func (c *Client) RawUsageStats(ctx context.Context) (*UsageStats, error)

RawUsageStats returns the sampled CPU, network and disk series.

func (*Client) ReinstallOS

func (c *Client) ReinstallOS(ctx context.Context, os string) (*ReinstallResult, error)

ReinstallOS reinstalls the operating system, erasing the disk. os must be one of the templates from Client.AvailableOS.

The returned root password is shown once and is not retrievable afterwards — callers must surface it immediately.

func (*Client) ResetRootPassword

func (c *Client) ResetRootPassword(ctx context.Context) (*RootPassword, error)

ResetRootPassword generates a new root password and returns it. The previous password is unrecoverable.

func (*Client) ResolvePolicyViolation

func (c *Client) ResolvePolicyViolation(ctx context.Context, recordID string) error

ResolvePolicyViolation marks a violation resolved, which is what stops the pending suspension. Only cases where PolicyViolation.APIResolvable reports true can be resolved this way.

func (*Client) Restart

func (c *Client) Restart(ctx context.Context) error

Restart reboots the VPS.

func (*Client) RestoreSnapshot

func (c *Client) RestoreSnapshot(ctx context.Context, fileName string) error

RestoreSnapshot overwrites the VPS with a snapshot.

func (*Client) SSHKeys

func (c *Client) SSHKeys(ctx context.Context) (*SSHKeys, error)

SSHKeys returns the keys reinstallOS would install.

func (*Client) ScriptExec

func (c *Client) ScriptExec(ctx context.Context, script string) (*ScriptExec, error)

ScriptExec runs a shell script inside the VPS as root, detached, and returns the name of the log file it writes to.

func (*Client) ServiceInfo

func (c *Client) ServiceInfo(ctx context.Context) (*ServiceInfo, error)

ServiceInfo returns plan, location, network and quota state.

func (*Client) SetHostname

func (c *Client) SetHostname(ctx context.Context, hostname string) error

SetHostname sets the VPS hostname recorded by KiwiVM. It does not change the hostname inside a running guest.

func (*Client) SetNotificationPreferences

func (c *Client) SetNotificationPreferences(ctx context.Context, prefs map[string]bool) (*NotificationUpdate, error)

SetNotificationPreferences enables or disables notifications by preference ID. IDs come from Client.NotificationPreferences.

KiwiVM silently ignores unknown IDs, so compare the returned Updated map against what was submitted rather than assuming success.

func (*Client) SetPTR

func (c *Client) SetPTR(ctx context.Context, ip, ptr string) error

SetPTR sets the PTR (rDNS) record for an IP. Check ServiceInfo.RDNSAPIAvailable first: not every plan allows it.

func (*Client) SetSnapshotSticky

func (c *Client) SetSnapshotSticky(ctx context.Context, fileName string, sticky bool) error

SetSnapshotSticky protects a snapshot from automatic purge, or stops protecting it.

func (*Client) ShellCD

func (c *Client) ShellCD(ctx context.Context, currentDir, newDir string) (*ShellCD, error)

ShellCD resolves a directory change inside the VPS, for building an interactive shell on top of Client.ShellExec. It changes nothing.

func (*Client) ShellExec

func (c *Client) ShellExec(ctx context.Context, command string) (*ShellExec, error)

ShellExec runs a command inside the VPS as root and waits for it.

KiwiVM reuses the response envelope for the command's result, so a non-zero exit status arrives as ShellExec.ExitStatus rather than as a Go error. The error return covers transport and permission failures only — always check ExitStatus too.

func (*Client) Snapshots

func (c *Client) Snapshots(ctx context.Context) (*SnapshotList, error)

Snapshots lists stored snapshots.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

Start boots the VPS.

func (*Client) StartMigration

func (c *Client) StartMigration(ctx context.Context, location string) (*MigrateStarted, error)

StartMigration moves the VPS to another location. Every IPv4 address is replaced; the old ones do not come back.

func (*Client) Stop

func (c *Client) Stop(ctx context.Context) error

Stop shuts the VPS down. Client.Start undoes it.

func (*Client) SuspensionDetails

func (c *Client) SuspensionDetails(ctx context.Context) (*SuspensionDetails, error)

SuspensionDetails returns suspensions, abuse points and evidence.

func (*Client) UnmountISO

func (c *Client) UnmountISO(ctx context.Context) error

UnmountISO restores booting from primary storage. The VPS must be fully shut down first and restarted afterwards.

func (*Client) Unsuspend

func (c *Client) Unsuspend(ctx context.Context, recordID string) error

Unsuspend clears an abuse case and lifts the suspension. Only cases where Suspension.APIResolvable reports true can be cleared this way.

func (*Client) UpdateSSHKeys

func (c *Client) UpdateSSHKeys(ctx context.Context, keys []string) error

UpdateSSHKeys replaces the per-VM keys held in Hypervisor Vault. These shadow the account-level keys entirely during a reinstall. Passing no keys clears them, which restores the account-level keys.

func (*Client) UsageGraphs

func (c *Client) UsageGraphs(ctx context.Context) (map[string]any, error)

UsageGraphs returns the legacy graph payload. KiwiVM marks this obsolete; use Client.RawUsageStats. The shape is undocumented, so it comes back as decoded JSON rather than a typed struct.

func (*Client) VEID

func (c *Client) VEID() string

VEID returns the VPS ID this client is bound to.

type IPv6Added

type IPv6Added struct {
	AssignedSubnet string `json:"assigned_subnet"`
}

IPv6Added is the response from ipv6/add.

type Int

type Int int64

Int is a JSON number that KiwiVM may send as a string.

func (Int) Int

func (i Int) Int() int

Int returns the value as a plain int.

func (Int) Int64

func (i Int) Int64() int64

Int64 returns the value as a plain int64.

func (Int) MarshalJSON

func (i Int) MarshalJSON() ([]byte, error)

func (*Int) UnmarshalJSON

func (i *Int) UnmarshalJSON(b []byte) error

type LiveServiceInfo

type LiveServiceInfo struct {
	ServiceInfo

	IsCPUThrottled Bool `json:"is_cpu_throttled"`
	SSHPort        Int  `json:"ssh_port"`

	// OpenVZ
	VzStatus map[string]any `json:"vz_status,omitempty"`
	VzQuota  map[string]any `json:"vz_quota,omitempty"`

	// KVM
	VeStatus            string `json:"ve_status,omitempty"`
	VeMac1              string `json:"ve_mac1,omitempty"`
	VeUsedDiskSpaceB    Int    `json:"ve_used_disk_space_b,omitempty"`
	VeDiskQuotaGB       Int    `json:"ve_disk_quota_gb,omitempty"`
	IsDiskThrottled     Bool   `json:"is_disk_throttled,omitempty"`
	LiveHostname        string `json:"live_hostname,omitempty"`
	LoadAverage         string `json:"load_average,omitempty"`
	MemAvailableKB      Int    `json:"mem_available_kb,omitempty"`
	SwapTotalKB         Int    `json:"swap_total_kb,omitempty"`
	SwapAvailableKB     Int    `json:"swap_available_kb,omitempty"`
	ScreendumpPNGBase64 string `json:"screendump_png_base64,omitempty"`
}

LiveServiceInfo is ServiceInfo plus guest-reported state, from getLiveServiceInfo. Which hypervisor-specific group is populated depends on VMType.

func (*LiveServiceInfo) DiskTotalBytes

func (l *LiveServiceInfo) DiskTotalBytes() (int64, bool)

DiskTotalBytes returns the disk quota, preferring the live figure and falling back to the plan.

func (*LiveServiceInfo) DiskUsedBytes

func (l *LiveServiceInfo) DiskUsedBytes() (int64, bool)

DiskUsedBytes returns occupied disk space, and false when the hypervisor does not report it. OpenVZ exposes this through vz_quota in 1 KiB blocks rather than a dedicated byte field.

func (*LiveServiceInfo) MemUsedBytes

func (l *LiveServiceInfo) MemUsedBytes() (int64, bool)

MemUsedBytes returns used RAM, and false when the hypervisor does not report enough to compute it.

func (*LiveServiceInfo) Running

func (l *LiveServiceInfo) Running() bool

Running reports whether the guest is up.

func (*LiveServiceInfo) State

func (l *LiveServiceInfo) State() string

State returns a normalized power state: "running", "stopped", "starting", or "unknown". KVM reports it directly; OpenVZ has no state field, so a populated vz_status is the only signal.

type LockingInfo

type LockingInfo struct {
	LastStatusUpdateSecondsAgo int    `json:"last_status_update_s_ago"`
	CompletedPercent           int    `json:"completed_percent"`
	FriendlyProgressMessage    string `json:"friendly_progress_message"`
}

LockingInfo reports progress of the task currently holding the VPS.

type Map

type Map[V any] map[string]V

Map is a JSON object that KiwiVM may serialize as `[]` when empty, because that is what PHP's json_encode does to an empty associative array. Decoding into a plain map fails on those responses.

func (Map[V]) Keys

func (m Map[V]) Keys() []string

Keys returns the map's keys sorted, so output and tests do not depend on Go's map iteration order.

func (*Map[V]) UnmarshalJSON

func (m *Map[V]) UnmarshalJSON(b []byte) error

type MigrateLocations

type MigrateLocations struct {
	CurrentLocation         string      `json:"currentLocation"`
	Locations               Strings     `json:"locations"`
	Descriptions            Map[string] `json:"descriptions"`
	DataTransferMultipliers Map[Int]    `json:"dataTransferMultipliers"`
}

MigrateLocations is the response from migrate/getLocations.

type MigrateStarted

type MigrateStarted struct {
	NotificationEmail string  `json:"notificationEmail"`
	NewIPs            Strings `json:"newIps"`
}

MigrateStarted is the response from migrate/start.

type NotificationPreference

type NotificationPreference struct {
	FriendlyDescription string `json:"friendly_description"`
	IsEnabled           Bool   `json:"is_enabled"`
	ChangedTimestamp    Int    `json:"changed_timestamp"`
	SValue              string `json:"s_value"`
}

NotificationPreference is one email notification setting.

type NotificationPreferences

type NotificationPreferences struct {
	EmailPreferences  Map[Map[NotificationPreference]] `json:"email_preferences"`
	NotificationEmail string                           `json:"notificationEmail"`
}

NotificationPreferences is the response from kiwivm/getNotificationPreferences. KiwiVM groups preferences by category, so EmailPreferences is category -> preference ID -> value.

func (*NotificationPreferences) Flat

Flat returns every preference keyed by its ID, dropping the category grouping that only matters for panel layout.

type NotificationUpdate

type NotificationUpdate struct {
	Submitted    Map[Int]    `json:"submitted_email_preferences"`
	Updated      Map[Int]    `json:"updated_email_preferences"`
	Descriptions Map[string] `json:"friendly_descriptions"`
}

NotificationUpdate is the response from kiwivm/setNotificationPreferences. Updated lists only what actually changed, which can be narrower than Submitted.

type Nullroute

type Nullroute struct {
	// IP is filled in from the map key.
	IP        string `json:"ip"`
	Timestamp Int    `json:"nullroute_timestamp"`
	DurationS Int    `json:"nullroute_duration_s"`
	// Log is the raw packet dump KiwiVM captured. It can be long.
	Log string `json:"log,omitempty"`
}

Nullroute describes an IP nullrouted during a (D)DoS attack.

func (Nullroute) ExpiresAt

func (n Nullroute) ExpiresAt() (time.Time, bool)

ExpiresAt returns when the nullroute lifts, and whether KiwiVM gave enough detail to say.

func (Nullroute) StartedAt

func (n Nullroute) StartedAt() time.Time

StartedAt returns when the nullroute began.

type Nullroutes

type Nullroutes map[string]Nullroute

Nullroutes maps an IP address to its nullroute detail. KiwiVM sends `[]` when nothing is nullrouted, an object when something is, and occasionally a bare array of IPs carrying no detail.

func (*Nullroutes) UnmarshalJSON

func (n *Nullroutes) UnmarshalJSON(b []byte) error

type Op

type Op struct {
	// Endpoint is the path under the API base URL, e.g. "snapshot/list".
	Endpoint string `json:"endpoint"`
	// Risk is what this endpoint can do. See [Risk].
	Risk Risk `json:"risk"`
	// Summary is a one-line description, reused by the CLI and the MCP
	// tool list so all three surfaces describe an endpoint identically.
	Summary string `json:"summary"`
	// Why explains a Destructive classification. Empty for other risks.
	Why string `json:"why,omitempty"`
}

Op describes one KiwiVM endpoint.

func ListOps

func ListOps() []Op

ListOps returns every registered operation sorted by endpoint, for stable output in docs, `bwg api ops` and the MCP tool list.

func LookupOp

func LookupOp(endpoint string) (Op, bool)

LookupOp returns the registered operation for an endpoint.

type Option

type Option func(*Client)

Option configures a Client.

func ReadOnly

func ReadOnly() Option

ReadOnly builds a client that refuses every operation classified above RiskRead, returning *ReadOnlyError before any request is made. This is the strongest guarantee the package offers: it cannot be undone on an existing client, and it applies to the SDK, the CLI and the MCP server alike.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the API root. Used by tests and by anyone proxying the API.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies the HTTP client to use.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-request timeout. Ignored when the caller also supplies their own client via WithHTTPClient after this.

func WithTrace

func WithTrace(fn func(method, endpoint string, status int, dur time.Duration)) Option

WithTrace installs a callback invoked after every HTTP round trip. It never receives credentials — only method, endpoint, status and duration — so it is safe to log verbatim.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent sets the User-Agent header.

type PolicyViolation

type PolicyViolation struct {
	RecordID     Int    `json:"record_id"`
	Timestamp    Int    `json:"timestamp"`
	SuspendAt    Int    `json:"suspend_at"`
	Flag         string `json:"flag"`
	IsSoft       Bool   `json:"is_soft"`
	AbusePoints  Int    `json:"abuse_points"`
	EvidenceData string `json:"evidence_data"`
}

PolicyViolation is one unresolved policy violation.

func (PolicyViolation) APIResolvable

func (p PolicyViolation) APIResolvable() bool

APIResolvable reports whether Client.ResolvePolicyViolation can clear this case, or whether it needs a support ticket.

func (PolicyViolation) SuspendsAt

func (p PolicyViolation) SuspendsAt() (time.Time, bool)

SuspendsAt returns the deadline after which the service is suspended, and whether one was given.

type PolicyViolations

type PolicyViolations struct {
	TotalAbusePoints Int               `json:"total_abuse_points"`
	MaxAbusePoints   Int               `json:"max_abuse_points"`
	PolicyViolations []PolicyViolation `json:"policy_violations,omitempty"`
}

PolicyViolations is the response from getPolicyViolations.

type PrivateIPsAssigned

type PrivateIPsAssigned struct {
	AssignedIPs Strings `json:"assigned_ips"`
}

PrivateIPsAssigned is the response from privateIp/assign.

type PrivateIPsAvailable

type PrivateIPsAvailable struct {
	AvailableIPs Strings `json:"available_ips"`
}

PrivateIPsAvailable is the response from privateIp/getAvailableIps.

type RateLimit

type RateLimit struct {
	Remaining15Min Int `json:"remaining_points_15min"`
	Remaining24H   Int `json:"remaining_points_24h"`
}

RateLimit is the remaining API budget from getRateLimitStatus.

type ReadOnlyError

type ReadOnlyError struct {
	Op Op
}

ReadOnlyError is returned instead of performing a non-read operation on a client built with ReadOnly. No HTTP request is made.

func (*ReadOnlyError) Error

func (e *ReadOnlyError) Error() string

func (*ReadOnlyError) Is

func (e *ReadOnlyError) Is(target error) bool

Is lets errors.Is(err, ErrReadOnly) match a *ReadOnlyError.

type ReinstallResult

type ReinstallResult struct {
	RootPassword      string  `json:"rootPassword"`
	SSHPort           Int     `json:"sshPort"`
	SSHKeys           Strings `json:"sshKeys"`
	SSHKeysBrief      Strings `json:"sshKeysBrief"`
	NotificationEmail string  `json:"notificationEmail"`
}

ReinstallResult is what reinstallOS hands back. The root password appears exactly once, here; nothing retrieves it later.

type Risk

type Risk int

Risk classifies what an endpoint can do to a VPS. The dividing line between Write and Destructive is deliberately narrow:

Destructive = irreversible loss of data, identity, or access
              that no other call in this package can restore.

Stopping a VPS is a Write because Start undoes it. Deleting a snapshot is Destructive because nothing brings it back. Keeping the Destructive set small is what makes a Destructive confirmation mean something; a prompt that guards everything guards nothing.

const (
	// RiskRead observes state and changes nothing.
	RiskRead Risk = iota
	// RiskWrite changes state in a way another call can undo.
	RiskWrite
	// RiskDestructive irreversibly loses data, identity, or access.
	RiskDestructive
)

func (Risk) MarshalText

func (r Risk) MarshalText() ([]byte, error)

MarshalText renders the risk as its lowercase name in JSON.

func (Risk) String

func (r Risk) String() string

type RootPassword

type RootPassword struct {
	Password string `json:"password"`
}

RootPassword is the result of resetRootPassword.

type SSHKeys

type SSHKeys struct {
	Veid               string `json:"ssh_keys_veid"`
	User               string `json:"ssh_keys_user"`
	Preferred          string `json:"ssh_keys_preferred"`
	ShortenedVeid      string `json:"shortened_ssh_keys_veid"`
	ShortenedUser      string `json:"shortened_ssh_keys_user"`
	ShortenedPreferred string `json:"shortened_ssh_keys_preferred"`
}

SSHKeys are the keys reinstallOS will install, from both storage tiers. Each field is a newline-separated key list; the slice accessors split them.

func (*SSHKeys) PreferredSlice

func (k *SSHKeys) PreferredSlice() []string

PreferredSlice returns the keys reinstallOS will actually install. Per-VM keys shadow account-level keys entirely.

func (*SSHKeys) UserSlice

func (k *SSHKeys) UserSlice() []string

UserSlice returns the account-level keys from the billing portal.

func (*SSHKeys) VeidSlice

func (k *SSHKeys) VeidSlice() []string

VeidSlice returns the per-VM keys held in Hypervisor Vault.

type ScriptExec

type ScriptExec struct {
	// Log is the name of the output log file inside the VPS.
	Log string `json:"log"`
}

ScriptExec is the response from shellScript/exec.

type ServiceInfo

type ServiceInfo struct {
	VMType   string `json:"vm_type"`
	Hostname string `json:"hostname"`
	Plan     string `json:"plan"`
	OS       string `json:"os"`
	Email    string `json:"email"`

	NodeAlias         string `json:"node_alias"`
	NodeLocationID    string `json:"node_location_id"`
	NodeLocation      string `json:"node_location"`
	NodeDatacenter    string `json:"node_datacenter"`
	LocationIPv6Ready Bool   `json:"location_ipv6_ready"`

	PlanDisk Int `json:"plan_disk"`
	PlanRAM  Int `json:"plan_ram"`
	PlanSwap Int `json:"plan_swap"`

	// PlanMonthlyData and DataCounter are raw counters. Both are scaled
	// by MonthlyDataMultiplier for the figures KiwiVM displays; use
	// [ServiceInfo.Bandwidth] rather than reading them directly.
	PlanMonthlyData       Int `json:"plan_monthly_data"`
	DataCounter           Int `json:"data_counter"`
	MonthlyDataMultiplier Int `json:"monthly_data_multiplier"`
	DataNextReset         Int `json:"data_next_reset"`

	IPAddresses           Strings    `json:"ip_addresses"`
	PrivateIPAddresses    Strings    `json:"private_ip_addresses"`
	IPv6SitTunnelEndpoint string     `json:"ipv6_sit_tunnel_endpoint,omitempty"`
	IPNullroutes          Nullroutes `json:"ip_nullroutes"`
	PlanMaxIPv6s          Int        `json:"plan_max_ipv6s"`

	ISO1          string  `json:"iso1,omitempty"`
	ISO2          string  `json:"iso2,omitempty"`
	AvailableISOs Strings `json:"available_isos"`

	PlanPrivateNetworkAvailable     Bool        `json:"plan_private_network_available"`
	LocationPrivateNetworkAvailable Bool        `json:"location_private_network_available"`
	RDNSAPIAvailable                Bool        `json:"rdns_api_available"`
	PTR                             Map[string] `json:"ptr"`

	Suspended        Bool `json:"suspended"`
	PolicyViolation  Bool `json:"policy_violation"`
	SuspensionCount  Int  `json:"suspension_count"`
	TotalAbusePoints Int  `json:"total_abuse_points"`
	MaxAbusePoints   Int  `json:"max_abuse_points"`
}

ServiceInfo is the plan, location, network and quota state of a VPS, from getServiceInfo.

func (*ServiceInfo) AbusePercent

func (s *ServiceInfo) AbusePercent() float64

AbusePercent returns accumulated abuse points as a share of the plan's yearly limit, 0-100. Zero when the limit is unknown.

func (*ServiceInfo) Bandwidth

func (s *ServiceInfo) Bandwidth() Bandwidth

Bandwidth reports monthly transfer with the location multiplier applied to both the allowance and the counter, matching the KiwiVM panel.

Example

Bandwidth applies the location multiplier to both the allowance and the counter, which is what the KiwiVM panel shows. Percent is the figure to trust: the multiplier scales both sides equally.

package main

import (
	"fmt"

	"github.com/lroolle/bwg-cli/kiwivm"
)

func main() {
	const gib = 1024 * 1024 * 1024
	info := &kiwivm.ServiceInfo{
		PlanMonthlyData:       kiwivm.Int(1000 * gib),
		DataCounter:           kiwivm.Int(250 * gib),
		MonthlyDataMultiplier: 3,
	}

	b := info.Bandwidth()
	fmt.Printf("%d GiB of %d GiB (%.0f%%), multiplier %dx\n",
		b.Used/gib, b.Total/gib, b.Percent, b.Multiplier)
}
Output:
750 GiB of 3000 GiB (25%), multiplier 3x

func (*ServiceInfo) Healthy

func (s *ServiceInfo) Healthy() bool

Healthy reports whether nothing demands attention: not suspended, no open policy violation, no live nullroute.

func (*ServiceInfo) IPv4

func (s *ServiceInfo) IPv4() []string

IPv4 returns the assigned IPv4 addresses.

func (*ServiceInfo) IPv6

func (s *ServiceInfo) IPv6() []string

IPv6 returns the assigned IPv6 /64 subnets. KiwiVM mixes them into the same ip_addresses array as the IPv4 addresses.

func (*ServiceInfo) PrimaryIP

func (s *ServiceInfo) PrimaryIP() string

PrimaryIP returns the first IPv4 address, falling back to the first IPv6 subnet, or "" when the VPS has neither.

type ShellCD

type ShellCD struct {
	PWD string `json:"pwd"`
}

ShellCD is the response from basicShell/cd.

type ShellExec

type ShellExec struct {
	ExitStatus int    `json:"error"`
	Output     string `json:"message"`
}

ShellExec is the response from basicShell/exec. KiwiVM overloads the shared envelope here: "error" carries the command's exit status and "message" its console output, so a non-zero exit is not an API failure. Client.ShellExec accounts for that.

type Snapshot

type Snapshot struct {
	FileName        string `json:"fileName"`
	OS              string `json:"os"`
	Description     string `json:"description"`
	Size            Int    `json:"size"`
	Uncompressed    Int    `json:"uncompressed"`
	MD5             string `json:"md5"`
	Sticky          Bool   `json:"sticky"`
	PurgesIn        Int    `json:"purgesIn"`
	DownloadLink    string `json:"downloadLink"`
	DownloadLinkSSL string `json:"downloadLinkSSL"`
}

Snapshot is one stored snapshot.

func (Snapshot) PurgesAt

func (s Snapshot) PurgesAt() (time.Time, bool)

PurgesAt returns when an unprotected snapshot will be purged. Sticky snapshots are never purged, so ok is false for them.

type SnapshotCreated

type SnapshotCreated struct {
	NotificationEmail string `json:"notificationEmail"`
}

SnapshotCreated is the response from snapshot/create.

type SnapshotExport

type SnapshotExport struct {
	Token string `json:"token"`
}

SnapshotExport carries the token snapshot/import consumes.

type SnapshotList

type SnapshotList struct {
	Snapshots []Snapshot `json:"snapshots"`
}

SnapshotList is the response from snapshot/list.

type Strings

type Strings []string

Strings is a JSON array of strings that KiwiVM may send as null, as an object keyed by index, or with non-string members.

func (*Strings) UnmarshalJSON

func (s *Strings) UnmarshalJSON(b []byte) error

type Suspension

type Suspension struct {
	RecordID         Int    `json:"record_id"`
	Flag             string `json:"flag"`
	IsSoft           Bool   `json:"is_soft"`
	EvidenceRecordID Int    `json:"evidence_record_id"`
	AbusePoints      Int    `json:"abuse_points"`
}

Suspension is one outstanding suspension case.

func (Suspension) APIResolvable

func (s Suspension) APIResolvable() bool

APIResolvable reports whether Client.Unsuspend can clear this case, or whether it needs a support ticket.

type SuspensionDetails

type SuspensionDetails struct {
	SuspensionCount  Int          `json:"suspension_count"`
	TotalAbusePoints Int          `json:"total_abuse_points"`
	MaxAbusePoints   Int          `json:"max_abuse_points"`
	Suspensions      []Suspension `json:"suspensions,omitempty"`
	// Evidence maps an evidence record ID to the complaint text.
	Evidence Map[string] `json:"evidence,omitempty"`
}

SuspensionDetails is the response from getSuspensionDetails.

type TransportError

type TransportError struct {
	Op     string
	Status int // 0 when the request never completed
	Err    error
}

TransportError is a failure to get a usable answer out of the API: a dial error, a timeout, a 5xx, or a body that is not JSON. It says nothing about whether the credentials are valid.

func (*TransportError) Error

func (e *TransportError) Error() string

func (*TransportError) Unwrap

func (e *TransportError) Unwrap() error

type UsageSample

type UsageSample struct {
	Timestamp       Int `json:"timestamp"`
	CPUUsage        Int `json:"cpu_usage"`
	NetworkInBytes  Int `json:"network_in_bytes"`
	NetworkOutBytes Int `json:"network_out_bytes"`
	DiskReadBytes   Int `json:"disk_read_bytes"`
	DiskWriteBytes  Int `json:"disk_write_bytes"`
}

UsageSample is one interval of resource usage.

func (UsageSample) Time

func (u UsageSample) Time() time.Time

Time returns the sample's timestamp.

type UsageStats

type UsageStats struct {
	VMType string        `json:"vm_type"`
	Data   []UsageSample `json:"data"`
}

UsageStats is the sampled usage series from getRawUsageStats.

func (*UsageStats) Totals

func (u *UsageStats) Totals() (netIn, netOut, diskRead, diskWrite int64)

Totals sums network and disk bytes across every sample.

func (*UsageStats) Window

func (u *UsageStats) Window() (start, end time.Time)

Window returns the time span the samples cover.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL