Class: Pvectl::Repositories::Container

Inherits:
Base
  • Object
show all
Defined in:
lib/pvectl/repositories/container.rb,
sig/pvectl/repositories/container.rbs

Overview

Repository for LXC containers.

Uses the /cluster/resources API endpoint to list containers across the cluster. Filters to only include LXC containers (excludes QEMU VMs).

Examples:

Listing all containers

repo = Container.new(connection)
containers = repo.list
containers.each { |ct| puts "#{ct.vmid}: #{ct.name}" }

Listing containers on a specific node

containers = repo.list(node: "pve-node1")

Getting a single container

ct = repo.get(100)
puts ct.name if ct

See Also:

Instance Attribute Summary

Attributes inherited from Base

#connection

Instance Method Summary collapse

Methods inherited from Base

#extract_data, #initialize, #models_from, #unwrap

Constructor Details

This class inherits a constructor from Pvectl::Repositories::Base

Instance Method Details

#build_describe_model(basic_data, config, status, snapshots = [], tasks = [], firewall = {}) ⇒ Models::Container

Builds Container model with describe-specific attributes.

Parameters:

  • basic_data (Hash)

    basic container data from cluster/resources

  • config (Hash)

    container config from /nodes/node/lxc/ctid/config

  • status (Hash)

    container status from /nodes/node/lxc/ctid/status/current

  • snapshots (Array<Hash>) (defaults to: [])

    container snapshots

  • tasks (Array[Models::TaskEntry]) (defaults to: [])
  • firewall (Hash[Symbol, untyped]) (defaults to: {})

Returns:



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/pvectl/repositories/container.rb', line 455

def build_describe_model(basic_data, config, status, snapshots = [], tasks = [], firewall = {})
  network_interfaces = extract_network_interfaces(config)

  Models::Container.new(
    # Basic attributes from cluster/resources
    vmid: basic_data[:vmid],
    name: basic_data[:name],
    status: basic_data[:status],
    node: basic_data[:node],
    type: basic_data[:type],
    cpu: basic_data[:cpu],
    maxcpu: basic_data[:maxcpu],
    mem: basic_data[:mem],
    maxmem: basic_data[:maxmem],
    swap: basic_data[:swap],
    maxswap: basic_data[:maxswap],
    disk: basic_data[:disk],
    maxdisk: basic_data[:maxdisk],
    uptime: basic_data[:uptime],
    template: basic_data[:template],
    tags: basic_data[:tags],
    pool: basic_data[:pool],
    lock: basic_data[:lock],
    netin: basic_data[:netin],
    netout: basic_data[:netout],

    # Config attributes (kept for backward compat)
    ostype: config[:ostype],
    arch: config[:arch],
    unprivileged: config[:unprivileged],
    features: config[:features],
    rootfs: config[:rootfs],
    description: config[:description],
    hostname: config[:hostname],

    # Status attributes
    pid: status[:pid],
    ha: status[:ha],

    # Parsed network interfaces
    network_interfaces: network_interfaces,

    # Raw API data for comprehensive describe
    describe_data: { config: config, status: status, snapshots: snapshots, tasks: tasks, firewall: firewall }
  )
end

#build_model(data) ⇒ Models::Container

Builds Container model from API response data.

Parameters:

  • data (Hash)

    API response hash

Returns:



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/pvectl/repositories/container.rb', line 324

def build_model(data)
  Models::Container.new(
    vmid: data[:vmid],
    name: data[:name],
    status: data[:status],
    node: data[:node],
    cpu: data[:cpu],
    maxcpu: data[:maxcpu],
    mem: data[:mem],
    maxmem: data[:maxmem],
    swap: data[:swap],
    maxswap: data[:maxswap],
    disk: data[:disk],
    maxdisk: data[:maxdisk],
    uptime: data[:uptime],
    template: data[:template],
    tags: data[:tags],
    pool: data[:pool],
    lock: data[:lock],
    netin: data[:netin],
    netout: data[:netout],
    type: data[:type]
  )
end

#clone(ctid, node, new_ctid, options = {}) ⇒ String

Clones a container to create a new container.

Posts to /nodes/{node}/lxc/{ctid}/clone with the specified parameters. Note: LXC API uses hostname parameter (not name like QEMU).

Parameters:

  • ctid (Integer, String)

    source container identifier

  • node (String)

    source node name

  • new_ctid (Integer)

    CTID for the new cloned container

  • options (Hash) (defaults to: {})

    optional clone parameters

Options Hash (options):

  • :hostname (String)

    hostname for the new container

  • :target (String)

    target node for the clone

  • :storage (String)

    target storage for the clone

  • :full (Boolean)

    full clone (true) or linked clone (false)

  • :description (String)

    description for the new container

  • :pool (String)

    resource pool for the new container

Returns:

  • (String)

    Task UPID



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/pvectl/repositories/container.rb', line 131

def clone(ctid, node, new_ctid, options = {})
  params = { newid: new_ctid }
  params[:hostname] = options[:hostname] if options[:hostname]
  params[:target] = options[:target] if options[:target]
  params[:storage] = options[:storage] if options[:storage]
  params[:full] = options[:full] ? 1 : 0 if options.key?(:full)
  params[:description] = options[:description] if options[:description]
  params[:pool] = options[:pool] if options[:pool]

  connection.client["nodes/#{node}/lxc/#{ctid}/clone"].post(params)
end

#convert_to_template(ctid, node) ⇒ void

This method returns an undefined value.

Converts a container to a template.

This is an irreversible operation. The container will become read-only and can only be used as a source for cloning.

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name



151
152
153
# File 'lib/pvectl/repositories/container.rb', line 151

def convert_to_template(ctid, node)
  connection.client["nodes/#{node}/lxc/#{ctid}/template"].post({})
end

#create(node, ctid, params = {}) ⇒ String

Creates a new LXC container on the specified node.

Posts to /nodes/{node}/lxc with the container configuration parameters. The ctid is merged into params automatically.

Examples:

Create a basic container

repo.create("pve1", 200, { hostname: "web-ct", ostemplate: "local:vztmpl/debian-12.tar.zst" })
#=> "UPID:pve1:..."

Parameters:

  • node (String)

    target node name

  • ctid (Integer)

    container identifier

  • params (Hash) (defaults to: {})

    container configuration parameters (hostname, ostemplate, etc.)

Returns:

  • (String)

    Task UPID



273
274
275
276
# File 'lib/pvectl/repositories/container.rb', line 273

def create(node, ctid, params = {})
  api_params = params.merge(vmid: ctid)
  connection.client["nodes/#{node}/lxc"].post(api_params)
end

#delete(ctid, node, destroy_disks: true, purge: false, force: false) ⇒ String

Deletes a container from the cluster.

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

  • destroy_disks (Boolean) (defaults to: true)

    destroy unreferenced disks (default: true)

  • purge (Boolean) (defaults to: false)

    remove from HA, replication, backups (default: false)

  • force (Boolean) (defaults to: false)

    force removal (default: false)

  • destroy_disks: (Boolean) (defaults to: true)
  • purge: (Boolean) (defaults to: false)
  • force: (Boolean) (defaults to: false)

Returns:

  • (String)

    Task UPID



79
80
81
82
83
84
85
86
# File 'lib/pvectl/repositories/container.rb', line 79

def delete(ctid, node, destroy_disks: true, purge: false, force: false)
  params = {}
  params["destroy-unreferenced-disks"] = 1 if destroy_disks
  params[:purge] = 1 if purge
  params[:force] = 1 if force

  connection.client["nodes/#{node}/lxc/#{ctid}"].delete(params)
end

#describe(ctid) ⇒ Models::Container?

Describes a container with comprehensive details from multiple API endpoints.

Parameters:

  • ctid (Integer, String)

    container identifier

Returns:

  • (Models::Container, nil)

    Container model with full details, or nil if not found



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pvectl/repositories/container.rb', line 54

def describe(ctid)
  ctid = ctid.to_i

  basic_data = find_container_basic_data(ctid)
  return nil if basic_data.nil?

  node = basic_data[:node]

  config = fetch_config(node, ctid)
  status = fetch_status(node, ctid)
  snapshots = fetch_snapshots(node, ctid)
  tasks = fetch_tasks(node, ctid)
  firewall = fetch_firewall(node, ctid)

  build_describe_model(basic_data, config, status, snapshots, tasks, firewall)
end

#extract_network_interfaces(config) ⇒ Array<Hash>

Extracts network interfaces from config. Network interfaces are stored as net0, net1, etc. keys.

Parameters:

  • config (Hash)

    container config

Returns:

  • (Array<Hash>)

    parsed network interfaces



418
419
420
421
422
423
424
425
426
427
# File 'lib/pvectl/repositories/container.rb', line 418

def extract_network_interfaces(config)
  interfaces = []
  config.each do |key, value|
    next unless key.to_s.match?(/^net\d+$/)

    parsed = parse_network_interface(key.to_s, value)
    interfaces << parsed if parsed
  end
  interfaces
end

#feature_available?(ctid, node, feature, snapname: nil) ⇒ Hash

Checks whether a feature (clone, snapshot, copy) is available for a container.

Calls GET /nodes/{node}/lxc/{vmid}/feature with the feature and optional snapshot name. The Proxmox LXC API returns hasFeature (0/1). Unlike the QEMU variant, the LXC endpoint does not return a nodes array; this method always returns nodes: [] for shape compatibility.

Examples:

Check whether container 200 can be cloned

repo.feature_available?(200, "pve1", "clone")
#=> { available: true, nodes: [] }

Parameters:

  • ctid (Integer, String)

    container identifier

  • node (String)

    node currently hosting the container

  • feature (String)

    feature name (one of: clone, snapshot, copy)

  • snapname (String, nil) (defaults to: nil)

    snapshot name (required for some checks)

  • snapname: (String, nil) (defaults to: nil)

Returns:

  • (Hash)

    result with :available (Boolean) and :nodes (Array)



217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/pvectl/repositories/container.rb', line 217

def feature_available?(ctid, node, feature, snapname: nil)
  params = { feature: feature }
  params[:snapname] = snapname unless snapname.nil?

  response = connection.client["nodes/#{node}/lxc/#{ctid}/feature"].get(params: params)
  data = extract_data(response)

  {
    available: data[:hasFeature].to_i == 1,
    nodes: Array(data[:nodes])
  }
end

#fetch_config(node, ctid) ⇒ Hash

Fetches container configuration.

Parameters:

  • node (String)

    node name

  • ctid (Integer)

    container identifier

Returns:

  • (Hash)

    config data



311
312
313
314
315
316
# File 'lib/pvectl/repositories/container.rb', line 311

def fetch_config(node, ctid)
  resp = connection.client["nodes/#{node}/lxc/#{ctid}/config"].get
  extract_data(resp)
rescue StandardError
  {}
end

#fetch_firewall(node, ctid) ⇒ Hash

Fetches firewall configuration (options, rules, aliases, IP sets).

Parameters:

  • node (String)

    node name

  • ctid (Integer)

    container identifier

Returns:

  • (Hash)

    firewall data with :options, :rules, :aliases, :ipset keys



389
390
391
392
393
394
395
396
397
398
# File 'lib/pvectl/repositories/container.rb', line 389

def fetch_firewall(node, ctid)
  base = "nodes/#{node}/lxc/#{ctid}/firewall"
  options = (extract_data(connection.client["#{base}/options"].get) rescue {})
  rules = (unwrap(connection.client["#{base}/rules"].get) rescue [])
  aliases_data = (unwrap(connection.client["#{base}/aliases"].get) rescue [])
  ipset = (unwrap(connection.client["#{base}/ipset"].get) rescue [])
  { options: options, rules: rules, aliases: aliases_data, ipset: ipset }
rescue StandardError
  {}
end

#fetch_snapshots(node, ctid) ⇒ Array<Hash>

Fetches container snapshots.

Parameters:

  • node (String)

    node name

  • ctid (Integer)

    container identifier

Returns:

  • (Array<Hash>)

    snapshots list (excluding "current")



377
378
379
380
381
382
# File 'lib/pvectl/repositories/container.rb', line 377

def fetch_snapshots(node, ctid)
  resp = connection.client["nodes/#{node}/lxc/#{ctid}/snapshot"].get
  unwrap(resp).reject { |s| s[:name] == "current" }
rescue StandardError
  []
end

#fetch_status(node, ctid) ⇒ Hash

Fetches container runtime status.

Parameters:

  • node (String)

    node name

  • ctid (Integer)

    container identifier

Returns:

  • (Hash)

    status data



365
366
367
368
369
370
# File 'lib/pvectl/repositories/container.rb', line 365

def fetch_status(node, ctid)
  resp = connection.client["nodes/#{node}/lxc/#{ctid}/status/current"].get
  extract_data(resp)
rescue StandardError
  {}
end

#fetch_tasks(node, ctid, limit: 10) ⇒ Array<Models::TaskEntry>

Fetches recent task history for the container.

Parameters:

  • node (String)

    node name

  • ctid (Integer)

    container identifier

  • limit (Integer) (defaults to: 10)

    max entries (default 10)

  • limit: (Integer) (defaults to: 10)

Returns:



406
407
408
409
410
411
# File 'lib/pvectl/repositories/container.rb', line 406

def fetch_tasks(node, ctid, limit: 10)
  task_list_repo = TaskList.new(connection)
  task_list_repo.list(node: node, vmid: ctid, limit: limit)
rescue StandardError
  []
end

#find_container_basic_data(ctid) ⇒ Hash?

Finds container basic data from cluster resources.

Parameters:

  • ctid (Integer)

    container identifier

Returns:

  • (Hash, nil)

    container data or nil if not found



355
356
357
358
# File 'lib/pvectl/repositories/container.rb', line 355

def find_container_basic_data(ctid)
  response = connection.client["cluster/resources"].get(params: { type: "vm" })
  unwrap(response).find { |r| r[:type] == "lxc" && r[:vmid] == ctid }
end

#get(ctid) ⇒ Models::Container?

Gets a single container by CTID.

Parameters:

  • ctid (Integer, String)

    container identifier

Returns:



46
47
48
# File 'lib/pvectl/repositories/container.rb', line 46

def get(ctid)
  list.find { |ct| ct.vmid == ctid.to_i }
end

#list(node: nil) ⇒ Array<Models::Container>

Lists all containers in the cluster.

Uses /cluster/resources?type=lxc endpoint for efficient cluster-wide listing. Filters to only include LXC containers (type == "lxc").

Parameters:

  • node (String, nil) (defaults to: nil)

    filter by node name

  • node: (String, nil) (defaults to: nil)

Returns:



35
36
37
38
39
40
# File 'lib/pvectl/repositories/container.rb', line 35

def list(node: nil)
  response = connection.client["cluster/resources"].get(params: { type: "vm" })
  containers = unwrap(response).select { |r| r[:type] == "lxc" }
  containers = containers.select { |r| r[:node] == node } if node
  containers.map { |data| build_model(data) }
end

#migrate(ctid, node, params = {}) ⇒ String

Migrates a container to another node.

Parameters:

  • ctid (Integer, String)

    container identifier

  • node (String)

    current node name

  • params (Hash) (defaults to: {})

    migration parameters (:target, :online, :restart, :targetstorage)

Returns:

  • (String)

    Task UPID

Raises:

  • (ArgumentError)

    if node name or ctid format is invalid



162
163
164
165
166
167
168
169
170
171
# File 'lib/pvectl/repositories/container.rb', line 162

def migrate(ctid, node, params = {})
  unless node.match?(/\A[a-z][a-z0-9-]*\z/)
    raise ArgumentError, "Invalid node name: #{node}"
  end
  unless ctid.is_a?(Integer) && ctid.positive?
    raise ArgumentError, "Invalid CTID: #{ctid}"
  end

  connection.client["nodes/#{node}/lxc/#{ctid}/migrate"].post(params)
end

#move_volume(ctid, node, volume, target_storage, delete: false, bwlimit: nil) ⇒ String

Moves a container volume to a different storage on the same node.

POSTs to /nodes/{node}/lxc/{ctid}/move_volume with the volume identifier and target storage. The operation is asynchronous — the returned UPID can be polled via Repositories::Task to track completion.

Examples:

Move rootfs to a different storage

repo.move_volume(200, "pve1", "rootfs", "local-lvm")
#=> "UPID:pve1:..."

Move and delete source volume

repo.move_volume(200, "pve1", "mp0", "ceph-pool", delete: true)

Parameters:

  • ctid (Integer, String)

    container identifier

  • node (String)

    node name where the container currently resides

  • volume (String)

    volume identifier (e.g., "rootfs", "mp0")

  • target_storage (String)

    destination storage ID

  • delete (Boolean) (defaults to: false)

    delete the source volume after copy (default: false)

  • bwlimit (Integer, nil) (defaults to: nil)

    I/O bandwidth limit in KiB/s

  • delete: (Boolean) (defaults to: false)
  • bwlimit: (Integer, nil) (defaults to: nil)

Returns:

  • (String)

    Task UPID



193
194
195
196
197
198
199
# File 'lib/pvectl/repositories/container.rb', line 193

def move_volume(ctid, node, volume, target_storage, delete: false, bwlimit: nil)
  params = { volume: volume, storage: target_storage }
  params[:delete] = 1 if delete
  params[:bwlimit] = bwlimit if bwlimit

  connection.client["nodes/#{node}/lxc/#{ctid}/move_volume"].post(params)
end

#next_available_ctidInteger

Returns the next available CTID from the Proxmox cluster.

Uses the /cluster/nextid API endpoint which performs server-side allocation. This is more reliable than client-side scanning because it detects stale config files that don't appear in /cluster/resources.

Returns:

  • (Integer)

    next available CTID



237
238
239
# File 'lib/pvectl/repositories/container.rb', line 237

def next_available_ctid
  connection.client["cluster/nextid"].get.to_i
end

#parse_network_interface(key, value) ⇒ Hash

Parses a single network interface string.

Parameters:

  • key (String)

    interface key (e.g., "net0")

  • value (String)

    interface configuration string

Returns:

  • (Hash)

    parsed interface data



434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/pvectl/repositories/container.rb', line 434

def parse_network_interface(key, value)
  return nil unless value.is_a?(String)

  interface = { id: key }

  # Parse key=value pairs from the interface string
  value.split(",").each do |part|
    k, v = part.split("=", 2)
    interface[k.to_sym] = v if k && v
  end

  interface
end

#resize(ctid, node, disk:, size:) ⇒ nil

Resizes a container disk.

PUTs to /nodes/{node}/lxc/{ctid}/resize with disk identifier and new size. This is a synchronous, irreversible operation — Proxmox does not support shrinking disks.

Parameters:

  • ctid (Integer, String)

    container identifier

  • node (String)

    node name

  • disk (String)

    disk identifier (e.g., "rootfs", "mp0")

  • size (String)

    new size, absolute ("50G") or relative ("+10G")

  • disk: (String)
  • size: (String)

Returns:

  • (nil)


302
303
304
# File 'lib/pvectl/repositories/container.rb', line 302

def resize(ctid, node, disk:, size:)
  connection.client["nodes/#{node}/lxc/#{ctid}/resize"].put({ disk: disk, size: size })
end

#restart(ctid, node) ⇒ String

Restarts a container (reboot).

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

Returns:

  • (String)

    Task UPID



246
247
248
# File 'lib/pvectl/repositories/container.rb', line 246

def restart(ctid, node)
  connection.client["nodes/#{node}/lxc/#{ctid}/status/reboot"].post
end

#shutdown(ctid, node) ⇒ String

Shuts down a container gracefully.

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

Returns:

  • (String)

    Task UPID



111
112
113
# File 'lib/pvectl/repositories/container.rb', line 111

def shutdown(ctid, node)
  connection.client["nodes/#{node}/lxc/#{ctid}/status/shutdown"].post
end

#start(ctid, node) ⇒ String

Starts a container.

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

Returns:

  • (String)

    Task UPID



93
94
95
# File 'lib/pvectl/repositories/container.rb', line 93

def start(ctid, node)
  connection.client["nodes/#{node}/lxc/#{ctid}/status/start"].post
end

#stop(ctid, node) ⇒ String

Stops a container (hard stop).

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

Returns:

  • (String)

    Task UPID



102
103
104
# File 'lib/pvectl/repositories/container.rb', line 102

def stop(ctid, node)
  connection.client["nodes/#{node}/lxc/#{ctid}/status/stop"].post
end

#termproxy(ctid, node) ⇒ Hash

Opens a terminal proxy session for a container.

Parameters:

  • ctid (Integer, String)

    Container identifier

  • node (String)

    Node name

Returns:

  • (Hash)

    termproxy data with :port, :ticket, :user keys



255
256
257
258
# File 'lib/pvectl/repositories/container.rb', line 255

def termproxy(ctid, node)
  response = connection.client["nodes/#{node}/lxc/#{ctid}/termproxy"].post({})
  extract_data(response)
end

#update(ctid, node, params = {}) ⇒ nil

Updates an existing LXC container configuration.

PUTs to /nodes/{node}/lxc/{ctid}/config with configuration parameters. This is a synchronous operation — changes are applied immediately.

Parameters:

  • ctid (Integer, String)

    container identifier

  • node (String)

    node name

  • params (Hash) (defaults to: {})

    container configuration parameters to update

Returns:

  • (nil)


287
288
289
# File 'lib/pvectl/repositories/container.rb', line 287

def update(ctid, node, params = {})
  connection.client["nodes/#{node}/lxc/#{ctid}/config"].put(params)
end