foreman_opentofu 0.0.1 → 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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +152 -0
  3. data/app/assets/javascripts/foreman_opentofu/locale/en/foreman_opentofu.js +58 -0
  4. data/app/controllers/api/v2/tf_states_controller.rb +43 -5
  5. data/app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb +1 -19
  6. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +61 -53
  7. data/app/lib/foreman_opentofu/hcl_format.rb +95 -0
  8. data/app/lib/foreman_opentofu/nic_helpers.rb +47 -0
  9. data/app/models/concerns/orchestration/tofu/compute.rb +25 -6
  10. data/app/models/foreman_opentofu/compute_vm.rb +10 -0
  11. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +12 -8
  12. data/app/models/foreman_opentofu/tofu.rb +48 -9
  13. data/app/models/foreman_opentofu/token.rb +16 -0
  14. data/app/services/foreman_opentofu/app_wrapper.rb +37 -14
  15. data/app/services/foreman_opentofu/opentofu_executer.rb +90 -37
  16. data/app/services/foreman_opentofu/provider_type.rb +52 -8
  17. data/app/views/compute_resources/form/_tofu.html.erb +5 -1
  18. data/app/views/compute_resources/show/_tofu.html.erb +8 -0
  19. data/app/views/compute_resources/tofu.json.rabl +1 -0
  20. data/app/views/compute_resources_vms/form/tofu/_base.html.erb +13 -2
  21. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +17 -7
  22. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +1 -5
  23. data/app/views/images/form/_tofu.html.erb +4 -0
  24. data/app/views/templates/provisioning/hetzner_provision_host.erb +62 -0
  25. data/app/views/templates/provisioning/nutanix_provision_default.erb +56 -0
  26. data/app/views/templates/provisioning/ovirt_provision_default.erb +43 -0
  27. data/db/migrate/20260127160904_add_table_token.foreman_opentofu.rb +10 -0
  28. data/db/seeds.d/71_provisioning_templates.rb +9 -1
  29. data/lib/foreman_opentofu/engine.rb +8 -24
  30. data/lib/foreman_opentofu/provider_types/hetzner.rb +66 -0
  31. data/lib/foreman_opentofu/provider_types/nutanix.rb +48 -0
  32. data/lib/foreman_opentofu/provider_types/ovirt.rb +19 -0
  33. data/lib/foreman_opentofu/version.rb +1 -1
  34. data/lib/tasks/foreman_opentofu_tasks.rake +2 -2
  35. data/locale/en/LC_MESSAGES/foreman_opentofu.mo +0 -0
  36. data/locale/en/foreman_opentofu.po +44 -9
  37. data/locale/foreman_opentofu.pot +56 -8
  38. data/test/controllers/api/v2/tf_states_controller_test.rb +33 -8
  39. data/test/factories/token.rb +10 -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 +110 -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 +14 -14
  51. data/test/models/foreman_opentofu/tofu_test.rb +68 -0
  52. data/test/models/foreman_opentofu/token_test.rb +36 -0
  53. data/test/services/app_wrapper_test.rb +39 -1
  54. data/test/services/foreman_opentofu/provider_type_test.rb +160 -0
  55. data/test/services/opentofu_executer_test.rb +28 -10
  56. data/test/test_plugin_helper.rb +6 -0
  57. metadata +43 -9
  58. data/app/services/foreman_opentofu/compute_fetcher.rb +0 -51
  59. data/app/views/templates/provisioning/nutanix_provision_host.erb +0 -51
  60. data/app/views/templates/provisioning/ovirt_provision_host.erb +0 -68
  61. data/config/initializers/compute_attrs.rb +0 -19
  62. data/config/nutanix.json +0 -27
  63. data/config/ovirt.json +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1a821d0e6f43b76c17c48f1a84cb7bc753f9bbebd6c31c532fea9fcf6cf843b
4
- data.tar.gz: 515d5f582f9911cbdaf339b9854baf644437171695f4a7062ac44eb2eb203105
3
+ metadata.gz: ccfa3742aa9fe09edc20f685bf1a65f0f32aa34603572812fd250f64f84c7f60
4
+ data.tar.gz: 632627c1dbecb3da3282fa29085647263f588eb99753db33d2f2ada53b57680c
5
5
  SHA512:
6
- metadata.gz: 3d0b019416cc34fd4f9c17ca83c08ef7f5861ea88d9d50905508a878be1e05ad2c7a998dacd8a58209eb494845923843015820c1acb20227e6f59b40b15d92c6
7
- data.tar.gz: 6286eb806d3b5afca40e28e6d064512b5477b698eb531ab1b4063d18d66ae4b5644536372511c40d7cccac8e0d26ff534625c3dd242c6d19534a949796ed50c6
6
+ metadata.gz: 1858acdfd4b8e7e32dd3c91765ca1e5e6921da70b8754842e1fa8284bb9b1ceb23b5abb7b22320218d6326167f821d032071915458155545e19ace88dfadd996
7
+ data.tar.gz: a9f29bd6e263acb43c2a692ed848a71b99c61dce3fb3b64a9ebd7597bceae0280f8b93879b2e5b89a6a8a2244f9cfd5644fdfdc20444353fc962211def091591
data/README.md CHANGED
@@ -34,6 +34,158 @@ Provisioning workflow:
34
34
 
35
35
  Provider-specific details (for example Nutanix, Hetzner) are handled entirely through openTOFU scripts.
36
36
 
37
+ ### Create new ProviderType
38
+
39
+ This Plugin empowers you to add support of a new backend VM- or Cloud-Platform yourself.
40
+ Follow these simple steps to do so:
41
+
42
+ #### Find OpenTofu Provider
43
+
44
+ Visit the OpenTofu Registry to find a suitable [provider supported by OpenTofu](https://search.opentofu.org/providers).
45
+ The Registry supplies the necessary Data Sources to read information from the Backend as well as Resources to create/update/destroy resources on the Backend.
46
+
47
+ #### Create Template
48
+
49
+ You may use the UI-Editor in Hosts -> Templates -> Provisioning Templates to create a new Template.
50
+ Either clone a pre-installed template or create one from scratch.
51
+ In the latter case be sure to select the correct Template Type: OpenTofu Script template.
52
+
53
+ #### Create Parameter Config
54
+
55
+ To define which Virtual Machine parameters can be set for a new Host a new config file under `/config` must be added.
56
+ Feel free to use either YAML or JSON (be sure to end the filename with `.json` or `.yaml`).
57
+ The config file defines an array of dicts, where each dict represents a configuration-parameter.
58
+
59
+ A config-parameter has the following values:
60
+
61
+ * `name`: the OpenTofu Provider Resource Arguments as stated on the OpenTofu Registry
62
+ * `label`: the label shown in the Foreman UI
63
+ * `type`: data-type of the value, supported values:
64
+ * `string`
65
+ * `number`
66
+ * `bool`
67
+ * `select`: requires setting `options`
68
+ * `help`: Tooltip describing what that value does and what values are allowed
69
+ * `mandatory`: `true`/`false` defines if omitting the value triggers an error
70
+ * `options`: array of strings representing the possible values
71
+ * `group`: define where the value should be configured
72
+ * `vm`: ones per Host in the 'Virtual Machine' tab,
73
+ * `disk`: for each defined disk/volume in the 'Virtual Machine' tab
74
+ * `nic`: for each defined network-interface on the 'Interfaces' tab
75
+
76
+ A short config file might look like this:
77
+
78
+ ```json
79
+ [
80
+ { "name": "memory_size_mib", "type": "number", "group": "vm", "mandatory": false,
81
+ "label": "Memory (MB)" },
82
+ { "name": "boot_type", "type": "select", "group": "vm", "mandatory": false,
83
+ "label": "Firmware", "options": [ "UEFI", "LEGACY", "SECURE_BOOT" ] },
84
+ { "name": "disk_size_mib", "type": "number", "group": "disk", "mandatory": true,
85
+ "label": "Size (MB)" },
86
+ { "name": "model", "type": "select", "group": "nic", "mandatory": true,
87
+ "options": [ "VIRTIO", "E1000" ] }
88
+ ]
89
+ ```
90
+
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
+
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
+
160
+ #### Create Provider Type
161
+
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/`.
163
+
164
+ A very simple ProviderType file to add a new Provider named `nutanix` has to be located in `lib/foreman_opentofu/provider_types/nutanix.rb` and might look like this:
165
+
166
+ ```ruby
167
+ ForemanOpentofu::ProviderTypeManager.register('nutanix') do
168
+ end
169
+ ```
170
+
171
+ Additional informations about the ProviderType can be set within the `register`-block:
172
+
173
+ ##### `default_attributes`
174
+
175
+ Define values that should be set as default for attributes.
176
+ The values do not have to be defined in the config-file.
177
+ If attributes are also defined in the config-file and therefore set during Host creation, the default\_attribute values will be overwritten.
178
+
179
+ ```ruby
180
+ ForemanOpentofu::ProviderTypeManager.register('nutanix') do
181
+ @default_attributes = {
182
+ 'enable_cpu_passthrough' => true,
183
+ 'num_threads_per_core' => 2,
184
+ }
185
+ end
186
+ ```
187
+
188
+
37
189
  ## Development
38
190
 
39
191
  ### Dev prerequisites
@@ -0,0 +1,58 @@
1
+ locales['foreman_opentofu'] = locales['foreman_opentofu'] || {}; locales['foreman_opentofu']['en'] = {
2
+ "domain": "foreman_opentofu",
3
+ "locale_data": {
4
+ "foreman_opentofu": {
5
+ "": {
6
+ "Project-Id-Version": "foreman_opentofu 1.0.0",
7
+ "Report-Msgid-Bugs-To": "",
8
+ "PO-Revision-Date": "2026-02-16 18:46+0000",
9
+ "Last-Translator": "FULL NAME <EMAIL@ADDRESS>",
10
+ "Language-Team": "LANGUAGE <LL@li.org>",
11
+ "Language": "",
12
+ "MIME-Version": "1.0",
13
+ "Content-Type": "text/plain; charset=UTF-8",
14
+ "Content-Transfer-Encoding": "8bit",
15
+ "Plural-Forms": "nplurals=INTEGER; plural=EXPRESSION;",
16
+ "lang": "en",
17
+ "domain": "foreman_opentofu",
18
+ "plural_forms": "nplurals=INTEGER; plural=EXPRESSION;"
19
+ },
20
+ "127.0.0.1": [
21
+ ""
22
+ ],
23
+ "Common fields": [
24
+ ""
25
+ ],
26
+ "OpenTofu Provider": [
27
+ ""
28
+ ],
29
+ "OpenTofu Script template": [
30
+ ""
31
+ ],
32
+ "OpenTofu Template": [
33
+ ""
34
+ ],
35
+ "Storage": [
36
+ ""
37
+ ],
38
+ "TODO: Description of ForemanPluginTemplate.": [
39
+ ""
40
+ ],
41
+ "URL": [
42
+ ""
43
+ ],
44
+ "Unable to find template specified by %s setting": [
45
+ ""
46
+ ],
47
+ "Unable to render provisioning template": [
48
+ ""
49
+ ],
50
+ "VM Config": [
51
+ ""
52
+ ],
53
+ "e.g. admin": [
54
+ ""
55
+ ]
56
+ }
57
+ }
58
+ };
@@ -3,14 +3,25 @@ module Api
3
3
  class TfStatesController < ::Api::V2::BaseController
4
4
  include ::Api::Version2
5
5
 
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
+ skip_before_action :set_taxonomy
10
+
11
+ # Allow HTTP POST methods without CSRF
12
+ skip_before_action :verify_authenticity_token
13
+
14
+ # overwrite authorize with the local token-based authorization
15
+ before_action :authorize, except: [:destroy]
16
+
6
17
  resource_description do
7
18
  api_version 'v2'
8
19
  api_base_url '/foreman_opentofu/api'
9
20
  end
10
21
 
11
- skip_before_action :verify_authenticity_token
12
22
  def show
13
23
  state = ForemanOpentofu::TfState.find_by(name: params[:name])
24
+
14
25
  if state
15
26
  render plain: state.state, content_type: 'application/json'
16
27
  else
@@ -19,8 +30,6 @@ module Api
19
30
  end
20
31
 
21
32
  def create
22
- state = ForemanOpentofu::TfState.find_or_create_by(name: params[:name])
23
-
24
33
  raw_state = request.body.read
25
34
  if raw_state.blank?
26
35
  render plain: 'Missing state body', status: :unprocessable_entity
@@ -29,6 +38,7 @@ module Api
29
38
  begin
30
39
  JSON.parse(raw_state)
31
40
 
41
+ state = ForemanOpentofu::TfState.find_or_initialize_by(name: params[:name])
32
42
  state.state = raw_state
33
43
  state.save!
34
44
  render plain: '', status: :ok
@@ -39,14 +49,42 @@ module Api
39
49
  end
40
50
 
41
51
  def destroy
42
- state = ForemanOpentofu::TfState.find_by(name: params[:name])
43
- state&.destroy
52
+ # TODO: at the moment we want to get 200 OK, if the TfState does not exist.
53
+ # normally, this would fail with 401, because of no valid token.
54
+ # Needs re-evaluation, if this is a security risk.
55
+ state = ForemanOpentofu::TfState.where(name: params[:name])
56
+ if state.any?
57
+ authorize
58
+ state.first.destroy
59
+ end
60
+
44
61
  render plain: '', status: :ok
45
62
  end
46
63
 
47
64
  def resource_class
48
65
  @resource_class ||= ForemanOpentofu::TfState
49
66
  end
67
+
68
+ private
69
+
70
+ def authorize
71
+ authenticate_or_request_with_http_token do |token, _options|
72
+ token = ForemanOpentofu::Token.find_by(name: params[:name], token: token)
73
+ unless token
74
+ Rails.logger.warn('TfState-Auth-Token not found')
75
+ render_error('unauthorized', status: :unauthorized)
76
+ return false
77
+ end
78
+
79
+ if token.expired?
80
+ Rails.logger.warn 'TfState token expired, if this keeps happening increase the validity of the token'
81
+ render_error('unauthorized', status: :unauthorized)
82
+ return false
83
+ end
84
+
85
+ return true
86
+ end
87
+ end
50
88
  end
51
89
  end
52
90
  end
@@ -1,22 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright 2018 Tristan Robert
4
-
5
- # This file is part of ForemanFogProxmox.
6
-
7
- # ForemanFogProxmox is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
-
12
- # ForemanFogProxmox is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
-
17
- # You should have received a copy of the GNU General Public License
18
- # along with ForemanFogProxmox. If not, see <http://www.gnu.org/licenses/>.
19
-
20
1
  module ForemanOpentofu
21
2
  module Controller
22
3
  module Parameters
@@ -28,6 +9,7 @@ module ForemanOpentofu
28
9
  super.tap do |filter|
29
10
  filter.permit :endpoint,
30
11
  :opentofu_provider,
12
+ :opentofu_template_id,
31
13
  :username
32
14
  end
33
15
  end
@@ -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