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.
- checksums.yaml +4 -4
- data/README.md +17 -0
- data/app/controllers/api/v2/tf_states_controller.rb +13 -13
- data/app/controllers/concerns/foreman_opentofu/compute_resources_controller.rb +22 -0
- data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +25 -10
- data/app/models/concerns/foreman_opentofu/compute_attribute_validation.rb +25 -0
- data/app/models/concerns/foreman_opentofu/power_commands.rb +57 -0
- data/app/models/concerns/foreman_opentofu/vm_command_collection_normalization.rb +28 -3
- data/app/models/concerns/orchestration/tofu/compute.rb +2 -0
- data/app/models/foreman_opentofu/compute_vm.rb +50 -2
- data/app/models/foreman_opentofu/opentofu_vm_commands.rb +22 -22
- data/app/models/foreman_opentofu/tofu.rb +14 -6
- data/app/services/foreman_opentofu/app_wrapper.rb +72 -7
- data/app/services/foreman_opentofu/opentofu_executer.rb +67 -16
- data/app/services/foreman_opentofu/provider_type.rb +101 -5
- data/app/services/foreman_opentofu/renderer/source/snapshot.rb +25 -0
- data/app/services/foreman_opentofu/template_snapshot_service.rb +68 -0
- data/app/views/api/v2/compute_resources/tofu.json.rabl +1 -0
- data/app/views/compute_resources/form/_tofu.html.erb +21 -4
- data/app/views/compute_resources/show/_tofu.html.erb +6 -4
- data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +11 -2
- data/app/views/compute_resources_vms/form/tofu/_network.html.erb +4 -1
- data/app/views/compute_resources_vms/form/tofu/_volume.html.erb +4 -2
- data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_interfaces_fields.html.erb +1 -0
- data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_volumes_fields.html.erb +8 -1
- data/app/views/templates/provisioning/hetzner_provision_host.erb +7 -22
- data/app/views/templates/provisioning/stackit_provision_default.erb +102 -0
- data/lib/foreman_opentofu/engine.rb +3 -0
- data/lib/foreman_opentofu/provider_types/hetzner.rb +24 -2
- data/lib/foreman_opentofu/provider_types/nutanix.rb +12 -5
- data/lib/foreman_opentofu/provider_types/stackit.rb +245 -0
- data/lib/foreman_opentofu/version.rb +1 -1
- data/lib/tasks/snapshots.rake +63 -0
- data/test/controllers/api/v2/tf_states_controller_test.rb +71 -2
- data/test/controllers/compute_resources_controller_test.rb +40 -0
- data/test/factories/compute_resources.rb +8 -8
- data/test/fixtures/foreman_opentofu/app_wrapper/apply_error.jsonl +9 -0
- data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.empty.snap.txt +72 -0
- data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.hetzner_host.snap.txt +74 -0
- data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.empty.snap.txt +56 -0
- data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.nutanix_host.snap.txt +62 -0
- data/test/fixtures/renderer/snapshots.yaml +6 -0
- data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +50 -17
- data/test/lib/foreman_opentofu/provider_types/hetzner_test.rb +45 -0
- data/test/lib/foreman_opentofu/provider_types/nutanix_test.rb +40 -0
- data/test/lib/foreman_opentofu/provider_types/stackit_test.rb +52 -0
- data/test/models/foreman_opentofu/compute_attribute_validation_test.rb +79 -0
- data/test/models/foreman_opentofu/compute_vm_test.rb +70 -0
- data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +224 -14
- data/test/models/foreman_opentofu/tofu_test.rb +25 -2
- data/test/renderer/renderer_test.rb +60 -0
- data/test/renderer/stackit_template_test.rb +210 -0
- data/test/services/app_wrapper_test.rb +145 -0
- data/test/services/foreman_opentofu/provider_type_test.rb +139 -0
- data/test/services/opentofu_executer_test.rb +105 -1
- data/test/services/tofu_key_pair_test.rb +4 -1
- metadata +39 -4
- data/app/views/templates/provisioning/ovirt_provision_default.erb +0 -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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8eceff0c5cce2e6fde716720f6262051d3ca5cd6355ca21db998d631f559eb78
|
|
4
|
+
data.tar.gz: 7e4ba716aca8ce9e6fede8abb74cfe33aae9838b02646a4804d008c9203f706a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
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
|
-
|
|
45
|
-
rescue JSON::ParserError
|
|
46
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
78
|
-
next if value.blank? && !conf['mandatory']
|
|
76
|
+
next unless vm_attribute_included?(config, value)
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
attributes[key] = format_value(value, config['type'])
|
|
81
79
|
end
|
|
82
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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::
|
|
88
|
-
e,
|
|
87
|
+
raise ::Foreman::Exception.new(
|
|
89
88
|
N_(
|
|
90
|
-
|
|
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
|
-
|
|
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.
|
|
71
|
-
|
|
72
|
-
|
|
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
|