Class: Pvectl::Services::SetNode

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

Overview

Orchestrates non-interactive node configuration updates.

Takes key-value pairs directly (no editor), computes diff against current config, and applies changes via the API. Supports dry-run mode and optimistic locking via digest.

The timezone key is special: it lives at PUT /nodes/{node}/time rather than PUT /nodes/{node}/config, so the service splits it off and routes it to a separate time_repository.

Examples:

Basic usage

service = SetNode.new(node_repository: repo)
result = service.execute(node_name: "pve1", params: { description: "updated" })

Setting timezone

service = SetNode.new(node_repository: node_repo, time_repository: time_repo)
result = service.execute(node_name: "pve1", params: { timezone: "Europe/Warsaw" })

Dry run

service = SetNode.new(node_repository: repo, options: { dry_run: true })
result = service.execute(node_name: "pve1", params: { description: "updated" })

Constant Summary collapse

TIMEZONE_KEY =

Key recognized by this service as the node timezone (handled by the /time endpoint rather than /config). Accepted as Symbol or String.

:timezone

Instance Method Summary collapse

Constructor Details

#initialize(node_repository:, time_repository: nil, options: {}) ⇒ SetNode

Creates a new SetNode service.



38
39
40
41
42
# File 'lib/pvectl/services/set_node.rb', line 38

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

Instance Method Details

#apply_config_changes(node_name, changes, original_config) ⇒ void

This method returns an undefined value.

Applies non-timezone diff entries via the node config endpoint.



173
174
175
176
177
178
# File 'lib/pvectl/services/set_node.rb', line 173

def apply_config_changes(node_name, changes, original_config)
  update_params = build_update_params(changes, original_config)
  return if update_params.empty? || update_params.keys == [:digest]

  @node_repository.update(node_name, update_params)
end

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

Builds a NodeOperationResult with the :set operation.



220
221
222
223
224
225
# File 'lib/pvectl/services/set_node.rb', line 220

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

#build_update_params(changes, original_config) ⇒ Hash

Builds API update parameters from diff (excluding timezone, which is routed elsewhere).



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/pvectl/services/set_node.rb', line 199

def build_update_params(changes, original_config)
  params = {}
  changes[:changed].each do |key, (_old, new_val)|
    next if key == TIMEZONE_KEY

    params[key] = new_val
  end
  changes[:added].each do |key, val|
    next if key == TIMEZONE_KEY

    params[key] = val
  end
  params[:digest] = original_config[:digest] if original_config[:digest] && !params.empty?
  params
end

#compute_diff(config, params) ⇒ Hash

Computes diff between current config and requested params.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/pvectl/services/set_node.rb', line 141

def compute_diff(config, params)
  changed = {}
  added = {}

  params.each do |key, value|
    sym_key = key.to_sym
    current = config[sym_key]

    if current.nil?
      added[sym_key] = value
    elsif current.to_s != value.to_s
      changed[sym_key] = [current.to_s, value.to_s]
    end
  end

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

#compute_timezone_change(current, requested) ⇒ Hash{from: String, to: String}?

Builds a timezone-change descriptor when requested != current.



115
116
117
118
119
120
# File 'lib/pvectl/services/set_node.rb', line 115

def compute_timezone_change(current, requested)
  return nil if requested.nil?
  return nil if current.to_s == requested.to_s

  { from: current.to_s, to: requested.to_s }
end

#config_changes_empty?(changes, tz_change) ⇒ Boolean

True when the only changes in the diff are timezone-related.



185
186
187
188
189
190
191
# File 'lib/pvectl/services/set_node.rb', line 185

def config_changes_empty?(changes, tz_change)
  return false if tz_change.nil?

  non_tz_changed = changes[:changed].keys - [TIMEZONE_KEY]
  non_tz_added = changes[:added].keys - [TIMEZONE_KEY]
  non_tz_changed.empty? && non_tz_added.empty?
end

#current_timezone(node_name) ⇒ String?

Fetches the current timezone for the node.



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

def current_timezone(node_name)
  return nil unless @time_repository

  @time_repository.fetch(node_name).timezone
end

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

Executes the non-interactive node config update.

Fetches current config and (if timezone is requested) current timezone, computes diffs against the requested params, and applies changes via the appropriate API endpoint (unless dry-run).



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/pvectl/services/set_node.rb', line 53

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

  config = @node_repository.fetch_config(node_name)
  config_params, requested_tz = split_timezone(params)
  current_tz = requested_tz ? current_timezone(node_name) : nil

  changes = compute_diff(config, config_params)
  tz_change = compute_timezone_change(current_tz, requested_tz)
  merge_timezone_into_diff(changes, tz_change) if tz_change

  return nil if no_changes?(changes)

  resource_info = { node_name: node_name, status: node.status, diff: changes }
  return build_result(resource_info, success: true) if @options[:dry_run]

  apply_config_changes(node_name, changes, config) unless config_changes_empty?(changes, tz_change)
  @time_repository.set_timezone(node_name, tz_change[:to]) if tz_change

  build_result(resource_info, success: true)
rescue StandardError => e
  build_result({ node_name: node_name }, success: false, error: e.message)
end

#merge_timezone_into_diff(changes, tz_change) ⇒ void

This method returns an undefined value.

Folds a timezone change into the diff structure so it shows up in the operation result alongside config-key changes.



128
129
130
131
132
133
134
# File 'lib/pvectl/services/set_node.rb', line 128

def merge_timezone_into_diff(changes, tz_change)
  if tz_change[:from].nil? || tz_change[:from].empty?
    changes[:added][TIMEZONE_KEY] = tz_change[:to]
  else
    changes[:changed][TIMEZONE_KEY] = [tz_change[:from], tz_change[:to]]
  end
end

#no_changes?(changes) ⇒ Boolean

Returns true when the diff contains no actionable change.



163
164
165
# File 'lib/pvectl/services/set_node.rb', line 163

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

#not_found_result(node_name) ⇒ Models::NodeOperationResult

Builds a not-found error result.



231
232
233
# File 'lib/pvectl/services/set_node.rb', line 231

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

#split_timezone(params) ⇒ Array(Hash, String?)

Separates the :timezone key (accepted as Symbol or String) from other config params.



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

def split_timezone(params)
  remaining = {}
  requested_tz = nil

  params.each do |key, value|
    if key.to_sym == TIMEZONE_KEY
      requested_tz = value.to_s
    else
      remaining[key] = value
    end
  end

  [remaining, requested_tz]
end