Class: Knife::Proxmox::Api

Inherits:
Object
  • Object
show all
Defined in:
lib/knife-proxmox-ve/api.rb

Overview

Stateless map of the verified Proxmox VE endpoints onto a Knife::Proxmox::Client. Every method is a single round-trip with no cross-call sequencing (that lives in the Provisioner). It knows the shape of each endpoint's payload, applies the qemu filter where the cluster API mixes VM types, and keeps path segments injection-safe by routing node names and UPIDs through Client.encode_segment.

Constant Summary collapse

DEFAULT_TIMEOUT =

Polling defaults for #wait_task. A monotonic deadline (not wall clock) guards the timeout so a clock step cannot stretch or cut short the wait.

300
DEFAULT_INTERVAL =
2

Instance Method Summary collapse

Constructor Details

#initialize(client) ⇒ Api

Returns a new instance of Api.



19
20
21
# File 'lib/knife-proxmox-ve/api.rb', line 19

def initialize(client)
  @client = client
end

Instance Method Details

#agent_ip_addresses(node:, vmid:) ⇒ Object

Routable IPs reported by the guest agent, IPv4 first. Skips loopback (lo / 127.*) and IPv6 link-local (fe80::). Raises (NotFoundError/ApiError) when the agent is not available rather than returning an empty result that the caller would misread.



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/knife-proxmox-ve/api.rb', line 102

def agent_ip_addresses(node:, vmid:)
  result = @client.get(
    "/nodes/#{enc(node)}/qemu/#{enc(vmid)}/agent/network-get-interfaces"
  )
  interfaces = (result && result["result"]) || []

  addresses = interfaces.reject { |iface| iface["name"] == "lo" }.flat_map do |iface|
    (iface["ip-addresses"] || []).map { |a| a["ip-address"] }
  end

  addresses.compact.reject { |ip| routable_skip?(ip) }.sort_by { |ip| ipv4?(ip) ? 0 : 1 }
end

#clone(node:, vmid:, newid:, **params) ⇒ Object

Clone a template into a new VM. Returns the UPID of the async clone task.



66
67
68
69
70
71
# File 'lib/knife-proxmox-ve/api.rb', line 66

def clone(node:, vmid:, newid:, **params)
  @client.post(
    "/nodes/#{enc(node)}/qemu/#{enc(vmid)}/clone",
    { newid: }.merge(params)
  )
end

#find_template(name_or_vmid) ⇒ Object

Resolve a template by name or numeric vmid. Filters /cluster/resources down to qemu templates and matches either the name or the vmid. Raises NotFoundError if no template matches, so the caller never proceeds with a phantom source.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/knife-proxmox-ve/api.rb', line 26

def find_template(name_or_vmid)
  wanted = name_or_vmid.to_s
  entry = qemu_resources.find do |r|
    r["template"].to_i == 1 &&
      (r["name"].to_s == wanted || r["vmid"].to_s == wanted)
  end

  unless entry
    raise NotFoundError.new(
      "no qemu template matches #{name_or_vmid.inspect}", status: 404
    )
  end

  { vmid: entry["vmid"], node: entry["node"], name: entry["name"] }
end

#list_resources(template: nil) ⇒ Object

List qemu resources, optionally narrowed to templates (template: true) or plain VMs (template: false); nil keeps both. Returns a uniform Hash per entry with missing fields normalized to nil.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/knife-proxmox-ve/api.rb', line 45

def list_resources(template: nil)
  qemu_resources.select { |r| matches_template?(r, template) }.map do |r|
    {
      vmid:     r["vmid"],
      node:     r["node"],
      name:     r["name"],
      status:   r["status"],
      template: r["template"],
      maxmem:   r["maxmem"],
      maxdisk:  r["maxdisk"],
      uptime:   r["uptime"],
    }
  end
end

#next_idObject

The cluster's next free VMID. Proxmox returns it as a string; coerce to Integer.



61
62
63
# File 'lib/knife-proxmox-ve/api.rb', line 61

def next_id
  @client.get("/cluster/nextid").to_i
end

#resize_disk(node:, vmid:, disk:, size:) ⇒ Object

Grow a virtual disk. size uses Proxmox's own format: "+[KMGT]" adds to the current size, a bare "[KMGT]" sets the new absolute size (Proxmox refuses to shrink). PVE 8 runs the resize in a worker and returns a UPID; earlier releases do the work inline and return nil — the caller decides whether there is a task to wait on, since sequencing is not this class's job.



84
85
86
# File 'lib/knife-proxmox-ve/api.rb', line 84

def resize_disk(node:, vmid:, disk:, size:)
  @client.put("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/resize", { disk:, size: })
end

#start(node:, vmid:) ⇒ Object

Start a VM. Returns the UPID of the async start task.



89
90
91
# File 'lib/knife-proxmox-ve/api.rb', line 89

def start(node:, vmid:)
  @client.post("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/status/start")
end

#update_config(node:, vmid:, **params) ⇒ Object

Apply VM config (cores, memory, net0, cloud-init keys, ...). Only the supplied keys are sent, so a partial update never clears unrelated config.



75
76
77
# File 'lib/knife-proxmox-ve/api.rb', line 75

def update_config(node:, vmid:, **params)
  @client.post("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/config", params)
end

#vm_config(node:, vmid:) ⇒ Object

Current VM config Hash (string keys). Used to detect the cloud-init drive and to read back a static IP after configuration.



95
96
97
# File 'lib/knife-proxmox-ve/api.rb', line 95

def vm_config(node:, vmid:)
  @client.get("/nodes/#{enc(node)}/qemu/#{enc(vmid)}/config")
end

#wait_task(upid:, timeout: DEFAULT_TIMEOUT, interval: DEFAULT_INTERVAL, sleeper: nil) ⇒ Object

Poll a task to completion. The node is parsed from the UPID itself (UPID::...) so the caller cannot pass a mismatched node. Returns true on exitstatus "OK"; raises TaskError (with a scrubbed log) on any other exitstatus; raises TimeoutError once the monotonic deadline passes.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/knife-proxmox-ve/api.rb', line 119

def wait_task(upid:, timeout: DEFAULT_TIMEOUT, interval: DEFAULT_INTERVAL, sleeper: nil)
  node = node_from_upid(upid)
  sleeper ||= ->(seconds) { sleep(seconds) }
  deadline = monotonic_now + timeout

  loop do
    status = task_status(node, upid)
    if status["status"] == "stopped"
      return true if status["exitstatus"] == "OK"

      raise_task_error(node, upid, status["exitstatus"])
    end

    raise TimeoutError, "task #{upid} did not finish within #{timeout}s" if monotonic_now >= deadline

    sleeper.call(interval)
  end
end