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.
- checksums.yaml +4 -4
- data/README.md +109 -12
- data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +87 -44
- data/app/lib/foreman_opentofu/hcl_format.rb +95 -0
- data/app/lib/foreman_opentofu/nic_helpers.rb +47 -0
- data/app/models/concerns/foreman_opentofu/vm_command_collection_normalization.rb +22 -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 +22 -11
- data/app/models/foreman_opentofu/tofu.rb +43 -10
- data/app/overrides/compute_resources_vms/tofu_indexed_networks_fields.rb +7 -0
- data/app/overrides/compute_resources_vms/tofu_indexed_volumes_fields.rb +8 -0
- data/app/services/foreman_opentofu/app_wrapper.rb +51 -14
- data/app/services/foreman_opentofu/opentofu_executer.rb +67 -29
- data/app/services/foreman_opentofu/provider_type.rb +61 -7
- data/app/views/compute_resources/form/_tofu.html.erb +3 -0
- data/app/views/compute_resources/show/_tofu.html.erb +4 -0
- data/app/views/compute_resources/tofu.json.rabl +1 -0
- data/app/views/compute_resources_vms/form/tofu/_base.html.erb +12 -6
- 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/compute_resources_vms/form/tofu/_volume.html.erb +2 -0
- data/app/views/foreman_opentofu/compute_resources_vms/_indexed_networks_fields.html.erb +47 -0
- data/app/views/foreman_opentofu/compute_resources_vms/_indexed_volumes_fields.html.erb +30 -0
- data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_interfaces_fields.html.erb +12 -0
- data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_volumes_fields.html.erb +11 -0
- data/app/views/images/form/_tofu.html.erb +4 -0
- data/app/views/templates/provisioning/hetzner_provision_host.erb +64 -0
- data/app/views/templates/provisioning/nutanix_provision_default.erb +31 -22
- data/app/views/templates/provisioning/ovirt_provision_default.erb +7 -29
- data/lib/foreman_opentofu/provider_types/hetzner.rb +98 -0
- data/lib/foreman_opentofu/provider_types/nutanix.rb +74 -0
- data/lib/foreman_opentofu/provider_types/ovirt.rb +19 -0
- data/lib/foreman_opentofu/version.rb +1 -1
- data/lib/foreman_opentofu.rb +4 -0
- data/selinux/Makefile +22 -0
- data/selinux/foreman_opentofu.fc +3 -0
- data/selinux/foreman_opentofu.if +0 -0
- data/selinux/foreman_opentofu.te +93 -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 +137 -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 +41 -14
- data/test/models/foreman_opentofu/tofu_test.rb +22 -0
- data/test/services/app_wrapper_test.rb +51 -1
- data/test/services/foreman_opentofu/provider_type_test.rb +115 -10
- data/test/services/opentofu_executer_test.rb +60 -19
- data/test/test_plugin_helper.rb +6 -0
- metadata +40 -6
- data/app/services/foreman_opentofu/compute_fetcher.rb +0 -51
- 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: e3f7d22aa8036489bd3356eff61fb54608d15c1ced729e2a592257e3383213a5
|
|
4
|
+
data.tar.gz: 3a7272bc1b5e4a36974b859362d18209d3cbc36293e00c9722a2c6991bdcd0ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7efbc5b6168046cac8b716864636ac9f16acd32b229e017ff876d93cde3aecf3ff4e94572e09407ecf2e3de6462592efd46e4bcacf49285c765aceba4e32787b
|
|
7
|
+
data.tar.gz: f7b2493f8dcca6913ab2167f9cb529610d0e9c91240d43ea324503b24352fa5b0cb774b8ffee4f1cf76791e38ef9440c90ad0bfdbe8390b3b62fa1d4d09d38f5
|
data/README.md
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
[](https://github.com/ATIX-AG/foreman_opentofu/actions/workflows/ruby.yml)
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Foreman OpenTofu
|
|
4
4
|
|
|
5
|
-
[Foreman](http://theforeman.org/) plugin that adds that adds a generic
|
|
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
|
|
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
|
|
16
|
-
* Provider:
|
|
17
|
-
*
|
|
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
|
|
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
|
|
59
|
+
* The plugin renders and executes OpenTofu plans
|
|
30
60
|
|
|
31
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
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,57 @@ module ForemanOpentofu
|
|
|
28
74
|
next if conf['group'] != 'vm'
|
|
29
75
|
next if value.blank? && !conf['mandatory']
|
|
30
76
|
|
|
31
|
-
|
|
77
|
+
data[key] = format_value(value, conf['type'])
|
|
32
78
|
end
|
|
33
|
-
res <<
|
|
79
|
+
res << to_hcl(data, snippet: true)
|
|
34
80
|
end
|
|
35
81
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 }).
|
|
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.
|
|
16
|
-
|
|
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.
|
|
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 }).
|
|
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' }).
|
|
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' }).
|
|
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
|
-
|
|
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.
|
|
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)
|