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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/foreman_opentofu/compute_resources_controller.rb +22 -0
- data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +14 -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 +2 -2
- data/app/models/concerns/orchestration/tofu/compute.rb +2 -0
- data/app/models/foreman_opentofu/compute_vm.rb +22 -0
- data/app/models/foreman_opentofu/opentofu_vm_commands.rb +20 -21
- 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 +50 -20
- data/app/services/foreman_opentofu/provider_type.rb +95 -5
- 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/templates/provisioning/hetzner_provision_host.erb +2 -2
- data/app/views/templates/provisioning/stackit_provision_default.erb +102 -0
- data/lib/foreman_opentofu/engine.rb +2 -0
- data/lib/foreman_opentofu/provider_types/hetzner.rb +19 -1
- data/lib/foreman_opentofu/provider_types/nutanix.rb +11 -4
- data/lib/foreman_opentofu/provider_types/stackit.rb +245 -0
- data/lib/foreman_opentofu/version.rb +1 -1
- 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 +2 -2
- data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.hetzner_host.snap.txt +2 -2
- data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +17 -0
- 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 +38 -0
- data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +186 -18
- data/test/models/foreman_opentofu/tofu_test.rb +25 -2
- 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 +129 -0
- data/test/services/opentofu_executer_test.rb +81 -4
- metadata +21 -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
|
|
@@ -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
|
|
@@ -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::
|
|
89
|
-
e,
|
|
87
|
+
raise ::Foreman::Exception.new(
|
|
90
88
|
N_(
|
|
91
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: #{
|
|
209
|
+
Rails.logger.error "Command failed with output: #{readable}"
|
|
147
210
|
# TODO: do we need to use a specific exception-type here?
|
|
148
|
-
raise
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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",
|