Class: Pvectl::Repositories::Volume

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

Overview

Repository for virtual disk volumes attached to VMs and containers.

Aggregates volume data from two sources:

  • VM/CT config endpoints (parsed disk keys from configuration)
  • Storage content API (+/nodes/node/storage/storage/content+)

Uses composition: delegates to VmRepository and ContainerRepository for config fetching and node resolution.

Examples:

Listing volumes from VM config

repo = Volume.new(connection)
volumes = repo.list_from_config(resource_type: "vm", ids: [100, 101])
volumes.each { |v| puts "#{v.name}: #{v.storage}:#{v.volume_id} (#{v.size})" }

Finding a specific disk

volume = repo.find(resource_type: "vm", id: 100, disk_name: "scsi0")
puts volume.size if volume

See Also:

Constant Summary collapse

VM_DISK_PATTERN =

Pattern matching VM disk keys (scsi0, virtio1, ide2, sata3, efidisk0, tpmstate0)

/\A(?:scsi|virtio|ide|sata|efidisk|tpmstate)\d+\z/
CT_DISK_PATTERN =

Pattern matching container disk keys (rootfs, mp0, mp1, ...)

/\A(?:rootfs|mp\d+)\z/

Instance Attribute Summary

Attributes inherited from Base

#connection

Instance Method Summary collapse

Methods inherited from Base

#build_model, #extract_data, #get, #list, #models_from, #unwrap

Constructor Details

#initialize(connection, vm_repo: nil, container_repo: nil) ⇒ Volume

Creates a new Volume repository.



39
40
41
42
43
# File 'lib/pvectl/repositories/volume.rb', line 39

def initialize(connection, vm_repo: nil, container_repo: nil)
  super(connection)
  @vm_repo = vm_repo
  @container_repo = container_repo
end

Instance Method Details

#build_storage_volume(data, node, storage) ⇒ Models::Volume

Builds a Volume model from storage content API data.



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/pvectl/repositories/volume.rb', line 221

def build_storage_volume(data, node, storage)
  resource_type, resource_id = extract_resource_from_volume_id(data[:volid], data[:content])

  Models::Volume.new(
    volid: data[:volid],
    volume_id: data[:volid]&.split(":")&.last,
    storage: storage,
    size: format_bytes_to_size(data[:size]),
    format: data[:format],
    content: data[:content],
    resource_type: resource_type,
    resource_id: resource_id,
    node: node
  )
end

#container_repoRepositories::Container

Returns container repository instance. Uses injected repository if provided, otherwise creates new one.



121
122
123
# File 'lib/pvectl/repositories/volume.rb', line 121

def container_repo
  @container_repo ||= Repositories::Container.new(connection)
end

#extract_resource_from_volume_id(volume_id, content) ⇒ Array(String?, Integer?)

Extracts resource type and ID from a volume identifier.

Volume IDs follow patterns like:

  • "vm-100-disk-0" => ["vm", 100]
  • "subvol-200-disk-0" => ["ct", 200]
  • "base-100-disk-0" => ["vm", 100]


247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/pvectl/repositories/volume.rb', line 247

def extract_resource_from_volume_id(volume_id, content)
  return [nil, nil] unless volume_id

  vol_part = volume_id.split(":").last
  return [nil, nil] unless vol_part

  case vol_part
  when /\Avm-(\d+)-/
    ["vm", ::Regexp.last_match(1).to_i]
  when /\Asubvol-(\d+)-/
    ["ct", ::Regexp.last_match(1).to_i]
  when /\Abase-(\d+)-/
    type = content == "rootdir" ? "ct" : "vm"
    [type, ::Regexp.last_match(1).to_i]
  else
    [nil, nil]
  end
end

#extract_volumes(config, resource_type, resource_id, node) ⇒ Array<Models::Volume>

Extracts volume models from a config hash.

Iterates over config keys, selects disk-related entries, excludes CD-ROMs, and builds Volume models.



147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/pvectl/repositories/volume.rb', line 147

def extract_volumes(config, resource_type, resource_id, node)
  pattern = resource_type == "vm" ? VM_DISK_PATTERN : CT_DISK_PATTERN

  config.each_with_object([]) do |(key, value), volumes|
    key_str = key.to_s
    next unless key_str.match?(pattern)

    value_str = value.to_s
    next if value_str.include?("media=cdrom")

    volumes << parse_config_value(key_str, value_str, resource_type, resource_id, node)
  end
end

#fetch_storage_volumes(node_name, storage) ⇒ Array<Models::Volume>

Fetches volumes from a storage on a specific node.



207
208
209
210
211
212
213
# File 'lib/pvectl/repositories/volume.rb', line 207

def fetch_storage_volumes(node_name, storage)
  response = connection.client["nodes/#{node_name}/storage/#{storage}/content"].get
  data = unwrap(response)
  data.map { |item| build_storage_volume(item, node_name, storage) }
rescue StandardError
  []
end

#find(resource_type:, id:, disk_name:, node: nil) ⇒ Models::Volume?

Finds a specific volume by disk name in a VM/CT config.



91
92
93
94
# File 'lib/pvectl/repositories/volume.rb', line 91

def find(resource_type:, id:, disk_name:, node: nil)
  volumes = list_from_config(resource_type: resource_type, ids: [id], node: node)
  volumes.find { |v| v.name == disk_name }
end

#format_bytes_to_size(bytes) ⇒ String?

Formats bytes to human-readable size string.



270
271
272
273
274
275
276
277
278
279
280
# File 'lib/pvectl/repositories/volume.rb', line 270

def format_bytes_to_size(bytes)
  return nil unless bytes.is_a?(Integer) && bytes.positive?

  gb = bytes / (1024 * 1024 * 1024)
  return "#{gb}G" if gb.positive?

  mb = bytes / (1024 * 1024)
  return "#{mb}M" if mb.positive?

  "#{bytes}B"
end

#list_from_config(resource_type:, ids:, node: nil) ⇒ Array<Models::Volume>

Lists volumes from VM/CT configuration for given resource IDs.

Fetches config from each VM/CT and extracts disk entries. Excludes CD-ROM entries (containing media=cdrom).



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

def list_from_config(resource_type:, ids:, node: nil)
  type = normalize_resource_type(resource_type)
  repo = repo_for(type)
  return [] unless repo

  volumes = ids.flat_map do |id|
    resource = repo.get(id)
    next [] if resource.nil?
    next [] if node && resource.node != node

    config = repo.fetch_config(resource.node, id.to_i)
    extract_volumes(config, type, id.to_i, resource.node)
  end

  volumes
end

#list_from_storage(storage:, node: nil) ⇒ Array<Models::Volume>

Lists volumes from storage content API.

Queries /nodes/{node}/storage/{storage}/content to list all volumes in the given storage.



79
80
81
82
# File 'lib/pvectl/repositories/volume.rb', line 79

def list_from_storage(storage:, node: nil)
  nodes = node ? [node] : online_nodes
  nodes.flat_map { |node_name| fetch_storage_volumes(node_name, storage) }
end

#normalize_resource_type(type) ⇒ String

Normalizes resource type string to canonical form.



129
130
131
132
133
134
135
# File 'lib/pvectl/repositories/volume.rb', line 129

def normalize_resource_type(type)
  case type.to_s.downcase
  when "vm", "qemu" then "vm"
  when "ct", "container", "lxc" then "ct"
  else type.to_s.downcase
  end
end

#online_nodesArray<String>

Fetches list of online node names.



285
286
287
288
289
290
291
292
293
# File 'lib/pvectl/repositories/volume.rb', line 285

def online_nodes
  response = connection.client["nodes"].get
  nodes_data = unwrap(response)
  nodes_data
    .select { |n| n[:status] == "online" }
    .map { |n| n[:node] || n[:name] }
rescue StandardError
  []
end

#parse_config_value(name, value, resource_type, resource_id, node) ⇒ Models::Volume

Parses a config value string into a Volume model.

Config values have the format:

"storage:volume-id,key1=val1,key2=val2"


172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/pvectl/repositories/volume.rb', line 172

def parse_config_value(name, value, resource_type, resource_id, node)
  # Split "storage:volume-id,key=val,..." into storage_part and options
  parts = value.split(",")
  storage_spec = parts.shift || ""

  storage, volume_id = storage_spec.split(":", 2)

  # Parse key=value options
  attrs = { name: name, storage: storage, volume_id: volume_id,
            resource_type: resource_type, resource_id: resource_id, node: node }

  parts.each do |part|
    k, v = part.split("=", 2)
    next unless k && v

    case k
    when "size"     then attrs[:size] = v
    when "format"   then attrs[:format] = v
    when "cache"    then attrs[:cache] = v
    when "discard"  then attrs[:discard] = v
    when "ssd"      then attrs[:ssd] = parse_int(v)
    when "iothread" then attrs[:iothread] = parse_int(v)
    when "backup"   then attrs[:backup] = parse_int(v)
    when "mp"       then attrs[:mp] = v
    end
  end

  Models::Volume.new(attrs)
end

#parse_int(value) ⇒ Integer?

Parses a string value to integer.



299
300
301
302
303
# File 'lib/pvectl/repositories/volume.rb', line 299

def parse_int(value)
  return nil unless value

  value.to_i
end

#repo_for(type) ⇒ Repositories::Vm, ...

Returns the appropriate repository for the given resource type.



102
103
104
105
106
107
# File 'lib/pvectl/repositories/volume.rb', line 102

def repo_for(type)
  case type
  when "vm" then vm_repo
  when "ct" then container_repo
  end
end

#vm_repoRepositories::Vm

Returns VM repository instance. Uses injected repository if provided, otherwise creates new one.



113
114
115
# File 'lib/pvectl/repositories/volume.rb', line 113

def vm_repo
  @vm_repo ||= Repositories::Vm.new(connection)
end