foreman_opentofu 0.0.5 → 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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/app/controllers/api/v2/tf_states_controller.rb +13 -13
  4. data/app/controllers/concerns/foreman_opentofu/compute_resources_controller.rb +22 -0
  5. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +25 -10
  6. data/app/models/concerns/foreman_opentofu/compute_attribute_validation.rb +25 -0
  7. data/app/models/concerns/foreman_opentofu/power_commands.rb +57 -0
  8. data/app/models/concerns/foreman_opentofu/vm_command_collection_normalization.rb +28 -3
  9. data/app/models/concerns/orchestration/tofu/compute.rb +2 -0
  10. data/app/models/foreman_opentofu/compute_vm.rb +50 -2
  11. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +22 -22
  12. data/app/models/foreman_opentofu/tofu.rb +14 -6
  13. data/app/services/foreman_opentofu/app_wrapper.rb +72 -7
  14. data/app/services/foreman_opentofu/opentofu_executer.rb +67 -16
  15. data/app/services/foreman_opentofu/provider_type.rb +101 -5
  16. data/app/services/foreman_opentofu/renderer/source/snapshot.rb +25 -0
  17. data/app/services/foreman_opentofu/template_snapshot_service.rb +68 -0
  18. data/app/views/api/v2/compute_resources/tofu.json.rabl +1 -0
  19. data/app/views/compute_resources/form/_tofu.html.erb +21 -4
  20. data/app/views/compute_resources/show/_tofu.html.erb +6 -4
  21. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +11 -2
  22. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +4 -1
  23. data/app/views/compute_resources_vms/form/tofu/_volume.html.erb +4 -2
  24. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_interfaces_fields.html.erb +1 -0
  25. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_volumes_fields.html.erb +8 -1
  26. data/app/views/templates/provisioning/hetzner_provision_host.erb +7 -22
  27. data/app/views/templates/provisioning/stackit_provision_default.erb +102 -0
  28. data/lib/foreman_opentofu/engine.rb +3 -0
  29. data/lib/foreman_opentofu/provider_types/hetzner.rb +24 -2
  30. data/lib/foreman_opentofu/provider_types/nutanix.rb +12 -5
  31. data/lib/foreman_opentofu/provider_types/stackit.rb +245 -0
  32. data/lib/foreman_opentofu/version.rb +1 -1
  33. data/lib/tasks/snapshots.rake +63 -0
  34. data/test/controllers/api/v2/tf_states_controller_test.rb +71 -2
  35. data/test/controllers/compute_resources_controller_test.rb +40 -0
  36. data/test/factories/compute_resources.rb +8 -8
  37. data/test/fixtures/foreman_opentofu/app_wrapper/apply_error.jsonl +9 -0
  38. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.empty.snap.txt +72 -0
  39. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.hetzner_host.snap.txt +74 -0
  40. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.empty.snap.txt +56 -0
  41. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.nutanix_host.snap.txt +62 -0
  42. data/test/fixtures/renderer/snapshots.yaml +6 -0
  43. data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +50 -17
  44. data/test/lib/foreman_opentofu/provider_types/hetzner_test.rb +45 -0
  45. data/test/lib/foreman_opentofu/provider_types/nutanix_test.rb +40 -0
  46. data/test/lib/foreman_opentofu/provider_types/stackit_test.rb +52 -0
  47. data/test/models/foreman_opentofu/compute_attribute_validation_test.rb +79 -0
  48. data/test/models/foreman_opentofu/compute_vm_test.rb +70 -0
  49. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +224 -14
  50. data/test/models/foreman_opentofu/tofu_test.rb +25 -2
  51. data/test/renderer/renderer_test.rb +60 -0
  52. data/test/renderer/stackit_template_test.rb +210 -0
  53. data/test/services/app_wrapper_test.rb +145 -0
  54. data/test/services/foreman_opentofu/provider_type_test.rb +139 -0
  55. data/test/services/opentofu_executer_test.rb +105 -1
  56. data/test/services/tofu_key_pair_test.rb +4 -1
  57. metadata +39 -4
  58. data/app/views/templates/provisioning/ovirt_provision_default.erb +0 -43
  59. 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: 13aa21537aa49784d6b945e5e0169c20509c46e361e951b6db79a17d330559ef
4
- data.tar.gz: e0f23b3286a236bd161421d8464518a9b47878d61e7ebd03ab41b361cb0b513f
3
+ metadata.gz: 8eceff0c5cce2e6fde716720f6262051d3ca5cd6355ca21db998d631f559eb78
4
+ data.tar.gz: 7e4ba716aca8ce9e6fede8abb74cfe33aae9838b02646a4804d008c9203f706a
5
5
  SHA512:
6
- metadata.gz: f1459d8aa7ad4e99b3d43b2725093f7cdc06d7473073432ca8386f46b1d2db4a5ac3acad3ec7c3580830e2d13a11e25354713c9a702a1f3ac669a527a504137f
7
- data.tar.gz: 4724e84ebc240ef60bdd95c65710fc3156f718628d38ba65570d8305df762be8883e00df13783002927589cdb2e7eed0304b98b414e5ac4e1edf9a48c5ca3d8c
6
+ metadata.gz: 2db6f7efade936b283136c2b47dea0bd8cded850c94cdae3ef085f0a7f38c388f849f3ea7cb0fc398c4c3171c47cf0737d04b000c531373b8757cdd8607a3dc7
7
+ data.tar.gz: c0a8e35c4b2d39281bfbad14109be06b37f066b71738defbaff167bb235dee73978fa39811d5c07a25d258f23527dffae02e8ad4d9af186b3e8065b3ddee5edf
data/README.md CHANGED
@@ -337,6 +337,23 @@ bundle exec foreman start
337
337
 
338
338
  See details in [foreman plugin development](https://projects.theforeman.org/projects/foreman/wiki/How_to_Create_a_Plugin)
339
339
 
340
+
341
+ ### Testing
342
+
343
+ #### Template Snapshots
344
+
345
+ The OpenTofu templates are tested against recorded snapshots similar to the Provisioning Templates in Foreman.
346
+ The snapshots are generated by rendering the templates with predefined fixture-data using the following command in the foreman-directory:
347
+
348
+ ```shell
349
+ bundle exec rake foreman_opentofu:snapshots:generate RAILS_ENV=test
350
+ ```
351
+
352
+ At the moment the fixture-data is defined in `app/services/foreman_opentofu/template_snapshot_service.rb`.
353
+
354
+ Templates to test must be added to `test/fixtures/renderer/snapshots.yaml`.
355
+
356
+
340
357
  ## Contributing
341
358
 
342
359
  Fork and send a Pull Request or create Issue. Thank you.
@@ -2,10 +2,8 @@ module Api
2
2
  module V2
3
3
  class TfStatesController < ::Api::V2::BaseController
4
4
  include ::Api::Version2
5
+ include ::Foreman::Controller::FilterParameters
5
6
 
6
- # TODO: verify this
7
- # We don't require any of these methods for provisioning
8
- # skip_before_action :require_login, :check_user_enabled, :session_expiry, :update_activity_time, :set_taxonomy, :authorize, unless: -> { preview? }
9
7
  skip_before_action :set_taxonomy
10
8
 
11
9
  # Allow HTTP POST methods without CSRF
@@ -14,6 +12,11 @@ module Api
14
12
  # overwrite authorize with the local token-based authorization
15
13
  before_action :authorize, except: [:destroy]
16
14
 
15
+ # Don't log tfstate response in logs as it can have sensitive information
16
+ skip_after_action :log_response_body
17
+
18
+ filter_parameters :version, :terraform_version, :serial, :lineage, :outputs, :resources, :tf_state, :check_results
19
+
17
20
  resource_description do
18
21
  api_version 'v2'
19
22
  api_base_url '/foreman_opentofu/api'
@@ -22,15 +25,13 @@ module Api
22
25
  def show
23
26
  state = ForemanOpentofu::TfState.find_by(name: params[:name])
24
27
 
25
- if state
26
- render plain: state.state, content_type: 'application/json'
27
- else
28
- render plain: '', status: :not_found
29
- end
28
+ return head :not_found unless state
29
+
30
+ render plain: state.state, content_type: 'application/json'
30
31
  end
31
32
 
32
33
  def create
33
- raw_state = request.body.read
34
+ raw_state = request.raw_post
34
35
  if raw_state.blank?
35
36
  render plain: 'Missing state body', status: :unprocessable_entity
36
37
  return
@@ -41,10 +42,9 @@ module Api
41
42
  state = ForemanOpentofu::TfState.find_or_initialize_by(name: params[:name])
42
43
  state.state = raw_state
43
44
  state.save!
44
- render plain: '', status: :ok
45
- rescue JSON::ParserError => e
46
- Rails.logger.error("Invalid state JSON: #{e.message}")
47
- render plain: 'Invalid state format', status: :unprocessable_entity
45
+ head :ok
46
+ rescue JSON::ParserError
47
+ render plain: 'Invalid state format', status: :bad_request
48
48
  end
49
49
  end
50
50
 
@@ -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
@@ -97,6 +99,8 @@ module ForemanOpentofu
97
99
  end
98
100
 
99
101
  def build_disks
102
+ return rendered_disk(0, disk_render_source.first).to_s if disk_renderer_collection?
103
+
100
104
  disk_render_source.each_with_index.filter_map do |disk, index|
101
105
  # drop removed disks; tofu will drop them automatically, if they are no longer defined
102
106
  next if disk.respond_to?(:[]) && disk['_delete'].to_i == 1
@@ -106,6 +110,8 @@ module ForemanOpentofu
106
110
  end
107
111
 
108
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
+
109
115
  nics_from_cr_attrs.each_with_index.map do |nic, index|
110
116
  data = @compute_resource.render_nic(nic, self, index)
111
117
  render_provider_data(data)
@@ -151,6 +157,15 @@ module ForemanOpentofu
151
157
  to_hcl(data, snippet: true)
152
158
  end
153
159
  end
160
+
161
+ def disk_renderer_collection?
162
+ return false unless @compute_resource.respond_to?(:tofu_provider)
163
+
164
+ provider = @compute_resource.tofu_provider
165
+ provider.respond_to?(:disk_renderer_collection?) && provider.disk_renderer_collection?
166
+ rescue StandardError
167
+ false
168
+ end
154
169
  end
155
170
  end
156
171
  end
@@ -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)
@@ -29,13 +29,38 @@ module ForemanOpentofu
29
29
  end
30
30
 
31
31
  def normalize_collection_input(collection, value)
32
- return nested_attributes_for(collection, value) if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
32
+ return normalize_nested_hash_collection(collection, value) if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
33
33
 
34
34
  Array(value).map do |entry|
35
35
  entry.respond_to?(:deep_symbolize_keys) ? entry.deep_symbolize_keys : entry
36
36
  end
37
37
  end
38
38
 
39
+ def normalize_nested_hash_collection(collection, value)
40
+ values = (value.respond_to?(:to_unsafe_h) ? value.to_unsafe_h : value.to_h).deep_stringify_keys
41
+ values.delete("new_#{collection}")
42
+
43
+ values.sort_by { |key, _| key.to_s.sub('new_', '').to_i }.filter_map do |_, val|
44
+ normalized = normalize_nested_collection_value(val)
45
+ next if normalized.is_a?(Hash) && normalized[:_delete] == '1' && normalized[:id].blank?
46
+
47
+ normalized
48
+ end
49
+ end
50
+
51
+ def normalize_nested_collection_value(value)
52
+ normalized =
53
+ if value.is_a?(ActionController::Parameters) && value.respond_to?(:to_unsafe_h)
54
+ value.to_unsafe_h
55
+ elsif value.respond_to?(:to_h)
56
+ value.to_h
57
+ else
58
+ value
59
+ end
60
+
61
+ normalized.respond_to?(:deep_symbolize_keys) ? normalized.deep_symbolize_keys : normalized
62
+ end
63
+
39
64
  def prefill_mandatory_attributes(args)
40
65
  # FIXME: add volume/nic attributes
41
66
  res = {}
@@ -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}"
@@ -14,13 +14,24 @@ module ForemanOpentofu
14
14
  end
15
15
 
16
16
  def to_h
17
- unwrap(@attributes.to_dup)
17
+ unwrap(@attributes.deep_dup)
18
18
  end
19
19
 
20
20
  def power
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'
@@ -30,6 +41,21 @@ module ForemanOpentofu
30
41
  self['name']
31
42
  end
32
43
 
44
+ def vm_description
45
+ return nil unless @provider.respond_to?(:tofu_provider)
46
+
47
+ vm_attrs = @provider.tofu_provider.attributes('vm')
48
+ values = vm_attrs.filter_map do |attr|
49
+ key = attr['name'].to_s
50
+ value = attribute_value(key)
51
+ next if value.blank?
52
+
53
+ label = attr['label'].presence || key.humanize
54
+ "#{label}: #{value}"
55
+ end
56
+ values.join(', ') if values.present?
57
+ end
58
+
33
59
  def persisted?
34
60
  self['identity'].present? || self['id'].present?
35
61
  end
@@ -61,6 +87,17 @@ module ForemanOpentofu
61
87
  instance_eval(&block)
62
88
  end
63
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
+
64
101
  def volumes_attributes
65
102
  vols_attrs = attribute_value('volumes_attributes')
66
103
  return vols_attrs if vols_attrs.is_a?(Hash)
@@ -119,6 +156,17 @@ module ForemanOpentofu
119
156
  end
120
157
  end
121
158
 
159
+ def unwrap(value)
160
+ case value
161
+ when Hash
162
+ value.transform_values { |v| unwrap(v) }
163
+ when Array
164
+ value.map { |v| unwrap(v) }
165
+ else
166
+ value
167
+ end
168
+ end
169
+
122
170
  def flatten_attrs(attrs)
123
171
  result = {}
124
172
 
@@ -144,7 +192,7 @@ module ForemanOpentofu
144
192
  key = method_name.to_s
145
193
  return attribute_value(key) if dynamic_attribute_keys.include?(key)
146
194
 
147
- super
195
+ nil
148
196
  end
149
197
  end
150
198
  end
@@ -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,19 +18,24 @@ 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'] || {}
21
- OpenStruct.new(attrs)
26
+ attrs = args.deep_stringify_keys.merge(attrs.to_h.deep_stringify_keys)
27
+ ComputeVM.new(self, attrs)
22
28
  end
23
29
  end
24
30
 
25
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
+
26
36
  vm_command_errors('create vm') do
27
- args = default_attributes.merge(args).to_h.symbolize_keys
28
- normalize_vm_args_collections!(args)
29
37
  executor = client(args)
30
- output = executor.run_create
38
+ output = executor.run_create(cleanup_on_failure: true)
31
39
  ComputeVM.new(self, output)
32
40
  end
33
41
  end
@@ -41,24 +49,16 @@ module ForemanOpentofu
41
49
  tf_state.destroy
42
50
  end
43
51
 
44
- def start_vm(name)
45
- output = client({ 'name' => name, 'power_state' => 'on' }).run_create
46
- output['vm']['power_state'] == 'on'
47
- end
48
-
49
- def stop_vm(name)
50
- output = client({ 'name' => name, 'power_state' => 'off' }).run_create
51
- output['vm']['power_state'] == 'off'
52
- end
53
-
54
52
  def save_vm(uuid, attrs)
55
53
  old_attrs = vm_compute_attributes_for(uuid).to_h.deep_stringify_keys
56
54
  tf_state = TfState.find_by(uuid: uuid)
57
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
+
58
61
  vm_command_errors('update vm') do
59
- new_attrs = attrs.to_h.deep_stringify_keys
60
- merged_attrs = old_attrs.merge(new_attrs).deep_symbolize_keys
61
- normalize_vm_args_collections!(merged_attrs)
62
62
  data = client({ 'name' => tf_state.name }.merge(merged_attrs)).run_create(raise_if_recreate: true)
63
63
  ComputeVM.new(self, data)
64
64
  end
@@ -84,13 +84,13 @@ module ForemanOpentofu
84
84
  yield
85
85
  rescue StandardError => e
86
86
  Foreman::Logging.exception("Caught #{provider} error", e)
87
- raise ::Foreman::WrappedException.new(
88
- e,
87
+ raise ::Foreman::Exception.new(
89
88
  N_(
90
- "Foreman could not find a required %<provider>s resource in #{method_name}. " \
91
- 'Check if Foreman has the required permissions and the resource exists. Reason: %<error>s'
89
+ '%<provider>s failed to %<method>s. Reason: %<error>s'
92
90
  ),
93
- { provider: provider, error: e.message }
91
+ provider: provider,
92
+ method: method_name,
93
+ error: e.message
94
94
  )
95
95
  end
96
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