Class: Pvectl::Services::EditNode

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

Overview

Orchestrates the interactive node configuration editing flow.

Fetches current config, presents it as YAML in an editor, computes diff, and applies changes. Uses plain YAML (not ConfigSerializer) since node config is flat key-value.

Examples:

Basic usage

service = EditNode.new(node_repository: repo)
result = service.execute(node_name: "pve1")

Dry run with injected editor session

service = EditNode.new(node_repository: repo, editor_session: session,
                       options: { dry_run: true })
result = service.execute(node_name: "pve1")

Constant Summary collapse

READONLY_KEYS =

Read-only keys that should not be sent back to the API.

Returns:

i[digest].freeze

Instance Method Summary collapse

Constructor Details

#initialize(node_repository:, editor_session: nil, options: {}) ⇒ EditNode

Creates a new EditNode service.

Parameters:

  • Node repository

  • (defaults to: nil)

    optional injected editor session

  • (defaults to: {})

    options (dry_run)

  • (defaults to: nil)
  • (defaults to: {})


29
30
31
32
33
# File 'lib/pvectl/services/edit_node.rb', line 29

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

Instance Method Details

#build_result(resource_info, **attrs) ⇒ Models::NodeOperationResult

Builds a NodeOperationResult with the :edit operation.

Parameters:

  • resource info (node_name, status)

  • additional result attributes

Returns:



131
132
133
134
135
136
# File 'lib/pvectl/services/edit_node.rb', line 131

def build_result(resource_info, **attrs)
  node_model = Models::Node.new(name: resource_info[:node_name])
  Models::NodeOperationResult.new(
    operation: :edit, node_model: node_model, resource: resource_info, **attrs
  )
end

#build_update_params(changes, original_config) ⇒ Hash

Builds API update parameters from diff.

Parameters:

  • diff hash with :changed, :added, :removed

  • original config (for digest)

Returns:

  • API parameters



115
116
117
118
119
120
121
122
123
124
# File 'lib/pvectl/services/edit_node.rb', line 115

def build_update_params(changes, original_config)
  params = {}
  changes[:changed].each { |key, (_old, new_val)| params[key] = new_val }
  changes[:added].each { |key, val| params[key] = val }
  unless changes[:removed].empty?
    params[:delete] = changes[:removed].map(&:to_s).join(",")
  end
  params[:digest] = original_config[:digest] if original_config[:digest]
  params
end

#compute_diff(original, edited) ⇒ Hash

Computes diff between original and edited config.

Parameters:

  • original config (without readonly keys)

  • edited config

Returns:

  • diff with :changed, :added, :removed



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/pvectl/services/edit_node.rb', line 89

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

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

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

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

#execute(node_name:) ⇒ Models::NodeOperationResult?

Executes the interactive node edit flow.

Parameters:

  • node name

Returns:

  • result, or nil if cancelled/no changes



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
73
74
75
76
77
78
79
80
# File 'lib/pvectl/services/edit_node.rb', line 39

def execute(node_name:)
  node = @node_repository.get(node_name)
  return not_found_result(node_name) unless node

  config = @node_repository.fetch_config(node_name)
  resource_info = { node_name: node_name, status: node.status }

  # Build editable YAML (exclude digest — it's for optimistic locking only)
  editable = config.reject { |k, _| READONLY_KEYS.include?(k) }
  # Convert symbol keys to strings for clean YAML output (avoids :key: format)
  string_keyed = editable.transform_keys(&:to_s)
  yaml_content = "# Node: #{node_name}\n# Edit configuration below. Save and close to apply.\n" +
                  string_keyed.to_yaml

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

  return nil unless edited

  # Parse edited YAML (strip comment lines)
  cleaned = edited.lines.reject { |l| l.strip.start_with?("#") }.join
  edited_config = YAML.safe_load(cleaned, symbolize_names: true) || {}

  # Compute diff against original editable config (with symbol keys)
  changes = compute_diff(editable, edited_config)

  if changes[:changed].empty? && changes[:added].empty? && changes[:removed].empty?
    return nil
  end

  resource_info[:diff] = changes

  if @options[:dry_run]
    return build_result(resource_info, success: true)
  end

  update_params = build_update_params(changes, config)
  @node_repository.update(node_name, update_params)
  build_result(resource_info, success: true)
rescue StandardError => e
  build_result({ node_name: node_name }, success: false, error: e.message)
end

#not_found_result(node_name) ⇒ Models::NodeOperationResult

Builds a not-found error result.

Parameters:

  • node name

Returns:



142
143
144
# File 'lib/pvectl/services/edit_node.rb', line 142

def not_found_result(node_name)
  build_result({ node_name: node_name }, success: false, error: "Node #{node_name} not found")
end