foreman_opentofu 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/foreman_opentofu/compute_resources_controller.rb +22 -0
  3. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +14 -10
  4. data/app/models/concerns/foreman_opentofu/compute_attribute_validation.rb +25 -0
  5. data/app/models/concerns/foreman_opentofu/power_commands.rb +57 -0
  6. data/app/models/concerns/foreman_opentofu/vm_command_collection_normalization.rb +2 -2
  7. data/app/models/concerns/orchestration/tofu/compute.rb +2 -0
  8. data/app/models/foreman_opentofu/compute_vm.rb +22 -0
  9. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +20 -21
  10. data/app/models/foreman_opentofu/tofu.rb +14 -6
  11. data/app/services/foreman_opentofu/app_wrapper.rb +72 -7
  12. data/app/services/foreman_opentofu/opentofu_executer.rb +50 -20
  13. data/app/services/foreman_opentofu/provider_type.rb +95 -5
  14. data/app/views/compute_resources/form/_tofu.html.erb +21 -4
  15. data/app/views/compute_resources/show/_tofu.html.erb +6 -4
  16. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +11 -2
  17. data/app/views/templates/provisioning/hetzner_provision_host.erb +2 -2
  18. data/app/views/templates/provisioning/stackit_provision_default.erb +102 -0
  19. data/lib/foreman_opentofu/engine.rb +2 -0
  20. data/lib/foreman_opentofu/provider_types/hetzner.rb +19 -1
  21. data/lib/foreman_opentofu/provider_types/nutanix.rb +11 -4
  22. data/lib/foreman_opentofu/provider_types/stackit.rb +245 -0
  23. data/lib/foreman_opentofu/version.rb +1 -1
  24. data/test/controllers/compute_resources_controller_test.rb +40 -0
  25. data/test/factories/compute_resources.rb +8 -8
  26. data/test/fixtures/foreman_opentofu/app_wrapper/apply_error.jsonl +9 -0
  27. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.empty.snap.txt +2 -2
  28. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.hetzner_host.snap.txt +2 -2
  29. data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +17 -0
  30. data/test/lib/foreman_opentofu/provider_types/hetzner_test.rb +45 -0
  31. data/test/lib/foreman_opentofu/provider_types/nutanix_test.rb +40 -0
  32. data/test/lib/foreman_opentofu/provider_types/stackit_test.rb +52 -0
  33. data/test/models/foreman_opentofu/compute_attribute_validation_test.rb +79 -0
  34. data/test/models/foreman_opentofu/compute_vm_test.rb +38 -0
  35. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +186 -18
  36. data/test/models/foreman_opentofu/tofu_test.rb +25 -2
  37. data/test/renderer/stackit_template_test.rb +210 -0
  38. data/test/services/app_wrapper_test.rb +145 -0
  39. data/test/services/foreman_opentofu/provider_type_test.rb +129 -0
  40. data/test/services/opentofu_executer_test.rb +81 -4
  41. metadata +21 -4
  42. data/app/views/templates/provisioning/ovirt_provision_default.erb +0 -43
  43. data/lib/foreman_opentofu/provider_types/ovirt.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aac98f7ab29438afb053493e3ef572e5374d5140eec49cc67e4f4d9787776a08
4
- data.tar.gz: 85f7eb01a3b220b06b357c5ef9b0270bbf49fc19ea286f7e421964871f7bf726
3
+ metadata.gz: 8eceff0c5cce2e6fde716720f6262051d3ca5cd6355ca21db998d631f559eb78
4
+ data.tar.gz: 7e4ba716aca8ce9e6fede8abb74cfe33aae9838b02646a4804d008c9203f706a
5
5
  SHA512:
6
- metadata.gz: 3a8231fb351a2f809d768880ed32dd0be78b13333453f4d5fbb71fbcc4c801dc963c0b08beeed69ba916c307c669856d17e8513d1cdb4923f25e0c6b49487e73
7
- data.tar.gz: c221f8852cfedf5d07bb40bb93832c2512b77b59b58c043dd8f23cfdf61ee102321be84825158e61981bcd1255b4bc2b6c18fa589d368c5c0d2df16dc2697994
6
+ metadata.gz: 2db6f7efade936b283136c2b47dea0bd8cded850c94cdae3ef085f0a7f38c388f849f3ea7cb0fc398c4c3171c47cf0737d04b000c531373b8757cdd8607a3dc7
7
+ data.tar.gz: c0a8e35c4b2d39281bfbad14109be06b37f066b71738defbaff167bb235dee73978fa39811d5c07a25d258f23527dffae02e8ad4d9af186b3e8065b3ddee5edf
@@ -0,0 +1,22 @@
1
+ module ForemanOpentofu
2
+ module ComputeResourcesController
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ prepend Overrides
7
+ end
8
+
9
+ module Overrides
10
+ def provider_selected
11
+ return super unless params[:opentofu_provider_selected]
12
+
13
+ @compute_resource = ComputeResource.new_provider(provider: 'Tofu')
14
+ @compute_resource.opentofu_provider = params[:provider]
15
+ default_template = @compute_resource.opentofu_template
16
+ @compute_resource.opentofu_template_id = default_template.id if default_template
17
+
18
+ render partial: 'form'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -64,22 +64,24 @@ module ForemanOpentofu
64
64
  end
65
65
  def vm_attributes(skip_list = [])
66
66
  available_attributes = @compute_resource.available_attributes
67
- data = {}
68
- res = ''
69
- @cr_attrs.each do |key, value|
70
- next if skip_list.include? key
67
+ skipped_attributes = skip_list.map(&:to_s)
68
+ data = @cr_attrs.each_with_object({}) do |(key, value), attributes|
69
+ next if skipped_attributes.include?(key.to_s)
71
70
 
72
- conf = available_attributes[key]
73
- if conf.nil?
71
+ config = available_attributes[key]
72
+ unless config
74
73
  Rails.logger.warn("Attribute #{key.inspect} is not supported.")
75
74
  next
76
75
  end
77
- next if conf['group'] != 'vm'
78
- next if value.blank? && !conf['mandatory']
76
+ next unless vm_attribute_included?(config, value)
79
77
 
80
- data[key] = format_value(value, conf['type'])
78
+ attributes[key] = format_value(value, config['type'])
81
79
  end
82
- res << to_hcl(data, snippet: true)
80
+ to_hcl(data, snippet: true)
81
+ end
82
+
83
+ def vm_attribute_included?(config, value)
84
+ config['group'] == 'vm' && (value.present? || config['mandatory'])
83
85
  end
84
86
 
85
87
  def backend_block
@@ -108,6 +110,8 @@ module ForemanOpentofu
108
110
  end
109
111
 
110
112
  def build_nics
113
+ return render_provider_data(@compute_resource.render_nic(nil, self, 0)) if @compute_resource.tofu_provider.nic_renderer_collection?
114
+
111
115
  nics_from_cr_attrs.each_with_index.map do |nic, index|
112
116
  data = @compute_resource.render_nic(nic, self, index)
113
117
  render_provider_data(data)
@@ -0,0 +1,25 @@
1
+ module ForemanOpentofu
2
+ module ComputeAttributeValidation
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ validate :validate_opentofu_vm_attributes
7
+ end
8
+
9
+ private
10
+
11
+ def validate_opentofu_vm_attributes
12
+ return unless compute_resource.is_a?(ForemanOpentofu::Tofu)
13
+
14
+ args = compute_resource.default_attributes.merge(vm_attrs.deep_dup).to_h.symbolize_keys
15
+ compute_resource.normalize_vm_args_collections!(args)
16
+ provider = compute_resource.tofu_provider
17
+
18
+ begin
19
+ provider.validate_vm!(args, compute_resource)
20
+ rescue ArgumentError => e
21
+ errors.add(:base, e.message)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,57 @@
1
+ module ForemanOpentofu
2
+ module PowerCommands
3
+ def start_vm(name)
4
+ return change_vm_power(name, 'on') if tofu_provider.respond_to?(:power_state_attributes)
5
+
6
+ output = client({ 'name' => name, 'power_state' => 'on' }).run_create
7
+ output['vm']['power_state'] == 'on'
8
+ end
9
+
10
+ def stop_vm(name)
11
+ return change_vm_power(name, 'off') if tofu_provider.respond_to?(:power_state_attributes)
12
+
13
+ output = client({ 'name' => name, 'power_state' => 'off' }).run_create
14
+ output['vm']['power_state'] == 'off'
15
+ end
16
+
17
+ private
18
+
19
+ def change_vm_power(identifier, state)
20
+ vm_command_errors('change power') do
21
+ tf_state = power_state_record(identifier)
22
+ output = power_state_output(tf_state)
23
+ result = client(power_state_arguments(output, tf_state.name, state)).run_create(power_only: true)
24
+ verify_power_result(result, state)
25
+ end
26
+ end
27
+
28
+ def power_state_record(identifier)
29
+ tf_state = TfState.find_by(uuid: identifier) || TfState.find_by(name: identifier)
30
+ raise "No OpenTofu state found for #{identifier}" unless tf_state
31
+
32
+ tf_state
33
+ end
34
+
35
+ def power_state_output(tf_state)
36
+ output = client('name' => tf_state.name).run_output
37
+ valid = output['vm'].is_a?(Hash) && output['vm'].present?
38
+ raise 'Power control requires VM output from the provisioning template.' unless valid
39
+
40
+ output
41
+ end
42
+
43
+ def power_state_arguments(output, name, state)
44
+ arguments = ComputeVM.new(self, output).to_h
45
+ arguments.merge!(tofu_provider.power_state_attributes(state))
46
+ arguments['name'] = name
47
+ arguments
48
+ end
49
+
50
+ def verify_power_result(result, state)
51
+ vm = ComputeVM.new(self, result)
52
+ raise "OpenTofu did not return the requested power state #{state}" unless vm.power == state
53
+
54
+ vm
55
+ end
56
+ end
57
+ end
@@ -1,7 +1,5 @@
1
1
  module ForemanOpentofu
2
2
  module VMCommandCollectionNormalization
3
- private
4
-
5
3
  def normalize_vm_args_collections!(args)
6
4
  args[:image_id] = args[:image] if args.key?(:image)
7
5
  normalize_vm_boolean_args!(args)
@@ -13,6 +11,8 @@ module ForemanOpentofu
13
11
  end
14
12
  end
15
13
 
14
+ private
15
+
16
16
  def normalize_vm_boolean_args!(args)
17
17
  vm_boolean_keys.each do |key|
18
18
  next unless args.key?(key)
@@ -6,6 +6,8 @@ module Orchestration
6
6
  def match_macs_to_nics(fog_attr)
7
7
  return super unless compute_resource.is_a?(ForemanOpentofu::Tofu)
8
8
 
9
+ return super if compute_resource.tofu_provider.respond_to?(:select_nic_for_mac)
10
+
9
11
  interfaces.select(&:physical?).each do |nic|
10
12
  mac = vm.send(fog_attr)
11
13
  logger.debug "Orchestration::Compute: nic #{nic.inspect} assigned to #{vm.inspect}"
@@ -21,6 +21,17 @@ module ForemanOpentofu
21
21
  self['status'] || self['power'] || self['power_state']
22
22
  end
23
23
 
24
+ def state
25
+ power
26
+ end
27
+
28
+ def reload
29
+ refreshed = @provider.find_vm_by_uuid(self['identity'])
30
+ @attributes = refreshed.to_h.deep_stringify_keys
31
+ define_dynamic_readers!
32
+ self
33
+ end
34
+
24
35
  # TODO: add definitions for different power on/off values
25
36
  def ready?
26
37
  power.to_s == 'on' || power.to_s == 'running'
@@ -76,6 +87,17 @@ module ForemanOpentofu
76
87
  instance_eval(&block)
77
88
  end
78
89
 
90
+ # Foreman's MAC orchestration expects NIC objects with attribute readers.
91
+ def interfaces
92
+ value = attribute_value('interfaces_attributes') || attribute_value('interfaces') || []
93
+ value = value.sort_by { |key, _| key.to_i }.map(&:last) if value.is_a?(Hash)
94
+ Array(value).map { |attrs| OpenStruct.new(attrs) }
95
+ end
96
+
97
+ def select_nic(nics, nic)
98
+ @provider.tofu_provider.select_nic_for_mac(nics, nic)
99
+ end
100
+
79
101
  def volumes_attributes
80
102
  vols_attrs = attribute_value('volumes_attributes')
81
103
  return vols_attrs if vols_attrs.is_a?(Hash)
@@ -1,11 +1,14 @@
1
1
  module ForemanOpentofu
2
2
  module OpentofuVMCommands
3
3
  include ForemanOpentofu::VMCommandCollectionNormalization
4
+ include ForemanOpentofu::PowerCommands
4
5
 
5
6
  def find_vm_by_uuid(uuid)
6
7
  vm_command_errors('find vm') do
7
8
  tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
8
9
  data = client({ 'name' => tf_state&.name }).run_output
10
+ # trying to add a volume with invalid parameter may persist in the volumes_attributes-hash
11
+ data['volumes_attributes'] = data['volumes_attributes']&.delete_if { |_idx, vol| vol.key?('id') && vol['id'].nil? }
9
12
  ComputeVM.new(self, data)
10
13
  end
11
14
  end
@@ -15,6 +18,8 @@ module ForemanOpentofu
15
18
  args = default_attributes.merge(args).to_h.symbolize_keys
16
19
  normalize_vm_args_collections!(args)
17
20
  args = prefill_mandatory_attributes(args).merge(args)
21
+ return ComputeVM.new(self, args.deep_stringify_keys) unless tofu_provider.plan_on_new_vm?
22
+
18
23
  executor = client(args)
19
24
  data = executor.run_new
20
25
  attrs = data['resource_changes'].first['change']['after'] || {}
@@ -24,11 +29,13 @@ module ForemanOpentofu
24
29
  end
25
30
 
26
31
  def create_vm(args = {})
32
+ args = default_attributes.merge(args).to_h.symbolize_keys
33
+ normalize_vm_args_collections!(args)
34
+ tofu_provider.validate_vm!(args, self)
35
+
27
36
  vm_command_errors('create vm') do
28
- args = default_attributes.merge(args).to_h.symbolize_keys
29
- normalize_vm_args_collections!(args)
30
37
  executor = client(args)
31
- output = executor.run_create
38
+ output = executor.run_create(cleanup_on_failure: true)
32
39
  ComputeVM.new(self, output)
33
40
  end
34
41
  end
@@ -42,24 +49,16 @@ module ForemanOpentofu
42
49
  tf_state.destroy
43
50
  end
44
51
 
45
- def start_vm(name)
46
- output = client({ 'name' => name, 'power_state' => 'on' }).run_create
47
- output['vm']['power_state'] == 'on'
48
- end
49
-
50
- def stop_vm(name)
51
- output = client({ 'name' => name, 'power_state' => 'off' }).run_create
52
- output['vm']['power_state'] == 'off'
53
- end
54
-
55
52
  def save_vm(uuid, attrs)
56
53
  old_attrs = vm_compute_attributes_for(uuid).to_h.deep_stringify_keys
57
54
  tf_state = TfState.find_by(uuid: uuid)
58
55
  raise StandardError, "VM with UUID #{uuid} does not exist" unless tf_state
56
+ new_attrs = attrs.to_h.deep_stringify_keys
57
+ merged_attrs = old_attrs.merge(new_attrs).deep_symbolize_keys
58
+ normalize_vm_args_collections!(merged_attrs)
59
+ tofu_provider.validate_vm!(merged_attrs, self)
60
+
59
61
  vm_command_errors('update vm') do
60
- new_attrs = attrs.to_h.deep_stringify_keys
61
- merged_attrs = old_attrs.merge(new_attrs).deep_symbolize_keys
62
- normalize_vm_args_collections!(merged_attrs)
63
62
  data = client({ 'name' => tf_state.name }.merge(merged_attrs)).run_create(raise_if_recreate: true)
64
63
  ComputeVM.new(self, data)
65
64
  end
@@ -85,13 +84,13 @@ module ForemanOpentofu
85
84
  yield
86
85
  rescue StandardError => e
87
86
  Foreman::Logging.exception("Caught #{provider} error", e)
88
- raise ::Foreman::WrappedException.new(
89
- e,
87
+ raise ::Foreman::Exception.new(
90
88
  N_(
91
- "Foreman could not find a required %<provider>s resource in #{method_name}. " \
92
- 'Check if Foreman has the required permissions and the resource exists. Reason: %<error>s'
89
+ '%<provider>s failed to %<method>s. Reason: %<error>s'
93
90
  ),
94
- { provider: provider, error: e.message }
91
+ provider: provider,
92
+ method: method_name,
93
+ error: e.message
95
94
  )
96
95
  end
97
96
 
@@ -21,9 +21,9 @@ module ForemanOpentofu
21
21
  include ComputeResourceCaching
22
22
  include KeyPairComputeResource
23
23
  validates :provider, presence: true, inclusion: { in: %w[Tofu] }
24
- validates :url, presence: true
25
- validates :user, presence: true
26
- validates :password, presence: true
24
+ validates :url, presence: true, if: -> { mandatory_compute_resource_attribute?('url') }
25
+ validates :user, presence: true, if: -> { mandatory_compute_resource_attribute?('user') }
26
+ validates :password, presence: true, if: -> { mandatory_compute_resource_attribute?('password') }
27
27
 
28
28
  # alias_attribute :username, :user
29
29
  # alias_attribute :endpoint, :url
@@ -67,9 +67,11 @@ module ForemanOpentofu
67
67
  end
68
68
 
69
69
  def opentofu_template
70
- return ProvisioningTemplate.find(attrs[:opentofu_template_id]) if attrs.key? :opentofu_template_id
71
- # or default-template for this opentofu_provider
72
- nil
70
+ return ProvisioningTemplate.find(attrs[:opentofu_template_id]) if attrs[:opentofu_template_id].present?
71
+
72
+ ProvisioningTemplate.unscoped.find_by!(
73
+ name: tofu_provider.default_template
74
+ )
73
75
  end
74
76
 
75
77
  def opentofu_template_id
@@ -148,6 +150,12 @@ module ForemanOpentofu
148
150
 
149
151
  private
150
152
 
153
+ def mandatory_compute_resource_attribute?(name)
154
+ tofu_provider&.connection_attrs&.any? do |attribute|
155
+ attribute['name'] == name && attribute['mandatory']
156
+ end
157
+ end
158
+
151
159
  def set_vm_volumes_attributes(vm, vm_attrs)
152
160
  volumes = if vm.respond_to?(:volumes_attributes)
153
161
  vm.volumes_attributes.values
@@ -7,7 +7,6 @@ module ForemanOpentofu
7
7
  # - manage temp-work-dir; problem: no auto-remove after finished :-(
8
8
  # - handle ENVVars if applicable
9
9
  # - handle stderr and stdout separately
10
- # - use JSON-output for easier parsing
11
10
  # - do we need locking or has the object be atomic
12
11
 
13
12
  def initialize(workdir, opts = {})
@@ -42,6 +41,7 @@ module ForemanOpentofu
42
41
  def default_params
43
42
  [
44
43
  '-no-color',
44
+ '-json',
45
45
  ]
46
46
  end
47
47
 
@@ -76,12 +76,12 @@ module ForemanOpentofu
76
76
  end
77
77
 
78
78
  def output(params = [])
79
- JSON.parse(tofu_execute('output', ['-json'].concat(parse_params(params))))
79
+ tofu_execute('output', parse_params(params))
80
80
  end
81
81
 
82
82
  # TODO: find better name ;-)
83
83
  def show_plan(params = [])
84
- JSON.parse(tofu_execute('show', ['-json', planfile].concat(parse_params(params))))
84
+ tofu_execute('show', [planfile].concat(parse_params(params)))
85
85
  end
86
86
 
87
87
  def planned?
@@ -98,12 +98,71 @@ module ForemanOpentofu
98
98
 
99
99
  private
100
100
 
101
+ def filter_sensitive_output(output)
102
+ filter_json_output(output.to_s)
103
+ rescue JSON::ParserError
104
+ output.to_s.each_line.map do |line|
105
+ filter_json_output(line)
106
+ rescue JSON::ParserError
107
+ line
108
+ end.join
109
+ end
110
+
111
+ def filter_json_output(output)
112
+ json = JSON.parse(output)
113
+ return output unless json.is_a?(Hash)
114
+
115
+ filtered = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters).filter(json)
116
+ return output if filtered == json
117
+
118
+ filtered.to_json + (output.end_with?("\n") ? "\n" : '')
119
+ end
120
+
121
+ def readable_output(output)
122
+ output.to_s.each_line.map do |line|
123
+ event = JSON.parse(line)
124
+ next line.chomp unless event.is_a?(Hash)
125
+
126
+ message = event.fetch('@message', line.chomp)
127
+ detail = event.dig('diagnostic', 'detail') if event['type'] == 'diagnostic' && event['diagnostic'].is_a?(Hash)
128
+ [message, detail].compact.reject(&:empty?).join("\n")
129
+ rescue JSON::ParserError
130
+ line.chomp
131
+ end.join("\n")
132
+ end
133
+
134
+ def error_summary(output)
135
+ errors = output.to_s.each_line.filter_map do |line|
136
+ diagnostic_summary(JSON.parse(line))
137
+ rescue JSON::ParserError
138
+ # stderr and startup failures may still contain plain text.
139
+ next
140
+ end
141
+ errors.empty? ? output : errors.join("\n\n")
142
+ end
143
+
144
+ def diagnostic_summary(event)
145
+ return unless event.is_a?(Hash) && event['type'] == 'diagnostic'
146
+
147
+ diagnostic = event['diagnostic']
148
+ return unless diagnostic.is_a?(Hash) && diagnostic['severity'] == 'error'
149
+ return if diagnostic['summary'].to_s.empty?
150
+
151
+ message = "Error: #{diagnostic['summary']}"
152
+ detail = diagnostic['detail']
153
+
154
+ detail.to_s.empty? ? message : "#{message}\n#{detail}"
155
+ end
156
+
101
157
  def parse_params(params)
102
158
  params.is_a?(String) ? [params] : params
103
159
  end
104
160
 
105
161
  def tofu_execute(action, params = [], &block)
106
- execute [base_command, action].concat(default_params).concat(params), &block
162
+ output = execute [base_command, action].concat(default_params).concat(params), &block
163
+ return JSON.parse(output) if %w[output show].include?(action) && !block
164
+
165
+ output
107
166
  end
108
167
 
109
168
  def command(cmd)
@@ -141,13 +200,19 @@ module ForemanOpentofu
141
200
  end
142
201
  ret = $CHILD_STATUS
143
202
  Rails.logger.info "#{cmd} returned #{ret.inspect}"
144
- Rails.logger.debug output.to_s
203
+ filtered_output = filter_sensitive_output(output)
204
+ Rails.logger.debug filtered_output
205
+
206
+ readable = readable_output(filtered_output)
207
+
145
208
  unless ret.success?
146
- Rails.logger.error "Command failed with output: #{output}"
209
+ Rails.logger.error "Command failed with output: #{readable}"
147
210
  # TODO: do we need to use a specific exception-type here?
148
- raise "command failed with code #{ret.exitstatus}:\n#{output}"
211
+ raise error_summary(output)
149
212
  end
150
213
 
214
+ Rails.logger.info readable unless readable.empty?
215
+
151
216
  output
152
217
  end
153
218
  end
@@ -3,6 +3,11 @@ module ForemanOpentofu
3
3
  # 'destroy' works with TfState rather than the actual tf-file
4
4
  # also it might not receive all the necessary data to create a valid tf-file
5
5
  DRY_RUN_MODES = %w[destroy test].freeze
6
+ PROVIDER_VARIABLE_NAMES = {
7
+ 'url' => :endpoint,
8
+ 'user' => :username,
9
+ 'password' => :password,
10
+ }.freeze
6
11
 
7
12
  def initialize(compute_resource, args = {})
8
13
  @compute_resource = compute_resource
@@ -21,11 +26,7 @@ module ForemanOpentofu
21
26
  f.write(@cr_attrs['user_data'])
22
27
  end
23
28
  end
24
- tofu = AppWrapper.new(dir, variables: {
25
- username: @compute_resource.user,
26
- password: @compute_resource.password,
27
- endpoint: @compute_resource.url,
28
- })
29
+ tofu = AppWrapper.new(dir, variables: provider_variables)
29
30
  @use_backend = %w[create destroy output keygen].include?(mode)
30
31
  @token = create_token(@host_name) if @use_backend
31
32
  tofu.main_configuration = render_template(mode)
@@ -73,21 +74,11 @@ module ForemanOpentofu
73
74
  run('test', &:plan)
74
75
  end
75
76
 
76
- def run_create(raise_if_recreate: false)
77
+ def run_create(raise_if_recreate: false, cleanup_on_failure: false, power_only: false)
77
78
  run('create') do |tofu|
78
- if raise_if_recreate
79
- # check the plan in advance to verify we do not replace the VM
80
- tofu.plan
81
- raise 'OpenTofu planned to re-create a resource; action aborted (check logs for details)!' if plan_wants_recreate? tofu.show_plan
82
- end
83
- begin
84
- tofu.apply
85
- attrs = tofu.output('vm_attrs')
86
- ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.update(uuid: attrs['identity'])
87
- attrs
88
- rescue StandardError => e
89
- handle_failed_create(tofu, e)
90
- end
79
+ validate_power_plan(tofu) if power_only
80
+ validate_update_plan(tofu) if raise_if_recreate
81
+ apply_create(tofu, cleanup_on_failure)
91
82
  end
92
83
  end
93
84
 
@@ -122,6 +113,42 @@ module ForemanOpentofu
122
113
 
123
114
  private
124
115
 
116
+ def validate_power_plan(tofu)
117
+ tofu.plan
118
+ changes = tofu.show_plan.fetch('resource_changes', [])
119
+ return if changes.all? { |change| @compute_resource.tofu_provider.power_change_allowed?(change) }
120
+
121
+ raise 'OpenTofu planned changes other than server power; action aborted.'
122
+ end
123
+
124
+ def validate_update_plan(tofu)
125
+ tofu.plan
126
+ return unless plan_wants_recreate?(tofu.show_plan)
127
+
128
+ raise 'OpenTofu planned to re-create a resource; action aborted (check logs for details)!'
129
+ end
130
+
131
+ def apply_create(tofu, cleanup_on_failure)
132
+ tofu.apply
133
+ attrs = tofu.output('vm_attrs')
134
+ ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.update(uuid: attrs['identity'])
135
+ attrs
136
+ rescue StandardError => e
137
+ raise e unless cleanup_on_failure
138
+
139
+ handle_failed_create(tofu, e)
140
+ end
141
+
142
+ def provider_variables
143
+ @compute_resource.tofu_provider.connection_attrs.each_with_object({}) do |attribute, variables|
144
+ attribute_name = attribute['name'].to_s
145
+ variable_name = PROVIDER_VARIABLE_NAMES[attribute_name]
146
+ next unless variable_name
147
+
148
+ variables[variable_name] = @compute_resource.public_send(attribute_name)
149
+ end
150
+ end
151
+
125
152
  def render_template(mode)
126
153
  template = provision_template
127
154
  variables = {
@@ -160,7 +187,10 @@ module ForemanOpentofu
160
187
  end
161
188
 
162
189
  def plan_wants_recreate?(plan)
163
- need_recreate = plan['resource_changes'].select { |res| (res.dig('change', 'actions') || []).include?('delete') }
190
+ need_recreate = plan['resource_changes'].select do |res|
191
+ actions = res.dig('change', 'actions') || []
192
+ actions.include?('delete') && actions.include?('create')
193
+ end
164
194
  need_recreate = @compute_resource.tofu_provider.filter_resource_changes need_recreate
165
195
  # {
166
196
  # "address": "hcloud_server.node1",