foreman_opentofu 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +67 -0
- 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 +33 -8
- data/app/services/foreman_opentofu/app_wrapper.rb +37 -14
- data/app/services/foreman_opentofu/opentofu_executer.rb +66 -28
- data/app/services/foreman_opentofu/provider_type.rb +51 -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 +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 +30 -22
- data/app/views/templates/provisioning/ovirt_provision_default.erb +7 -29
- 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/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 +22 -0
- data/test/services/app_wrapper_test.rb +39 -1
- data/test/services/foreman_opentofu/provider_type_test.rb +102 -10
- data/test/services/opentofu_executer_test.rb +27 -9
- data/test/test_plugin_helper.rb +6 -0
- metadata +28 -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: 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
|
@@ -90,6 +90,73 @@ A short config file might look like this:
|
|
|
90
90
|
|
|
91
91
|
The name of the file must be the same as the provider-type name we set in the next step (e.g. `/config/nutanix.json`).
|
|
92
92
|
|
|
93
|
+
##### Dynamic Config Parameter
|
|
94
|
+
|
|
95
|
+
Sometimes it is necessary to provide a list of possible values that are defined by the backend-service.
|
|
96
|
+
Curating the 'options'-Array is tedious at best or not possible if multiple instances of the backend service are in use.
|
|
97
|
+
This can be addressed by specifiying an OpenTofu-Provider's [DataSource](https://opentofu.org/docs/language/data-sources/) in the following way:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"name": "volume_group", "type": "select", "group": "disk", "mandatory": true, "label": "Volume Group",
|
|
102
|
+
"options": {
|
|
103
|
+
"data_source": {
|
|
104
|
+
"name": "nutanix_volume_groups_v2",
|
|
105
|
+
"arguments": {
|
|
106
|
+
"filter": "name eq 'volume_group_test'",
|
|
107
|
+
"limit": 20
|
|
108
|
+
},
|
|
109
|
+
"entity": {
|
|
110
|
+
"id": "metadata.uuid"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"output_path_postfix": "volume_groups"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
The GUI requires a list of objects that at least contain a name and an id for each select-option.
|
|
119
|
+
The `entity` section can be used to define a specific value from an object within the list that the DataSource returns.
|
|
120
|
+
If the object already has `name` and `id` entries, these will automatically used.
|
|
121
|
+
In the above example `name` exists in the object and can be used.
|
|
122
|
+
For the `id` however, a different value must be selected from the object.
|
|
123
|
+
|
|
124
|
+
This requests the data via OpenTofu in the following construct:
|
|
125
|
+
|
|
126
|
+
```hcl
|
|
127
|
+
data "nutanix_volume_groups_v2" "all" {
|
|
128
|
+
filter = "name eq 'volume_group_test'"
|
|
129
|
+
limit = 20
|
|
130
|
+
}
|
|
131
|
+
output "resources" {
|
|
132
|
+
value = [ for e in data.nutanix_volume_groups_v2.all.volume_groups: {
|
|
133
|
+
id = e.metadata.uuid
|
|
134
|
+
name = e.name
|
|
135
|
+
} ]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
##### Special Parameter
|
|
140
|
+
|
|
141
|
+
Some config parameter names have special meanings.
|
|
142
|
+
For instance, Image-based Deployment requires binding images available on the backend-service with Operating Systems configured in Foreman.
|
|
143
|
+
To enable Foreman OpenTofu to display the available images, a `select`-parameter with the name `available_images` must be specified.
|
|
144
|
+
It is recommended to tie this to a data-source available in the OpenTofu provider.
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"name": "available_images", "type": "select",
|
|
149
|
+
"options": {
|
|
150
|
+
"data_source": {
|
|
151
|
+
"name": "hcloud_images",
|
|
152
|
+
"arguments": { "with_architecture": ["x86"] }
|
|
153
|
+
},
|
|
154
|
+
"output_path_postfix": "images"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
|
|
93
160
|
#### Create Provider Type
|
|
94
161
|
|
|
95
162
|
To let the Foreman OpenTofu Plugin know about your new Provider Type, one additional file has to be created in `/lib/foreman_opentofu/provider_types/`.
|
|
@@ -3,19 +3,65 @@ module ForemanOpentofu
|
|
|
3
3
|
module BaseTemplateScopeExtensions
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
extend ApipieDSL::Module
|
|
6
|
+
include ForemanOpentofu::HclFormat
|
|
7
|
+
include ForemanOpentofu::NicHelpers
|
|
6
8
|
|
|
7
9
|
apipie :class, 'Base macros related to Opentofu templates' do
|
|
8
|
-
name '
|
|
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
|
|
@@ -47,6 +47,16 @@ module ForemanOpentofu
|
|
|
47
47
|
reboot
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def vm_ip_address
|
|
51
|
+
@attributes['vm_ip_address']
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def wait_for(&block)
|
|
55
|
+
# TODO: I guess we have nothing to wait for
|
|
56
|
+
# and we need to change the context of the given block
|
|
57
|
+
instance_eval(&block)
|
|
58
|
+
end
|
|
59
|
+
|
|
50
60
|
private
|
|
51
61
|
|
|
52
62
|
def define_dynamic_readers!
|
|
@@ -3,7 +3,7 @@ module ForemanOpentofu
|
|
|
3
3
|
def find_vm_by_uuid(uuid)
|
|
4
4
|
vm_command_errors('find vm') do
|
|
5
5
|
tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
|
|
6
|
-
data = client({ 'name' => tf_state&.name }).
|
|
6
|
+
data = client({ 'name' => tf_state&.name }).run_output
|
|
7
7
|
ComputeVM.new(self, data)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
@@ -12,7 +12,7 @@ module ForemanOpentofu
|
|
|
12
12
|
vm_command_errors('new vm') do
|
|
13
13
|
args = default_attributes.merge(args)
|
|
14
14
|
executor = client(args)
|
|
15
|
-
data = executor.
|
|
15
|
+
data = executor.run_new
|
|
16
16
|
OpenStruct.new(data['resource_changes'].first['change']['after'])
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -21,14 +21,14 @@ module ForemanOpentofu
|
|
|
21
21
|
vm_command_errors('create vm') do
|
|
22
22
|
args = default_attributes.merge(args)
|
|
23
23
|
executor = client(args)
|
|
24
|
-
output = executor.
|
|
24
|
+
output = executor.run_create
|
|
25
25
|
ComputeVM.new(self, output)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def destroy_vm(uuid)
|
|
30
30
|
tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
|
|
31
|
-
client({ 'name' => tf_state&.name }).
|
|
31
|
+
client({ 'name' => tf_state&.name }).run_destroy
|
|
32
32
|
return unless tf_state
|
|
33
33
|
|
|
34
34
|
Rails.logger.info "Deleting tfstate for #{tf_state&.name}"
|
|
@@ -36,12 +36,12 @@ module ForemanOpentofu
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def start_vm(name)
|
|
39
|
-
output = client({ 'name' => name, 'power_state' => 'on' }).
|
|
39
|
+
output = client({ 'name' => name, 'power_state' => 'on' }).run_create
|
|
40
40
|
output['vm']['power_state'] == 'on'
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def stop_vm(name)
|
|
44
|
-
output = client({ 'name' => name, 'power_state' => 'off' }).
|
|
44
|
+
output = client({ 'name' => name, 'power_state' => 'off' }).run_create
|
|
45
45
|
output['vm']['power_state'] == 'off'
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -50,15 +50,19 @@ module ForemanOpentofu
|
|
|
50
50
|
raise StandardError, "VM with UUID #{uuid} does not exist" unless tf_state
|
|
51
51
|
vm_command_errors('update vm') do
|
|
52
52
|
attrs = attrs.empty? ? {} : attrs.first
|
|
53
|
-
data = client({ 'name' => tf_state.name }.merge(attrs)).
|
|
53
|
+
data = client({ 'name' => tf_state.name }.merge(attrs)).run_create
|
|
54
54
|
ComputeVM.new(self, data)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def fetch_resource(resource_name = '', options = {})
|
|
59
|
+
client({ 'resource' => { name: resource_name, options: options } }).run_fetch
|
|
60
|
+
end
|
|
61
|
+
|
|
58
62
|
def test_connection(options = {})
|
|
59
63
|
super
|
|
60
64
|
begin
|
|
61
|
-
client.
|
|
65
|
+
client.run_test_connection
|
|
62
66
|
rescue StandardError => e
|
|
63
67
|
Rails.logger.error("OpenTofu test connection failed: #{e.message}")
|
|
64
68
|
errors.add(:base, e.message)
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
module ForemanOpentofu
|
|
19
19
|
class Tofu < ComputeResource
|
|
20
20
|
include OpentofuVMCommands
|
|
21
|
+
include ComputeResourceCaching
|
|
21
22
|
validates :provider, presence: true, inclusion: { in: %w[Tofu] }
|
|
22
23
|
validates :url, presence: true
|
|
23
24
|
validates :user, presence: true
|
|
@@ -26,12 +27,19 @@ module ForemanOpentofu
|
|
|
26
27
|
# alias_attribute :username, :user
|
|
27
28
|
# alias_attribute :endpoint, :url
|
|
28
29
|
|
|
29
|
-
delegate :available_attributes, to: :tofu_provider
|
|
30
|
+
delegate :available_attributes, :capabilities, to: :tofu_provider
|
|
31
|
+
|
|
32
|
+
def available_images
|
|
33
|
+
# make sure available_images can use this CR, e.g. for requesting data_source
|
|
34
|
+
tofu_provider.available_images(self)
|
|
35
|
+
end
|
|
30
36
|
|
|
31
37
|
def provided_attributes
|
|
32
|
-
super.merge(
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
super.merge(tofu_provider.provided_attributes || {})
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def user_data_supported?
|
|
42
|
+
true
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
def opentofu_provider
|
|
@@ -60,10 +68,6 @@ module ForemanOpentofu
|
|
|
60
68
|
'OpenTofu'
|
|
61
69
|
end
|
|
62
70
|
|
|
63
|
-
def capabilities
|
|
64
|
-
[:build]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
71
|
def self.model_name
|
|
68
72
|
ComputeResource.model_name
|
|
69
73
|
end
|
|
@@ -76,6 +80,12 @@ module ForemanOpentofu
|
|
|
76
80
|
true
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
def vm_ready(vm)
|
|
84
|
+
return tofu_provider.vm_ready(vm) if tofu_provider.respond_to? :vm_ready
|
|
85
|
+
|
|
86
|
+
vm.wait_for { ready? }
|
|
87
|
+
end
|
|
88
|
+
|
|
79
89
|
def tofu_provider
|
|
80
90
|
ProviderTypeManager.find(opentofu_provider)
|
|
81
91
|
end
|
|
@@ -84,8 +94,23 @@ module ForemanOpentofu
|
|
|
84
94
|
{ compute_attributes: {} }
|
|
85
95
|
end
|
|
86
96
|
|
|
97
|
+
def new_volume
|
|
98
|
+
{ compute_attributes: {} }
|
|
99
|
+
end
|
|
100
|
+
|
|
87
101
|
def editable_network_interfaces?
|
|
88
102
|
true
|
|
89
103
|
end
|
|
104
|
+
|
|
105
|
+
def available_resource(resource_name, options = {})
|
|
106
|
+
cache.cache("#{name}_#{resource_name}") do
|
|
107
|
+
resource = fetch_resource(resource_name, options)
|
|
108
|
+
resource.map { |h| OpenStruct.new(h) }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def available_resource_ui_select(resource_name, options = {})
|
|
113
|
+
available_resource(resource_name, options)&.map { |obj| [obj['name'], obj['id']] }
|
|
114
|
+
end
|
|
90
115
|
end
|
|
91
116
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module ForemanOpentofu
|
|
2
2
|
class AppWrapper
|
|
3
|
-
|
|
3
|
+
include HclFormat
|
|
4
|
+
attr_reader :workdir
|
|
4
5
|
|
|
5
6
|
# TODO: for future versions
|
|
6
7
|
# - manage temp-work-dir; problem: no auto-remove after finished :-(
|
|
@@ -9,14 +10,33 @@ module ForemanOpentofu
|
|
|
9
10
|
# - use JSON-output for easier parsing
|
|
10
11
|
# - do we need locking or has the object be atomic
|
|
11
12
|
|
|
12
|
-
def initialize(workdir)
|
|
13
|
+
def initialize(workdir, opts = {})
|
|
13
14
|
@workdir = workdir
|
|
14
|
-
@
|
|
15
|
-
@conffile = File.join(workdir, 'main.tf')
|
|
15
|
+
@variables = opts[:variables]
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# rubocop:disable Style/SingleLineMethods
|
|
19
|
+
def planfile() File.join(workdir, 'plan.bin') end
|
|
20
|
+
def conffile() File.join(workdir, 'main.tf') end
|
|
21
|
+
def vardeffile() File.join(workdir, 'variables.tf') end
|
|
22
|
+
|
|
23
|
+
def base_command() 'tofu' end
|
|
24
|
+
# rubocop:enable Style/SingleLineMethods
|
|
25
|
+
|
|
26
|
+
# write variables definition file based on @variables into 'variables.tf'
|
|
27
|
+
def create_variables_file
|
|
28
|
+
File.open(vardeffile, 'w') do |f|
|
|
29
|
+
@variables.each_key do |var|
|
|
30
|
+
data = { 'type' => :string }
|
|
31
|
+
data['sensitive'] = var.to_s == 'password'
|
|
32
|
+
|
|
33
|
+
f << block_to_hcl(['variable', var], data)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def variable_params
|
|
39
|
+
@variables.map { |var, val| ['-var', "#{var}=#{val}"] }.flatten
|
|
20
40
|
end
|
|
21
41
|
|
|
22
42
|
def default_params
|
|
@@ -32,6 +52,8 @@ module ForemanOpentofu
|
|
|
32
52
|
# end
|
|
33
53
|
# end
|
|
34
54
|
def init(params = [], &block)
|
|
55
|
+
create_variables_file if @variables
|
|
56
|
+
|
|
35
57
|
tofu_execute('init', ['-input=false'].concat(parse_params(params)), &block)
|
|
36
58
|
end
|
|
37
59
|
|
|
@@ -78,18 +100,19 @@ module ForemanOpentofu
|
|
|
78
100
|
cmd.map { |item| "'#{item}'" }.append('2>&1').join(' ')
|
|
79
101
|
end
|
|
80
102
|
|
|
103
|
+
def envvars
|
|
104
|
+
@variables.transform_keys { |variable| "TF_VAR_#{variable}" }
|
|
105
|
+
end
|
|
106
|
+
|
|
81
107
|
def execute(cmd)
|
|
82
108
|
output = nil
|
|
83
109
|
# quote cmdline parameters and add stderr to stdout
|
|
84
110
|
commandline = command(cmd)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
output = pipe.read
|
|
92
|
-
end
|
|
111
|
+
IO.popen(envvars, commandline, 'r+', chdir: workdir) do |pipe|
|
|
112
|
+
if block_given?
|
|
113
|
+
yield pipe
|
|
114
|
+
else
|
|
115
|
+
output = pipe.read
|
|
93
116
|
end
|
|
94
117
|
end
|
|
95
118
|
ret = $CHILD_STATUS
|