foreman_opentofu 0.0.2 → 0.0.4

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +109 -12
  3. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +87 -44
  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/foreman_opentofu/vm_command_collection_normalization.rb +22 -0
  7. data/app/models/concerns/orchestration/tofu/compute.rb +25 -6
  8. data/app/models/foreman_opentofu/compute_vm.rb +10 -0
  9. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +22 -11
  10. data/app/models/foreman_opentofu/tofu.rb +43 -10
  11. data/app/overrides/compute_resources_vms/tofu_indexed_networks_fields.rb +7 -0
  12. data/app/overrides/compute_resources_vms/tofu_indexed_volumes_fields.rb +8 -0
  13. data/app/services/foreman_opentofu/app_wrapper.rb +51 -14
  14. data/app/services/foreman_opentofu/opentofu_executer.rb +67 -29
  15. data/app/services/foreman_opentofu/provider_type.rb +61 -7
  16. data/app/views/compute_resources/form/_tofu.html.erb +3 -0
  17. data/app/views/compute_resources/show/_tofu.html.erb +4 -0
  18. data/app/views/compute_resources/tofu.json.rabl +1 -0
  19. data/app/views/compute_resources_vms/form/tofu/_base.html.erb +12 -6
  20. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +17 -7
  21. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +1 -5
  22. data/app/views/compute_resources_vms/form/tofu/_volume.html.erb +2 -0
  23. data/app/views/foreman_opentofu/compute_resources_vms/_indexed_networks_fields.html.erb +47 -0
  24. data/app/views/foreman_opentofu/compute_resources_vms/_indexed_volumes_fields.html.erb +30 -0
  25. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_interfaces_fields.html.erb +12 -0
  26. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_volumes_fields.html.erb +11 -0
  27. data/app/views/images/form/_tofu.html.erb +4 -0
  28. data/app/views/templates/provisioning/hetzner_provision_host.erb +64 -0
  29. data/app/views/templates/provisioning/nutanix_provision_default.erb +31 -22
  30. data/app/views/templates/provisioning/ovirt_provision_default.erb +7 -29
  31. data/lib/foreman_opentofu/provider_types/hetzner.rb +98 -0
  32. data/lib/foreman_opentofu/provider_types/nutanix.rb +74 -0
  33. data/lib/foreman_opentofu/provider_types/ovirt.rb +19 -0
  34. data/lib/foreman_opentofu/version.rb +1 -1
  35. data/lib/foreman_opentofu.rb +4 -0
  36. data/selinux/Makefile +22 -0
  37. data/selinux/foreman_opentofu.fc +3 -0
  38. data/selinux/foreman_opentofu.if +0 -0
  39. data/selinux/foreman_opentofu.te +93 -0
  40. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/backend_block.txt +6 -0
  41. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block.txt +8 -0
  42. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block_with_token.txt +14 -0
  43. data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/vm_attributes.txt +3 -0
  44. data/test/fixtures/snapshots/foreman_opentofu/hcl_format_test/to_hcl.txt +19 -0
  45. data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes.txt +8 -0
  46. data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes_block.txt +6 -0
  47. data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +137 -0
  48. data/test/lib/foreman_opentofu/concerns/nic_helpers_test.rb +36 -0
  49. data/test/lib/foreman_opentofu/hcl_format_test.rb +72 -0
  50. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +41 -14
  51. data/test/models/foreman_opentofu/tofu_test.rb +22 -0
  52. data/test/services/app_wrapper_test.rb +51 -1
  53. data/test/services/foreman_opentofu/provider_type_test.rb +115 -10
  54. data/test/services/opentofu_executer_test.rb +60 -19
  55. data/test/test_plugin_helper.rb +6 -0
  56. metadata +40 -6
  57. data/app/services/foreman_opentofu/compute_fetcher.rb +0 -51
  58. data/config/initializers/compute_attrs.rb +0 -19
  59. data/config/nutanix.json +0 -27
  60. 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: e3f7d22aa8036489bd3356eff61fb54608d15c1ced729e2a592257e3383213a5
4
+ data.tar.gz: 3a7272bc1b5e4a36974b859362d18209d3cbc36293e00c9722a2c6991bdcd0ee
5
5
  SHA512:
6
- metadata.gz: 67e87113d04444061daa977fca5721d26122035fd821f86284667eb5ad247c7a2002d5bd1d451ec0d4cea84ade9f530eb1dcffa3afb6b931b7a0b78f1f57c5ca
7
- data.tar.gz: d2627a46149453ae41fa6f5463bd792501ee51bc0760693a6bafdd3f0da7357ab9e8b8093c98149b5c4b4c7b2e8e03698c8d97996bb1ec3d1ac0990bc719a455
6
+ metadata.gz: 7efbc5b6168046cac8b716864636ac9f16acd32b229e017ff876d93cde3aecf3ff4e94572e09407ecf2e3de6462592efd46e4bcacf49285c765aceba4e32787b
7
+ data.tar.gz: f7b2493f8dcca6913ab2167f9cb529610d0e9c91240d43ea324503b24352fa5b0cb774b8ffee4f1cf76791e38ef9440c90ad0bfdbe8390b3b62fa1d4d09d38f5
data/README.md CHANGED
@@ -1,20 +1,50 @@
1
1
  [![Ruby Tests](https://github.com/ATIX-AG/foreman_opentofu/actions/workflows/ruby.yml/badge.svg)](https://github.com/ATIX-AG/foreman_opentofu/actions/workflows/ruby.yml)
2
2
 
3
- # ForemanOpenTOFU
3
+ # Foreman OpenTofu
4
4
 
5
- [Foreman](http://theforeman.org/) plugin that adds that adds a generic openTOFU-based compute resource, enabling host provisioning through openTOFU scripts instead of provider-specific SDK integrations such as fog-vsphere.
5
+ [Foreman](http://theforeman.org/) plugin that adds that adds a generic OpenTofu-based compute resource, enabling host provisioning through OpenTofu scripts instead of provider-specific SDK integrations such as fog-vsphere.
6
6
 
7
- This plugin introduces a new provisioning model where Foreman remains responsible for host lifecycle and orchestration, while openTOFU handles infrastructure creation using its provider ecosystem.
7
+ This plugin introduces a new provisioning model where Foreman remains responsible for host lifecycle and orchestration, while OpenTofu handles infrastructure creation using its provider ecosystem.
8
8
 
9
9
  The plugin is designed to be easily extendable and can support multiple infrastructure platforms (for example Nutanix, Hetzner) without requiring a dedicated Foreman compute resource plugin per provider.
10
10
 
11
11
  ## Installation
12
12
 
13
+ Install the rubygem as usual or use the RPM/Deb package for your distribution.
14
+
15
+ If you use the rubygem, make sure to create the folllowing directory and set the correct permissions:
16
+
17
+ ```shell
18
+
19
+ mkdir -p /var/lib/foreman-opentofu
20
+ mkdir -p /var/lib/foreman-opentofu/plugin-cache
21
+ mkdir -p /var/lib/foreman-opentofu/tmp
22
+
23
+ chown -R foreman:foreman /var/lib/foreman-opentofu
24
+ chmod 755 /var/lib/foreman-opentofu
25
+ chmod 755 /var/lib/foreman-opentofu/plugin-cache
26
+ chmod 700 /var/lib/foreman-opentofu/tmp
27
+ ```
28
+
29
+ ## SELinux
30
+
31
+ If you have installed the rubygem manually, you need to set the correct SELinux context for the plugin to work properly.
32
+ Re-use the selinux directives defined in the selinux directory of the plugin.
33
+
34
+ ```shell
35
+ cd selinux
36
+ make clean && make all
37
+ mkdir -p /usr/share/selinux/targeted/
38
+ install -m 0600 foreman_opentofu.pp /usr/share/selinux/targeted/
39
+ /usr/sbin/semodule -i /usr/share/selinux/targeted/foreman_opentofu.pp
40
+ /sbin/restorecon -ri /var/lib/foreman-opentofu/
41
+ ```
42
+
13
43
 
14
44
  ## Usage
15
- Create a openTofu compute resource and set:
16
- * Provider: openTofu
17
- * Opentofu Provider: Select desired hypervisor supported by openTofu plugin
45
+ Create a OpenTofu compute resource and set:
46
+ * Provider: OpenTofu
47
+ * OpenTofu Provider: Select desired hypervisor supported by OpenTofu plugin
18
48
  * URL: Hypervisor specific URL
19
49
 
20
50
 
@@ -22,17 +52,17 @@ Then add all necessary information to the form.
22
52
 
23
53
  Provisioning workflow:
24
54
 
25
- * Create a host in Foreman using the openTOFU based compute resource
55
+ * Create a host in Foreman using the OpenTofu based compute resource
26
56
 
27
57
  * Foreman passes host parameters to the plugin
28
58
 
29
- * The plugin renders and executes openTOFU plans
59
+ * The plugin renders and executes OpenTofu plans
30
60
 
31
- * openTOFU provisions the infrastructure
61
+ * OpenTofu provisions the infrastructure
32
62
 
33
63
  * Foreman continues with OS provisioning and configuration
34
64
 
35
- Provider-specific details (for example Nutanix, Hetzner) are handled entirely through openTOFU scripts.
65
+ Provider-specific details (for example Nutanix, Hetzner) are handled entirely through OpenTofu scripts.
36
66
 
37
67
  ### Create new ProviderType
38
68
 
@@ -90,6 +120,73 @@ A short config file might look like this:
90
120
 
91
121
  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
122
 
123
+ ##### Dynamic Config Parameter
124
+
125
+ Sometimes it is necessary to provide a list of possible values that are defined by the backend-service.
126
+ Curating the 'options'-Array is tedious at best or not possible if multiple instances of the backend service are in use.
127
+ This can be addressed by specifiying an OpenTofu provider's [DataSource](https://opentofu.org/docs/language/data-sources/) in the following way:
128
+
129
+ ```json
130
+ {
131
+ "name": "volume_group", "type": "select", "group": "disk", "mandatory": true, "label": "Volume Group",
132
+ "options": {
133
+ "data_source": {
134
+ "name": "nutanix_volume_groups_v2",
135
+ "arguments": {
136
+ "filter": "name eq 'volume_group_test'",
137
+ "limit": 20
138
+ },
139
+ "entity": {
140
+ "id": "metadata.uuid"
141
+ }
142
+ },
143
+ "output_path_postfix": "volume_groups"
144
+ }
145
+ }
146
+
147
+ ```
148
+ The GUI requires a list of objects that at least contain a name and an id for each select-option.
149
+ The `entity` section can be used to define a specific value from an object within the list that the DataSource returns.
150
+ If the object already has `name` and `id` entries, these will automatically used.
151
+ In the above example `name` exists in the object and can be used.
152
+ For the `id` however, a different value must be selected from the object.
153
+
154
+ This requests the data via OpenTofu in the following construct:
155
+
156
+ ```hcl
157
+ data "nutanix_volume_groups_v2" "all" {
158
+ filter = "name eq 'volume_group_test'"
159
+ limit = 20
160
+ }
161
+ output "resources" {
162
+ value = [ for e in data.nutanix_volume_groups_v2.all.volume_groups: {
163
+ id = e.metadata.uuid
164
+ name = e.name
165
+ } ]
166
+ }
167
+ ```
168
+
169
+ ##### Special Parameter
170
+
171
+ Some config parameter names have special meanings.
172
+ For instance, Image-based Deployment requires binding images available on the backend-service with Operating Systems configured in Foreman.
173
+ To enable Foreman OpenTofu to display the available images, a `select`-parameter with the name `available_images` must be specified.
174
+ It is recommended to tie this to a data-source available in the OpenTofu provider.
175
+
176
+ ```json
177
+ {
178
+ "name": "available_images", "type": "select",
179
+ "options": {
180
+ "data_source": {
181
+ "name": "hcloud_images",
182
+ "arguments": { "with_architecture": ["x86"] }
183
+ },
184
+ "output_path_postfix": "images"
185
+ }
186
+ }
187
+ ```
188
+
189
+
93
190
  #### Create Provider Type
94
191
 
95
192
  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/`.
@@ -125,7 +222,7 @@ end
125
222
 
126
223
  > See [Foreman dev setup](https://github.com/theforeman/foreman/blob/develop/developer_docs/foreman_dev_setup.asciidoc)
127
224
 
128
- * You need a openTOFU installed on your machine.
225
+ * You need a OpenTofu installed on your machine.
129
226
  * You need ruby 2.7. You can install it with [asdf-vm](https://asdf-vm.com).
130
227
 
131
228
  ### Platform
@@ -159,7 +256,7 @@ bundle install
159
256
  RAILS_ENV=development bundle exec bin/rake permissions:reset password=changeme
160
257
  ```
161
258
 
162
- * In foreman_openTofu source directory, check code syntax with rubocop and foreman rules:
259
+ * In the `foreman_opentofu` directory, check code syntax with rubocop and foreman rules:
163
260
 
164
261
  ```shell
165
262
  bundle exec rubocop
@@ -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,57 @@ 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"
77
+ data[key] = format_value(value, conf['type'])
32
78
  end
33
- res << nic_attributes(available_attributes)
79
+ res << to_hcl(data, snippet: true)
34
80
  end
35
81
 
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)
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)
91
+ else
92
+ ''
49
93
  end
50
- res
51
94
  end
52
95
 
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
60
- else
61
- Array(interfaces)
62
- end
96
+ def build_disks
97
+ disks = @cr_attrs['volumes'].presence || @cr_attrs['volumes_attributes'].presence || @compute_resource.default_volumes
98
+ disks = [disks] if disks.is_a?(Hash)
99
+ disks.each_with_index.map do |disk, index|
100
+ data = @compute_resource.render_disk(disk, self, index)
101
+ render_provider_data(data)
102
+ end.join("\n")
63
103
  end
64
104
 
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
105
+ def build_nics
106
+ nics = @cr_attrs['interfaces'].presence || @cr_attrs['interfaces_attributes'].presence || @compute_resource.default_interfaces
107
+ nics = normalize_interfaces(nics).map do |nic|
108
+ nic.respond_to?(:with_indifferent_access) ? nic.with_indifferent_access[:compute_attributes].presence || nic : nic
72
109
  end
73
- res << "}\n"
74
- res
110
+
111
+ nics.each_with_index.map do |nic, index|
112
+ data = @compute_resource.render_nic(nic, self, index)
113
+ render_provider_data(data)
114
+ end.join("\n")
75
115
  end
76
116
 
77
117
  private
78
118
 
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
119
+ def render_provider_data(data)
120
+ if data.is_a?(Hash) && data[:resource].present?
121
+ resource = data[:resource]
122
+ block_to_hcl(['resource', resource[:type], resource[:name]], resource[:content], depth: 0)
123
+ elsif data.is_a?(Hash) && data.size == 1 && data.values.first.is_a?(Hash)
124
+ block_name, block_content = data.first
125
+ block_to_hcl([block_name.to_s], block_content, depth: 1)
126
+ else
127
+ to_hcl(data, snippet: true)
85
128
  end
86
129
  end
87
130
  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
@@ -0,0 +1,22 @@
1
+ module ForemanOpentofu
2
+ module VMCommandCollectionNormalization
3
+ private
4
+
5
+ def normalize_vm_args_collections!(args)
6
+ [:volumes, :interfaces].each do |collection|
7
+ raw = args.delete(:"#{collection}_attributes") || args[collection]
8
+ next if raw.nil?
9
+
10
+ args[collection] = normalize_collection_input(collection, raw)
11
+ end
12
+ end
13
+
14
+ def normalize_collection_input(collection, value)
15
+ return nested_attributes_for(collection, value) if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
16
+
17
+ Array(value).map do |entry|
18
+ entry.respond_to?(:deep_symbolize_keys) ? entry.deep_symbolize_keys : entry
19
+ end
20
+ end
21
+ end
22
+ 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!
@@ -1,34 +1,39 @@
1
1
  module ForemanOpentofu
2
2
  module OpentofuVMCommands
3
+ include ForemanOpentofu::VMCommandCollectionNormalization
4
+
3
5
  def find_vm_by_uuid(uuid)
4
6
  vm_command_errors('find vm') do
5
7
  tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
6
- data = client({ 'name' => tf_state&.name }).run('output')
8
+ data = client({ 'name' => tf_state&.name }).run_output
7
9
  ComputeVM.new(self, data)
8
10
  end
9
11
  end
10
12
 
11
13
  def new_vm(args = {})
12
14
  vm_command_errors('new vm') do
13
- args = default_attributes.merge(args)
15
+ args = default_attributes.merge(args).to_h.symbolize_keys
16
+ normalize_vm_args_collections!(args)
14
17
  executor = client(args)
15
- data = executor.run('new')
16
- OpenStruct.new(data['resource_changes'].first['change']['after'])
18
+ data = executor.run_new
19
+ attrs = data['resource_changes'].first['change']['after'] || {}
20
+ OpenStruct.new(attrs)
17
21
  end
18
22
  end
19
23
 
20
24
  def create_vm(args = {})
21
25
  vm_command_errors('create vm') do
22
- args = default_attributes.merge(args)
26
+ args = default_attributes.merge(args).to_h.symbolize_keys
27
+ normalize_vm_args_collections!(args)
23
28
  executor = client(args)
24
- output = executor.run('create')
29
+ output = executor.run_create
25
30
  ComputeVM.new(self, output)
26
31
  end
27
32
  end
28
33
 
29
34
  def destroy_vm(uuid)
30
35
  tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
31
- client({ 'name' => tf_state&.name }).run('destroy')
36
+ client({ 'name' => tf_state&.name }).run_destroy
32
37
  return unless tf_state
33
38
 
34
39
  Rails.logger.info "Deleting tfstate for #{tf_state&.name}"
@@ -36,12 +41,12 @@ module ForemanOpentofu
36
41
  end
37
42
 
38
43
  def start_vm(name)
39
- output = client({ 'name' => name, 'power_state' => 'on' }).run('create')
44
+ output = client({ 'name' => name, 'power_state' => 'on' }).run_create
40
45
  output['vm']['power_state'] == 'on'
41
46
  end
42
47
 
43
48
  def stop_vm(name)
44
- output = client({ 'name' => name, 'power_state' => 'off' }).run('create')
49
+ output = client({ 'name' => name, 'power_state' => 'off' }).run_create
45
50
  output['vm']['power_state'] == 'off'
46
51
  end
47
52
 
@@ -50,15 +55,21 @@ module ForemanOpentofu
50
55
  raise StandardError, "VM with UUID #{uuid} does not exist" unless tf_state
51
56
  vm_command_errors('update vm') do
52
57
  attrs = attrs.empty? ? {} : attrs.first
53
- data = client({ 'name' => tf_state.name }.merge(attrs)).run('create')
58
+ attrs = attrs.to_h.symbolize_keys
59
+ normalize_vm_args_collections!(attrs)
60
+ data = client({ 'name' => tf_state.name }.merge(attrs)).run_create
54
61
  ComputeVM.new(self, data)
55
62
  end
56
63
  end
57
64
 
65
+ def fetch_resource(resource_name = '', options = {})
66
+ client({ 'resource' => { name: resource_name, options: options } }).run_fetch
67
+ end
68
+
58
69
  def test_connection(options = {})
59
70
  super
60
71
  begin
61
- client.run('test_connection')
72
+ client.run_test_connection
62
73
  rescue StandardError => e
63
74
  Rails.logger.error("OpenTofu test connection failed: #{e.message}")
64
75
  errors.add(:base, e.message)