Class: Pvectl::Services::EditVolume

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/services/edit_volume.rb,
sig/pvectl/services/edit_volume.rbs

Overview

Orchestrates the interactive volume property editing flow.

Fetches the disk config string from VM/CT config, parses it into editable YAML (key-value pairs), opens editor, and applies changes. Size changes delegate to ResizeVolume; config changes rebuild the disk config string.

Examples:

Basic usage

service = EditVolume.new(repository: vm_repo, resource_type: :vm)
result = service.execute(id: 100, disk: "scsi0", node: "pve1")

Dry run with injected editor session

service = EditVolume.new(repository: vm_repo, resource_type: :vm,
                         editor_session: session, options: { dry_run: true })
result = service.execute(id: 100, disk: "scsi0", node: "pve1")

Instance Method Summary collapse

Constructor Details

#initialize(repository:, resource_type:, editor_session: nil, options: {}) ⇒ EditVolume

Returns a new instance of EditVolume.

Parameters:

  • repository (Repositories::Vm, Repositories::Container)

    resource repository

  • resource_type (Symbol)

    :vm or :container

  • editor_session (EditorSession, nil) (defaults to: nil)

    optional injected editor session

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

    options (dry_run)

  • repository: (Object)
  • resource_type: (Symbol)
  • editor_session: (Object, nil) (defaults to: nil)
  • options: (Hash[Symbol, untyped]) (defaults to: {})


26
27
28
29
30
31
# File 'lib/pvectl/services/edit_volume.rb', line 26

def initialize(repository:, resource_type:, editor_session: nil, options: {})
  @repository = repository
  @resource_type = resource_type
  @editor_session = editor_session
  @options = options
end

Instance Method Details

#apply_changes(id, disk, node, original_disk_value, changes) ⇒ void

This method returns an undefined value.

Applies changes — delegates size to ResizeVolume, config to rebuild.

Parameters:

  • id (Integer)

    resource ID

  • disk (String)

    disk name

  • node (String)

    node name

  • original_disk_value (String)

    original disk config string

  • changes (Hash)

    diff hash



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/pvectl/services/edit_volume.rb', line 160

def apply_changes(id, disk, node, original_disk_value, changes)
  size_change = changes[:changed].delete("size") || changes[:added].delete("size")

  if size_change
    new_size = size_change.is_a?(Array) ? size_change[1] : size_change
    parsed_size = ResizeVolume.parse_size(new_size.to_s)
    resize_service = ResizeVolume.new(repository: @repository)
    resize_service.preflight(id, disk, parsed_size, node: node)
    resize_service.perform(id, disk, parsed_size.raw, node: node)
  end

  config_updates = {}
  changes[:changed].each { |key, (_old, new_val)| config_updates[key] = new_val }
  changes[:added].each { |key, val| config_updates[key] = val }

  if !config_updates.empty? || !changes[:removed].empty?
    new_disk_value = rebuild_disk_config(original_disk_value, config_updates, changes[:removed])
    @repository.update(id, node, { disk.to_sym => new_disk_value })
  end
end

#build_result(id, disk, node, **attrs) ⇒ Models::VolumeOperationResult

Builds a VolumeOperationResult.

Parameters:

  • id (Integer)

    resource ID

  • disk (String)

    disk name

  • node (String)

    node name

  • attrs (Hash)

    additional result attributes

Returns:



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/pvectl/services/edit_volume.rb', line 211

def build_result(id, disk, node, **attrs)
  volume = Models::Volume.new(
    name: disk,
    resource_type: @resource_type.to_s,
    resource_id: id,
    node: node
  )
  Models::VolumeOperationResult.new(
    operation: :edit, volume: volume, **attrs
  )
end

#build_yaml_content(editable, disk, id, node) ⇒ String

Builds YAML content for the editor with comment header.

Parameters:

  • editable (Hash)

    editable properties

  • disk (String)

    disk name

  • id (Integer)

    resource ID

  • node (String)

    node name

Returns:

  • (String)

    YAML content with comments



104
105
106
107
108
# File 'lib/pvectl/services/edit_volume.rb', line 104

def build_yaml_content(editable, disk, id, node)
  "# Volume: #{disk} (resource #{id} on #{node})\n" \
    "# Edit properties below. Save and close to apply.\n" +
    editable.to_yaml
end

#compute_diff(original, edited) ⇒ Hash

Computes diff between original and edited config.

Parameters:

  • original (Hash)

    original config (string keys)

  • edited (Hash)

    edited config (string keys)

Returns:

  • (Hash)

    diff with :changed, :added, :removed



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/pvectl/services/edit_volume.rb', line 132

def compute_diff(original, edited)
  changed = {}
  added = {}
  removed = []

  edited.each do |key, value|
    orig_value = original[key.to_s]
    if orig_value.nil?
      added[key.to_s] = value
    elsif orig_value.to_s != value.to_s
      changed[key.to_s] = [orig_value.to_s, value.to_s]
    end
  end

  original.each_key do |key|
    removed << key.to_s unless edited.key?(key.to_s)
  end

  { changed: changed, added: added, removed: removed }
end

#execute(id:, disk:, node:) ⇒ Models::VolumeOperationResult?

Executes the interactive volume edit flow.

Parameters:

  • id (Integer)

    resource ID (VMID or CTID)

  • disk (String)

    disk name (e.g., "scsi0", "rootfs")

  • node (String)

    node name

  • id: (Integer)
  • disk: (String)
  • node: (String)

Returns:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/pvectl/services/edit_volume.rb', line 39

def execute(id:, disk:, node:)
  config = @repository.fetch_config(node, id)
  disk_value = config[disk.to_sym]

  unless disk_value
    return build_result(id, disk, node, success: false,
                        error: "Volume '#{disk}' not found in config for resource #{id}")
  end

  editable = parse_disk_config(disk_value)
  yaml_content = build_yaml_content(editable, disk, id, node)

  session = @editor_session || EditorSession.new
  edited = session.edit(yaml_content)

  return nil unless edited

  edited_config = parse_edited_yaml(edited)
  changes = compute_diff(editable, edited_config)

  return nil if no_changes?(changes)

  if @options[:dry_run]
    return build_result(id, disk, node, success: true,
                        resource: { diff: changes })
  end

  apply_changes(id, disk, node, disk_value, changes)
  build_result(id, disk, node, success: true)
rescue ResizeVolume::VolumeNotFoundError, ResizeVolume::SizeTooSmallError, ArgumentError => e
  build_result(id, disk, node, success: false, error: e.message)
rescue StandardError => e
  build_result(id, disk, node, success: false, error: e.message)
end

#no_changes?(changes) ⇒ Boolean

Checks whether the diff contains any actual changes.

Parameters:

  • changes (Hash)

    diff hash with :changed, :added, :removed

Returns:

  • (Boolean)

    true if no changes detected



123
124
125
# File 'lib/pvectl/services/edit_volume.rb', line 123

def no_changes?(changes)
  changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
end

#parse_disk_config(disk_value) ⇒ Hash

Parses a disk config string into a hash of editable properties.

Input: "local-lvm:vm-100-disk-0,size=32G,cache=none" Output: { "size" => "32G", "cache" => "none" }

The base (storage:vol-id) is NOT included — it's read-only.

Parameters:

  • disk_value (String)

    disk config string

Returns:

  • (Hash)

    editable properties with string keys



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/pvectl/services/edit_volume.rb', line 85

def parse_disk_config(disk_value)
  parts = disk_value.to_s.split(",")
  parts.shift # Remove base "storage:vol-id"

  props = {}
  parts.each do |part|
    key, value = part.split("=", 2)
    props[key] = value
  end
  props
end

#parse_edited_yaml(edited) ⇒ Hash

Parses edited YAML content, stripping comment lines.

Parameters:

  • edited (String)

    raw editor content

Returns:

  • (Hash)

    parsed config with string keys



114
115
116
117
# File 'lib/pvectl/services/edit_volume.rb', line 114

def parse_edited_yaml(edited)
  cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
  YAML.safe_load(cleaned) || {}
end

#rebuild_disk_config(current_value, updates, removed_keys = []) ⇒ String

Rebuilds a disk config string with updated/removed properties.

Parameters:

  • current_value (String)

    current disk config string

  • updates (Hash)

    key-value pairs to update

  • removed_keys (Array<String>) (defaults to: [])

    keys to remove

Returns:

  • (String)

    updated config string



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/pvectl/services/edit_volume.rb', line 187

def rebuild_disk_config(current_value, updates, removed_keys = [])
  parts = current_value.to_s.split(",")
  base = parts.shift

  existing = {}
  parts.each do |part|
    key, value = part.split("=", 2)
    existing[key] = value
  end

  updates.each { |key, value| existing[key.to_s] = value.to_s }
  removed_keys.each { |key| existing.delete(key.to_s) }

  config_parts = existing.map { |k, v| "#{k}=#{v}" }
  ([base] + config_parts).join(",")
end