foreman_opentofu 0.0.2 → 0.0.3

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +67 -0
  3. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +61 -53
  4. data/app/lib/foreman_opentofu/hcl_format.rb +95 -0
  5. data/app/lib/foreman_opentofu/nic_helpers.rb +47 -0
  6. data/app/models/concerns/orchestration/tofu/compute.rb +25 -6
  7. data/app/models/foreman_opentofu/compute_vm.rb +10 -0
  8. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +12 -8
  9. data/app/models/foreman_opentofu/tofu.rb +33 -8
  10. data/app/services/foreman_opentofu/app_wrapper.rb +37 -14
  11. data/app/services/foreman_opentofu/opentofu_executer.rb +66 -28
  12. data/app/services/foreman_opentofu/provider_type.rb +51 -7
  13. data/app/views/compute_resources/form/_tofu.html.erb +3 -0
  14. data/app/views/compute_resources/show/_tofu.html.erb +4 -0
  15. data/app/views/compute_resources/tofu.json.rabl +1 -0
  16. data/app/views/compute_resources_vms/form/tofu/_base.html.erb +13 -2
  17. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +17 -7
  18. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +1 -5
  19. data/app/views/images/form/_tofu.html.erb +4 -0
  20. data/app/views/templates/provisioning/hetzner_provision_host.erb +62 -0
  21. data/app/views/templates/provisioning/nutanix_provision_default.erb +30 -22
  22. data/app/views/templates/provisioning/ovirt_provision_default.erb +7 -29
  23. data/lib/foreman_opentofu/provider_types/hetzner.rb +66 -0
  24. data/lib/foreman_opentofu/provider_types/nutanix.rb +48 -0
  25. data/lib/foreman_opentofu/provider_types/ovirt.rb +19 -0
  26. data/lib/foreman_opentofu/version.rb +1 -1
  27. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/backend_block.txt +6 -0
  28. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block.txt +8 -0
  29. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block_with_token.txt +14 -0
  30. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/vm_attributes.txt +3 -0
  31. data/test/fixtures/snapshots/foreman_opentofu/hcl_format_test/to_hcl.txt +19 -0
  32. data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes.txt +8 -0
  33. data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes_block.txt +6 -0
  34. data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +110 -0
  35. data/test/lib/foreman_opentofu/concerns/nic_helpers_test.rb +36 -0
  36. data/test/lib/foreman_opentofu/hcl_format_test.rb +72 -0
  37. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +14 -14
  38. data/test/models/foreman_opentofu/tofu_test.rb +22 -0
  39. data/test/services/app_wrapper_test.rb +39 -1
  40. data/test/services/foreman_opentofu/provider_type_test.rb +102 -10
  41. data/test/services/opentofu_executer_test.rb +27 -9
  42. data/test/test_plugin_helper.rb +6 -0
  43. metadata +28 -6
  44. data/app/services/foreman_opentofu/compute_fetcher.rb +0 -51
  45. data/config/initializers/compute_attrs.rb +0 -19
  46. data/config/nutanix.json +0 -27
  47. data/config/ovirt.json +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6bff811eebb374eb3983de89c83faf991c8021e416c677bbfda90837cbeeb5a
4
- data.tar.gz: 8741e8b977ba0a078c13ac4642a790e11646f719358f22b69c299368d698a8f0
3
+ metadata.gz: ccfa3742aa9fe09edc20f685bf1a65f0f32aa34603572812fd250f64f84c7f60
4
+ data.tar.gz: 632627c1dbecb3da3282fa29085647263f588eb99753db33d2f2ada53b57680c
5
5
  SHA512:
6
- metadata.gz: 67e87113d04444061daa977fca5721d26122035fd821f86284667eb5ad247c7a2002d5bd1d451ec0d4cea84ade9f530eb1dcffa3afb6b931b7a0b78f1f57c5ca
7
- data.tar.gz: d2627a46149453ae41fa6f5463bd792501ee51bc0760693a6bafdd3f0da7357ab9e8b8093c98149b5c4b4c7b2e8e03698c8d97996bb1ec3d1ac0990bc719a455
6
+ metadata.gz: 1858acdfd4b8e7e32dd3c91765ca1e5e6921da70b8754842e1fa8284bb9b1ceb23b5abb7b22320218d6326167f821d032071915458155545e19ace88dfadd996
7
+ data.tar.gz: a9f29bd6e263acb43c2a692ed848a71b99c61dce3fb3b64a9ebd7597bceae0280f8b93879b2e5b89a6a8a2244f9cfd5644fdfdc20444353fc962211def091591
data/README.md CHANGED
@@ -90,6 +90,73 @@ A short config file might look like this:
90
90
 
91
91
  The name of the file must be the same as the provider-type name we set in the next step (e.g. `/config/nutanix.json`).
92
92
 
93
+ ##### Dynamic Config Parameter
94
+
95
+ Sometimes it is necessary to provide a list of possible values that are defined by the backend-service.
96
+ Curating the 'options'-Array is tedious at best or not possible if multiple instances of the backend service are in use.
97
+ This can be addressed by specifiying an OpenTofu-Provider's [DataSource](https://opentofu.org/docs/language/data-sources/) in the following way:
98
+
99
+ ```json
100
+ {
101
+ "name": "volume_group", "type": "select", "group": "disk", "mandatory": true, "label": "Volume Group",
102
+ "options": {
103
+ "data_source": {
104
+ "name": "nutanix_volume_groups_v2",
105
+ "arguments": {
106
+ "filter": "name eq 'volume_group_test'",
107
+ "limit": 20
108
+ },
109
+ "entity": {
110
+ "id": "metadata.uuid"
111
+ }
112
+ },
113
+ "output_path_postfix": "volume_groups"
114
+ }
115
+ }
116
+
117
+ ```
118
+ The GUI requires a list of objects that at least contain a name and an id for each select-option.
119
+ The `entity` section can be used to define a specific value from an object within the list that the DataSource returns.
120
+ If the object already has `name` and `id` entries, these will automatically used.
121
+ In the above example `name` exists in the object and can be used.
122
+ For the `id` however, a different value must be selected from the object.
123
+
124
+ This requests the data via OpenTofu in the following construct:
125
+
126
+ ```hcl
127
+ data "nutanix_volume_groups_v2" "all" {
128
+ filter = "name eq 'volume_group_test'"
129
+ limit = 20
130
+ }
131
+ output "resources" {
132
+ value = [ for e in data.nutanix_volume_groups_v2.all.volume_groups: {
133
+ id = e.metadata.uuid
134
+ name = e.name
135
+ } ]
136
+ }
137
+ ```
138
+
139
+ ##### Special Parameter
140
+
141
+ Some config parameter names have special meanings.
142
+ For instance, Image-based Deployment requires binding images available on the backend-service with Operating Systems configured in Foreman.
143
+ To enable Foreman OpenTofu to display the available images, a `select`-parameter with the name `available_images` must be specified.
144
+ It is recommended to tie this to a data-source available in the OpenTofu provider.
145
+
146
+ ```json
147
+ {
148
+ "name": "available_images", "type": "select",
149
+ "options": {
150
+ "data_source": {
151
+ "name": "hcloud_images",
152
+ "arguments": { "with_architecture": ["x86"] }
153
+ },
154
+ "output_path_postfix": "images"
155
+ }
156
+ }
157
+ ```
158
+
159
+
93
160
  #### Create Provider Type
94
161
 
95
162
  To let the Foreman OpenTofu Plugin know about your new Provider Type, one additional file has to be created in `/lib/foreman_opentofu/provider_types/`.
@@ -3,19 +3,65 @@ module ForemanOpentofu
3
3
  module BaseTemplateScopeExtensions
4
4
  extend ActiveSupport::Concern
5
5
  extend ApipieDSL::Module
6
+ include ForemanOpentofu::HclFormat
7
+ include ForemanOpentofu::NicHelpers
6
8
 
7
9
  apipie :class, 'Base macros related to Opentofu templates' do
8
- name 'Base Content'
9
- sections only: %w[all provisioning]
10
+ name 'OpenTofu helpers'
11
+ sections only: %w[opentofu_script]
12
+ end
13
+
14
+ apipie :method, 'Returns `terraform`-block including necessary backend definition, if applicable' do
15
+ required :data, Hash, desc: 'Define the provider-type to pull in, e.g. `{ \'nutanix\' => { \'source\' => \'nutanix/nutanix\', \'version\' => \'2.4.0\' }`'
16
+ returns String, desc: '`terraform {}`-block based on the data-input, if applicable with TfState-Backend definition.'
17
+ end
18
+ # e.g. terraform_block({ 'nutanix' => { 'source' => 'nutanix/nutanix', 'version' => '2.4.0' })
19
+ def terraform_block(data)
20
+ block = block_to_hcl(['terraform'])
21
+ block << '{'
22
+ block << block_to_hcl(['required_providers'], data, depth: 1)
23
+ block << backend_block
24
+ block << "\n}"
25
+ end
26
+
27
+ apipie :method, 'Add "resource" data_source block' do
28
+ returns String, desc: ''
29
+ end
30
+ def resource_block(resource)
31
+ block = ''
32
+ path = ['data', resource[:name], 'all']
33
+
34
+ # data "<%= @resource[:name] %>" "all" {
35
+ # <% @resource.dig(:options, 'data_source', 'arguments')&.each do |key, value| %>
36
+ # <%= key %> = <%= value.inspect %>
37
+ # <% end %>
38
+ # }
39
+ block << block_to_hcl(path, resource.dig(:options, 'data_source', 'arguments') || {})
40
+
41
+ # output "resources" {
42
+ # value = [ for e in data.<%= @resource[:name] %>.all.<%= @resource.dig(:options, 'output_path_postfix') %>: {
43
+ # id = e.<%= @resource.dig(:options, 'entity', 'id') || 'id' %>
44
+ # name = e.<%= @resource.dig(:options, 'entity', 'name') || 'name' %>
45
+ # # obj = e
46
+ # } ]
47
+ # }
48
+ block << block_to_hcl(%w[output resources])
49
+ block << '{' << "\n"
50
+ block << " value = [ for e in data.#{resource[:name]}.all.#{resource.dig(:options, 'output_path_postfix')}: {\n"
51
+ block << " id = e.#{resource.dig(:options, 'entity', 'id') || 'id'}\n"
52
+ block << " name = e.#{resource.dig(:options, 'entity', 'name') || 'name'}\n"
53
+ # block << 'obj = e'
54
+ block << ' } ]' << "\n"
55
+ block << '}' << "\n"
10
56
  end
11
57
 
12
58
  apipie :method, 'Returns all VM parameters' do
13
59
  required :skip_list, Array, desc: 'List of parameters to skip'
14
60
  returns String, desc: '"key = value" lines'
15
61
  end
16
-
17
62
  def vm_attributes(skip_list = [])
18
63
  available_attributes = @compute_resource.available_attributes
64
+ data = {}
19
65
  res = ''
20
66
  @cr_attrs.each do |key, value|
21
67
  next if skip_list.include? key
@@ -28,60 +74,22 @@ module ForemanOpentofu
28
74
  next if conf['group'] != 'vm'
29
75
  next if value.blank? && !conf['mandatory']
30
76
 
31
- res << "#{key} = #{format_value(value, conf['type'])}\n"
32
- end
33
- res << nic_attributes(available_attributes)
34
- end
35
-
36
- def nic_attributes(available_attributes)
37
- interfaces = @cr_attrs['interfaces'] || @cr_attrs['interfaces_attributes']
38
- return '' if interfaces.blank?
39
-
40
- interfaces = normalize_interfaces(interfaces)
41
- nic_defs = available_attributes.values.select do |attrs|
42
- attrs['group'] == 'nic'
43
- end
44
- res = ''
45
- interfaces.each do |iface|
46
- next if iface['subnet_uuid'].blank?
47
-
48
- res << build_attribute_block('nic_list', iface, nic_defs)
77
+ data[key] = format_value(value, conf['type'])
49
78
  end
50
- res
79
+ res << to_hcl(data, snippet: true)
51
80
  end
52
81
 
53
- def normalize_interfaces(interfaces)
54
- if interfaces.is_a?(Hash)
55
- if interfaces.keys.all? { |k| k.to_s =~ /^\d+$/ }
56
- interfaces.values
57
- else
58
- [interfaces]
59
- end
82
+ def backend_block
83
+ if @token
84
+ data = {
85
+ address: "#{Setting[:foreman_url]}/api/v2/tf_states/#{@host_name}",
86
+ headers: {
87
+ 'Authorization' => "Token #{@token}",
88
+ },
89
+ }
90
+ block_to_hcl(%w[backend http], data, depth: 1)
60
91
  else
61
- Array(interfaces)
62
- end
63
- end
64
-
65
- def build_attribute_block(block_name, attrs, nic_defs)
66
- res = "#{block_name} {\n"
67
- attrs.each do |k, v|
68
- next if v.blank?
69
- conf = nic_defs.find { |a| (a['name'] || a[:name]) == k }
70
- next unless conf
71
- res << " #{k} = #{format_value(v, conf['type'])}\n" if conf
72
- end
73
- res << "}\n"
74
- res
75
- end
76
-
77
- private
78
-
79
- def format_value(val, type)
80
- case type
81
- when 'string', 'select' then "\"#{val}\""
82
- when 'bool' then Foreman::Cast.to_bool(val)
83
- when 'number' then val.to_i
84
- else val
92
+ ''
85
93
  end
86
94
  end
87
95
  end
@@ -0,0 +1,95 @@
1
+ module ForemanOpentofu
2
+ module HclFormat
3
+ def default_opts(opts = {})
4
+ {
5
+ indent: 2,
6
+ depth: 0,
7
+ snippet: false,
8
+ }.merge opts
9
+ end
10
+
11
+ # possible `opts`:
12
+ # `indent` : number of whitespace to indent; default 2
13
+ # `depth` : start value of depth (for indentation); default: 0
14
+ # `snippet`: if `true` and var is a `Hash` string will not be framed with `{}`
15
+ def to_hcl(var, opts = {})
16
+ opts = default_opts.merge(opts)
17
+
18
+ case var
19
+ when Hash then hash_to_hcl(var, opts)
20
+ when Array then array_to_hcl(var, opts)
21
+ when String then var.inspect
22
+ else var.to_s
23
+ end
24
+ end
25
+
26
+ def prefix_hcl(opts)
27
+ "\n#{' ' * opts[:indent] * opts[:depth]}"
28
+ end
29
+
30
+ # output a hcl-block:
31
+ # hello "how" "are" "you" { ... }
32
+ # the above would be created by: block_to_hcl(['hello,'how', 'are', 'you'], { .. })
33
+ def block_to_hcl(names, content = nil, opts = {})
34
+ opts = default_opts(opts)
35
+
36
+ hcl = prefix_hcl(opts)
37
+ hcl << names[0]
38
+ hcl << ' '
39
+ # sub-block-names are quoted
40
+ hcl << names[1..].map(&:to_s).map(&:inspect).join(' ')
41
+ hcl << ' ' if hcl[-1] != ' '
42
+ hcl << to_hcl(content, opts)
43
+ end
44
+
45
+ def hash_to_hcl(hsh, opts)
46
+ opts = default_opts(opts)
47
+ hcl = ''
48
+ new_opts = opts.merge(snippet: false)
49
+
50
+ unless opts[:snippet]
51
+ hcl << '{'
52
+ new_opts[:depth] = opts[:depth] + 1
53
+ end
54
+
55
+ close_block_on_newline = false
56
+
57
+ hsh.each do |key, value|
58
+ hcl << prefix_hcl(new_opts)
59
+ hcl << "#{key} = #{to_hcl(value, new_opts)}"
60
+ close_block_on_newline = true
61
+ end
62
+ hcl << prefix_hcl(opts) if close_block_on_newline
63
+ hcl << '}' unless opts[:snippet]
64
+ hcl
65
+ end
66
+
67
+ def array_to_hcl(hsh, opts)
68
+ opts = default_opts(opts)
69
+ hcl = '['
70
+ new_opts = opts.merge(
71
+ depth: opts[:depth] + 1
72
+ )
73
+ close_block_on_newline = false
74
+
75
+ hsh.each do |value|
76
+ hcl << prefix_hcl(new_opts)
77
+ hcl << to_hcl(value, new_opts)
78
+ hcl << ','
79
+ close_block_on_newline = true
80
+ end
81
+ hcl.chomp!(',')
82
+ hcl << prefix_hcl(opts) if close_block_on_newline
83
+ hcl << ']'
84
+ end
85
+
86
+ def format_value(val, type)
87
+ case type
88
+ when 'string', 'select' then val
89
+ when 'bool' then Foreman::Cast.to_bool(val)
90
+ when 'number' then val.to_i
91
+ else val
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,47 @@
1
+ module ForemanOpentofu
2
+ module NicHelpers
3
+ include ForemanOpentofu::HclFormat
4
+
5
+ def nic_attributes(block_name = nil)
6
+ nic_defs = @compute_resource.available_attributes('nic')
7
+ interfaces = normalize_interfaces(@cr_attrs['interfaces'] || @cr_attrs['interfaces_attributes'])
8
+ res = ''
9
+ interfaces.each do |iface|
10
+ missing_attrs = nic_defs.select { |name, cfg| cfg[:mandatory] && iface[name].blank? }
11
+ # TODO: log the fact that we skip this due to missing mandatory attributes
12
+ next unless missing_attrs.empty?
13
+
14
+ res << if block_given?
15
+ yield(iface, nic_defs)
16
+ else
17
+ block_to_hcl([block_name], sanitize_attributes(iface, nic_defs), depth: 1)
18
+ end
19
+ end
20
+ res
21
+ end
22
+
23
+ def normalize_interfaces(interfaces)
24
+ if interfaces.is_a?(Hash)
25
+ if interfaces.keys.all? { |k| k.to_s =~ /^\d+$/ }
26
+ interfaces.values
27
+ else
28
+ [interfaces]
29
+ end
30
+ else
31
+ Array(interfaces)
32
+ end
33
+ end
34
+
35
+ def sanitize_attributes(attrs, defs)
36
+ data = {}
37
+ attrs.each do |k, v|
38
+ next if v.blank?
39
+
40
+ next unless defs[k]
41
+
42
+ data[k] = format_value(v, defs.dig(k, :type))
43
+ end
44
+ data
45
+ end
46
+ end
47
+ end
@@ -3,13 +3,9 @@ module Orchestration
3
3
  module Compute
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def computeValue(_foreman_attr, fog_attr)
7
- value = ''
8
- value += vm.send(fog_attr).to_s
9
- value
10
- end
11
-
12
6
  def match_macs_to_nics(fog_attr)
7
+ return super unless compute_resource.is_a?(ForemanOpentofu::Tofu)
8
+
13
9
  interfaces.select(&:physical?).each do |nic|
14
10
  mac = vm.send(fog_attr)
15
11
  logger.debug "Orchestration::Compute: nic #{nic.inspect} assigned to #{vm.inspect}"
@@ -19,6 +15,29 @@ module Orchestration
19
15
  end
20
16
  true
21
17
  end
18
+
19
+ def setUserData
20
+ return super unless compute_resource.is_a?(ForemanOpentofu::Tofu)
21
+
22
+ logger.info "Rendering UserData template for #{name}"
23
+ template = provisioning_template(kind: 'cloud-init')
24
+ template ||= provisioning_template(kind: 'user_data')
25
+ # For some reason this renders as 'built' in spoof view but 'provision' when
26
+ # actually used. For now, use foreman_url('built') in the template
27
+ if template.nil?
28
+ # rubocop:disable Layout/LineLength
29
+ failure(format(_("Image \"%{image}\" needs user data, but \"%{os}\" is not associated to any provisioning template of the kind user_data. Please associate it with a suitable template or uncheck 'User data' from the image definition."),
30
+ image: image.name,
31
+ os: operatingsystem))
32
+ # rubocop:enable Layout/LineLength
33
+ return false
34
+ end
35
+
36
+ compute_attributes['user_data'] = render_template(template: template)
37
+
38
+ return false if errors.any?
39
+ true
40
+ end
22
41
  end
23
42
  end
24
43
  end
@@ -47,6 +47,16 @@ module ForemanOpentofu
47
47
  reboot
48
48
  end
49
49
 
50
+ def vm_ip_address
51
+ @attributes['vm_ip_address']
52
+ end
53
+
54
+ def wait_for(&block)
55
+ # TODO: I guess we have nothing to wait for
56
+ # and we need to change the context of the given block
57
+ instance_eval(&block)
58
+ end
59
+
50
60
  private
51
61
 
52
62
  def define_dynamic_readers!
@@ -3,7 +3,7 @@ module ForemanOpentofu
3
3
  def find_vm_by_uuid(uuid)
4
4
  vm_command_errors('find vm') do
5
5
  tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
6
- data = client({ 'name' => tf_state&.name }).run('output')
6
+ data = client({ 'name' => tf_state&.name }).run_output
7
7
  ComputeVM.new(self, data)
8
8
  end
9
9
  end
@@ -12,7 +12,7 @@ module ForemanOpentofu
12
12
  vm_command_errors('new vm') do
13
13
  args = default_attributes.merge(args)
14
14
  executor = client(args)
15
- data = executor.run('new')
15
+ data = executor.run_new
16
16
  OpenStruct.new(data['resource_changes'].first['change']['after'])
17
17
  end
18
18
  end
@@ -21,14 +21,14 @@ module ForemanOpentofu
21
21
  vm_command_errors('create vm') do
22
22
  args = default_attributes.merge(args)
23
23
  executor = client(args)
24
- output = executor.run('create')
24
+ output = executor.run_create
25
25
  ComputeVM.new(self, output)
26
26
  end
27
27
  end
28
28
 
29
29
  def destroy_vm(uuid)
30
30
  tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
31
- client({ 'name' => tf_state&.name }).run('destroy')
31
+ client({ 'name' => tf_state&.name }).run_destroy
32
32
  return unless tf_state
33
33
 
34
34
  Rails.logger.info "Deleting tfstate for #{tf_state&.name}"
@@ -36,12 +36,12 @@ module ForemanOpentofu
36
36
  end
37
37
 
38
38
  def start_vm(name)
39
- output = client({ 'name' => name, 'power_state' => 'on' }).run('create')
39
+ output = client({ 'name' => name, 'power_state' => 'on' }).run_create
40
40
  output['vm']['power_state'] == 'on'
41
41
  end
42
42
 
43
43
  def stop_vm(name)
44
- output = client({ 'name' => name, 'power_state' => 'off' }).run('create')
44
+ output = client({ 'name' => name, 'power_state' => 'off' }).run_create
45
45
  output['vm']['power_state'] == 'off'
46
46
  end
47
47
 
@@ -50,15 +50,19 @@ module ForemanOpentofu
50
50
  raise StandardError, "VM with UUID #{uuid} does not exist" unless tf_state
51
51
  vm_command_errors('update vm') do
52
52
  attrs = attrs.empty? ? {} : attrs.first
53
- data = client({ 'name' => tf_state.name }.merge(attrs)).run('create')
53
+ data = client({ 'name' => tf_state.name }.merge(attrs)).run_create
54
54
  ComputeVM.new(self, data)
55
55
  end
56
56
  end
57
57
 
58
+ def fetch_resource(resource_name = '', options = {})
59
+ client({ 'resource' => { name: resource_name, options: options } }).run_fetch
60
+ end
61
+
58
62
  def test_connection(options = {})
59
63
  super
60
64
  begin
61
- client.run('test_connection')
65
+ client.run_test_connection
62
66
  rescue StandardError => e
63
67
  Rails.logger.error("OpenTofu test connection failed: #{e.message}")
64
68
  errors.add(:base, e.message)
@@ -18,6 +18,7 @@
18
18
  module ForemanOpentofu
19
19
  class Tofu < ComputeResource
20
20
  include OpentofuVMCommands
21
+ include ComputeResourceCaching
21
22
  validates :provider, presence: true, inclusion: { in: %w[Tofu] }
22
23
  validates :url, presence: true
23
24
  validates :user, presence: true
@@ -26,12 +27,19 @@ module ForemanOpentofu
26
27
  # alias_attribute :username, :user
27
28
  # alias_attribute :endpoint, :url
28
29
 
29
- delegate :available_attributes, to: :tofu_provider
30
+ delegate :available_attributes, :capabilities, to: :tofu_provider
31
+
32
+ def available_images
33
+ # make sure available_images can use this CR, e.g. for requesting data_source
34
+ tofu_provider.available_images(self)
35
+ end
30
36
 
31
37
  def provided_attributes
32
- super.merge(
33
- mac: :mac
34
- )
38
+ super.merge(tofu_provider.provided_attributes || {})
39
+ end
40
+
41
+ def user_data_supported?
42
+ true
35
43
  end
36
44
 
37
45
  def opentofu_provider
@@ -60,10 +68,6 @@ module ForemanOpentofu
60
68
  'OpenTofu'
61
69
  end
62
70
 
63
- def capabilities
64
- [:build]
65
- end
66
-
67
71
  def self.model_name
68
72
  ComputeResource.model_name
69
73
  end
@@ -76,6 +80,12 @@ module ForemanOpentofu
76
80
  true
77
81
  end
78
82
 
83
+ def vm_ready(vm)
84
+ return tofu_provider.vm_ready(vm) if tofu_provider.respond_to? :vm_ready
85
+
86
+ vm.wait_for { ready? }
87
+ end
88
+
79
89
  def tofu_provider
80
90
  ProviderTypeManager.find(opentofu_provider)
81
91
  end
@@ -84,8 +94,23 @@ module ForemanOpentofu
84
94
  { compute_attributes: {} }
85
95
  end
86
96
 
97
+ def new_volume
98
+ { compute_attributes: {} }
99
+ end
100
+
87
101
  def editable_network_interfaces?
88
102
  true
89
103
  end
104
+
105
+ def available_resource(resource_name, options = {})
106
+ cache.cache("#{name}_#{resource_name}") do
107
+ resource = fetch_resource(resource_name, options)
108
+ resource.map { |h| OpenStruct.new(h) }
109
+ end
110
+ end
111
+
112
+ def available_resource_ui_select(resource_name, options = {})
113
+ available_resource(resource_name, options)&.map { |obj| [obj['name'], obj['id']] }
114
+ end
90
115
  end
91
116
  end
@@ -1,6 +1,7 @@
1
1
  module ForemanOpentofu
2
2
  class AppWrapper
3
- attr_reader :workdir, :planfile, :conffile
3
+ include HclFormat
4
+ attr_reader :workdir
4
5
 
5
6
  # TODO: for future versions
6
7
  # - manage temp-work-dir; problem: no auto-remove after finished :-(
@@ -9,14 +10,33 @@ module ForemanOpentofu
9
10
  # - use JSON-output for easier parsing
10
11
  # - do we need locking or has the object be atomic
11
12
 
12
- def initialize(workdir)
13
+ def initialize(workdir, opts = {})
13
14
  @workdir = workdir
14
- @planfile = File.join(workdir, 'plan.bin')
15
- @conffile = File.join(workdir, 'main.tf')
15
+ @variables = opts[:variables]
16
16
  end
17
17
 
18
- def base_command
19
- 'tofu'
18
+ # rubocop:disable Style/SingleLineMethods
19
+ def planfile() File.join(workdir, 'plan.bin') end
20
+ def conffile() File.join(workdir, 'main.tf') end
21
+ def vardeffile() File.join(workdir, 'variables.tf') end
22
+
23
+ def base_command() 'tofu' end
24
+ # rubocop:enable Style/SingleLineMethods
25
+
26
+ # write variables definition file based on @variables into 'variables.tf'
27
+ def create_variables_file
28
+ File.open(vardeffile, 'w') do |f|
29
+ @variables.each_key do |var|
30
+ data = { 'type' => :string }
31
+ data['sensitive'] = var.to_s == 'password'
32
+
33
+ f << block_to_hcl(['variable', var], data)
34
+ end
35
+ end
36
+ end
37
+
38
+ def variable_params
39
+ @variables.map { |var, val| ['-var', "#{var}=#{val}"] }.flatten
20
40
  end
21
41
 
22
42
  def default_params
@@ -32,6 +52,8 @@ module ForemanOpentofu
32
52
  # end
33
53
  # end
34
54
  def init(params = [], &block)
55
+ create_variables_file if @variables
56
+
35
57
  tofu_execute('init', ['-input=false'].concat(parse_params(params)), &block)
36
58
  end
37
59
 
@@ -78,18 +100,19 @@ module ForemanOpentofu
78
100
  cmd.map { |item| "'#{item}'" }.append('2>&1').join(' ')
79
101
  end
80
102
 
103
+ def envvars
104
+ @variables.transform_keys { |variable| "TF_VAR_#{variable}" }
105
+ end
106
+
81
107
  def execute(cmd)
82
108
  output = nil
83
109
  # quote cmdline parameters and add stderr to stdout
84
110
  commandline = command(cmd)
85
- Dir.chdir(workdir) do
86
- Rails.logger.debug "Start command: #{commandline.inspect}"
87
- IO.popen(commandline, 'r+') do |pipe|
88
- if block_given?
89
- yield pipe
90
- else
91
- output = pipe.read
92
- end
111
+ IO.popen(envvars, commandline, 'r+', chdir: workdir) do |pipe|
112
+ if block_given?
113
+ yield pipe
114
+ else
115
+ output = pipe.read
93
116
  end
94
117
  end
95
118
  ret = $CHILD_STATUS