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.
- checksums.yaml +4 -4
- data/README.md +152 -0
- data/app/assets/javascripts/foreman_opentofu/locale/en/foreman_opentofu.js +58 -0
- data/app/controllers/api/v2/tf_states_controller.rb +43 -5
- data/app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb +1 -19
- data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +61 -53
- data/app/lib/foreman_opentofu/hcl_format.rb +95 -0
- data/app/lib/foreman_opentofu/nic_helpers.rb +47 -0
- data/app/models/concerns/orchestration/tofu/compute.rb +25 -6
- data/app/models/foreman_opentofu/compute_vm.rb +10 -0
- data/app/models/foreman_opentofu/opentofu_vm_commands.rb +12 -8
- data/app/models/foreman_opentofu/tofu.rb +48 -9
- data/app/models/foreman_opentofu/token.rb +16 -0
- data/app/services/foreman_opentofu/app_wrapper.rb +37 -14
- data/app/services/foreman_opentofu/opentofu_executer.rb +90 -37
- data/app/services/foreman_opentofu/provider_type.rb +52 -8
- data/app/views/compute_resources/form/_tofu.html.erb +5 -1
- data/app/views/compute_resources/show/_tofu.html.erb +8 -0
- data/app/views/compute_resources/tofu.json.rabl +1 -0
- data/app/views/compute_resources_vms/form/tofu/_base.html.erb +13 -2
- data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +17 -7
- data/app/views/compute_resources_vms/form/tofu/_network.html.erb +1 -5
- data/app/views/images/form/_tofu.html.erb +4 -0
- data/app/views/templates/provisioning/hetzner_provision_host.erb +62 -0
- data/app/views/templates/provisioning/nutanix_provision_default.erb +56 -0
- data/app/views/templates/provisioning/ovirt_provision_default.erb +43 -0
- data/db/migrate/20260127160904_add_table_token.foreman_opentofu.rb +10 -0
- data/db/seeds.d/71_provisioning_templates.rb +9 -1
- data/lib/foreman_opentofu/engine.rb +8 -24
- data/lib/foreman_opentofu/provider_types/hetzner.rb +66 -0
- data/lib/foreman_opentofu/provider_types/nutanix.rb +48 -0
- data/lib/foreman_opentofu/provider_types/ovirt.rb +19 -0
- data/lib/foreman_opentofu/version.rb +1 -1
- data/lib/tasks/foreman_opentofu_tasks.rake +2 -2
- data/locale/en/LC_MESSAGES/foreman_opentofu.mo +0 -0
- data/locale/en/foreman_opentofu.po +44 -9
- data/locale/foreman_opentofu.pot +56 -8
- data/test/controllers/api/v2/tf_states_controller_test.rb +33 -8
- data/test/factories/token.rb +10 -0
- data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/backend_block.txt +6 -0
- data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block.txt +8 -0
- data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/terraform_block_with_token.txt +14 -0
- data/test/fixtures/snapshots/foreman_opentofu/base_template_scope_extensions_test/vm_attributes.txt +3 -0
- data/test/fixtures/snapshots/foreman_opentofu/hcl_format_test/to_hcl.txt +19 -0
- data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes.txt +8 -0
- data/test/fixtures/snapshots/foreman_opentofu/nic_helpers_test/nic_attributes_block.txt +6 -0
- data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +110 -0
- data/test/lib/foreman_opentofu/concerns/nic_helpers_test.rb +36 -0
- data/test/lib/foreman_opentofu/hcl_format_test.rb +72 -0
- data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +14 -14
- data/test/models/foreman_opentofu/tofu_test.rb +68 -0
- data/test/models/foreman_opentofu/token_test.rb +36 -0
- data/test/services/app_wrapper_test.rb +39 -1
- data/test/services/foreman_opentofu/provider_type_test.rb +160 -0
- data/test/services/opentofu_executer_test.rb +28 -10
- data/test/test_plugin_helper.rb +6 -0
- metadata +43 -9
- data/app/services/foreman_opentofu/compute_fetcher.rb +0 -51
- data/app/views/templates/provisioning/nutanix_provision_host.erb +0 -51
- data/app/views/templates/provisioning/ovirt_provision_host.erb +0 -68
- data/config/initializers/compute_attrs.rb +0 -19
- data/config/nutanix.json +0 -27
- data/config/ovirt.json +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccfa3742aa9fe09edc20f685bf1a65f0f32aa34603572812fd250f64f84c7f60
|
|
4
|
+
data.tar.gz: 632627c1dbecb3da3282fa29085647263f588eb99753db33d2f2ada53b57680c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
43
|
-
|
|
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 '
|
|
9
|
-
sections only: %w[
|
|
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
|
-
|
|
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
|
|
54
|
-
if
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|