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.



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/knife-proxmox-ve/api.rb', line 93

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

#start(node:, vmid:) ⇒ Object

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



80
81
82
# File 'lib/knife-proxmox-ve/api.rb', line 80

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.



86
87
88
# File 'lib/knife-proxmox-ve/api.rb', line 86

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:<node>:…) 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.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/knife-proxmox-ve/api.rb', line 110

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