foreman_opentofu 0.0.1
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 +7 -0
- data/LICENSE +674 -0
- data/README.md +134 -0
- data/Rakefile +30 -0
- data/app/controllers/api/v2/tf_states_controller.rb +52 -0
- data/app/controllers/concerns/foreman_opentofu/compute_resources_vms_controller.rb +17 -0
- data/app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb +42 -0
- data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +89 -0
- data/app/models/concerns/orchestration/tofu/compute.rb +24 -0
- data/app/models/foreman_opentofu/compute_vm.rb +99 -0
- data/app/models/foreman_opentofu/opentofu_vm_commands.rb +88 -0
- data/app/models/foreman_opentofu/tf_state.rb +8 -0
- data/app/models/foreman_opentofu/tofu.rb +77 -0
- data/app/overrides/compute_resources/remove_virtual_machines_tab.rb +8 -0
- data/app/services/foreman_opentofu/app_wrapper.rb +107 -0
- data/app/services/foreman_opentofu/compute_fetcher.rb +51 -0
- data/app/services/foreman_opentofu/opentofu_executer.rb +78 -0
- data/app/services/foreman_opentofu/provider_type.rb +30 -0
- data/app/services/foreman_opentofu/provider_type_manager.rb +36 -0
- data/app/views/compute_resources/form/_tofu.html.erb +9 -0
- data/app/views/compute_resources/show/_tofu.html.erb +8 -0
- data/app/views/compute_resources_vms/form/tofu/_base.html.erb +13 -0
- data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +18 -0
- data/app/views/compute_resources_vms/form/tofu/_network.html.erb +5 -0
- data/app/views/compute_resources_vms/index/_tofu.html.erb +0 -0
- data/app/views/templates/provisioning/nutanix_provision_host.erb +51 -0
- data/app/views/templates/provisioning/ovirt_provision_host.erb +68 -0
- data/config/initializers/compute_attrs.rb +19 -0
- data/config/nutanix.json +27 -0
- data/config/ovirt.json +18 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20250625192757_create_tf_state.foreman_opentofu.rb +10 -0
- data/db/seeds.d/71_provisioning_templates.rb +9 -0
- data/lib/foreman_opentofu/engine.rb +75 -0
- data/lib/foreman_opentofu/provider_types/nutanix.rb +6 -0
- data/lib/foreman_opentofu/provider_types/ovirt.rb +2 -0
- data/lib/foreman_opentofu/provider_types.rb +3 -0
- data/lib/foreman_opentofu/version.rb +3 -0
- data/lib/foreman_opentofu.rb +6 -0
- data/lib/tasks/foreman_opentofu_tasks.rake +30 -0
- data/locale/Makefile +73 -0
- data/locale/en/foreman_opentofu.po +19 -0
- data/locale/foreman_opentofu.pot +19 -0
- data/locale/gemspec.rb +2 -0
- data/test/controllers/api/v2/tf_states_controller_test.rb +67 -0
- data/test/factories/compute_resources.rb +26 -0
- data/test/factories/foreman_opentofu_factories.rb +7 -0
- data/test/factories/tf_state_factories.rb +7 -0
- data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +147 -0
- data/test/models/foreman_opentofu_test.rb +43 -0
- data/test/services/app_wrapper_test.rb +31 -0
- data/test/services/opentofu_executer_test.rb +62 -0
- data/test/test_plugin_helper.rb +6 -0
- metadata +119 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class AppWrapper
|
|
3
|
+
attr_reader :workdir, :planfile, :conffile
|
|
4
|
+
|
|
5
|
+
# TODO: for future versions
|
|
6
|
+
# - manage temp-work-dir; problem: no auto-remove after finished :-(
|
|
7
|
+
# - handle ENVVars if applicable
|
|
8
|
+
# - handle stderr and stdout separately
|
|
9
|
+
# - use JSON-output for easier parsing
|
|
10
|
+
# - do we need locking or has the object be atomic
|
|
11
|
+
|
|
12
|
+
def initialize(workdir)
|
|
13
|
+
@workdir = workdir
|
|
14
|
+
@planfile = File.join(workdir, 'plan.bin')
|
|
15
|
+
@conffile = File.join(workdir, 'main.tf')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def base_command
|
|
19
|
+
'tofu'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def default_params
|
|
23
|
+
[
|
|
24
|
+
'-no-color',
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# optional: specify block to access command-pipe (object of class `IO`), e.g.
|
|
29
|
+
# tofu.init do |t|
|
|
30
|
+
# until t.eof? do
|
|
31
|
+
# puts("Message from Tofu: #{t.gets}")
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
def init(params = [], &block)
|
|
35
|
+
tofu_execute('init', ['-input=false'].concat(parse_params(params)), &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def plan(params = [])
|
|
39
|
+
tofu_execute('plan', ["-out=#{planfile}"].concat(parse_params(params)))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def apply(params = [])
|
|
43
|
+
tofu_execute('apply', ['-auto-approve'].concat(parse_params(params)))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def destroy(params = [])
|
|
47
|
+
tofu_execute('destroy', ['-auto-approve'].concat(parse_params(params)))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def output(params = [])
|
|
51
|
+
JSON.parse(tofu_execute('output', ['-json'].concat(parse_params(params))))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# TODO: find better name ;-)
|
|
55
|
+
def show_plan(params = [])
|
|
56
|
+
JSON.parse(tofu_execute('show', ['-json', planfile].concat(parse_params(params))))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def main_configuration
|
|
60
|
+
File.read(conffile)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def main_configuration=(config)
|
|
64
|
+
File.write(conffile, config)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def parse_params(params)
|
|
70
|
+
params.is_a?(String) ? [params] : params
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def tofu_execute(action, params = [], &block)
|
|
74
|
+
execute [base_command, action].concat(default_params).concat(params), &block
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def command(cmd)
|
|
78
|
+
cmd.map { |item| "'#{item}'" }.append('2>&1').join(' ')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def execute(cmd)
|
|
82
|
+
output = nil
|
|
83
|
+
# quote cmdline parameters and add stderr to stdout
|
|
84
|
+
commandline = command(cmd)
|
|
85
|
+
Dir.chdir(workdir) do
|
|
86
|
+
Rails.logger.debug "Start command: #{commandline.inspect}"
|
|
87
|
+
IO.popen(commandline, 'r+') do |pipe|
|
|
88
|
+
if block_given?
|
|
89
|
+
yield pipe
|
|
90
|
+
else
|
|
91
|
+
output = pipe.read
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
ret = $CHILD_STATUS
|
|
96
|
+
Rails.logger.info "#{cmd} returned #{ret.inspect}"
|
|
97
|
+
Rails.logger.debug output.to_s
|
|
98
|
+
unless ret.success?
|
|
99
|
+
Rails.logger.error "Command failed with output: #{output}"
|
|
100
|
+
# TODO: do we need to use a specific exception-type here?
|
|
101
|
+
raise "command failed with code #{ret.exitstatus}:\n#{output}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
output
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class ComputeFetcher
|
|
3
|
+
def self.fetch_subnets(compute_resource)
|
|
4
|
+
return [] unless compute_resource == 'Nutanix'
|
|
5
|
+
attrs = []
|
|
6
|
+
Dir.mktmpdir('opentofu_') do |dir|
|
|
7
|
+
tofu = ForemanOpentofu::AppWrapper.new(dir)
|
|
8
|
+
tofu.main_configuration = tf_content(compute_resource)
|
|
9
|
+
tofu.init
|
|
10
|
+
tofu.apply
|
|
11
|
+
attrs = tofu.output('subnets')
|
|
12
|
+
end
|
|
13
|
+
attrs.map { |h| OpenStruct.new(h) }
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
Rails.logger.error("Failed to fetch subnets: #{e}")
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.tf_content(compute_resource)
|
|
20
|
+
<<~HCL
|
|
21
|
+
terraform {
|
|
22
|
+
required_providers {
|
|
23
|
+
nutanix = {
|
|
24
|
+
source = "nutanix/nutanix"
|
|
25
|
+
version = ">=1.6.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
provider "nutanix" {
|
|
31
|
+
username = "#{compute_resource.user}"
|
|
32
|
+
password = "#{compute_resource.password}"
|
|
33
|
+
endpoint = "#{compute_resource.url}"
|
|
34
|
+
insecure = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Fetch all subnets
|
|
38
|
+
data "nutanix_subnets" "all" {}
|
|
39
|
+
|
|
40
|
+
output "subnets" {
|
|
41
|
+
value = [
|
|
42
|
+
for s in data.nutanix_subnets.all.entities : {
|
|
43
|
+
id = s.metadata.uuid
|
|
44
|
+
name = s.name
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
HCL
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module ForemanOpentofu
|
|
4
|
+
class OpentofuExecuter
|
|
5
|
+
def initialize(*args)
|
|
6
|
+
@compute_resource = args[0]
|
|
7
|
+
@cr_attrs = args[1] || {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run(mode = 'create')
|
|
11
|
+
Dir.mktmpdir('opentofu_') do |dir|
|
|
12
|
+
tofu = AppWrapper.new(dir)
|
|
13
|
+
@use_backend = %w[create destroy output].include?(mode)
|
|
14
|
+
tofu.main_configuration = render_template
|
|
15
|
+
tofu.init
|
|
16
|
+
run_mode(tofu, mode)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_mode(tofu, mode = 'new')
|
|
21
|
+
@use_backend = true
|
|
22
|
+
case mode
|
|
23
|
+
when 'new'
|
|
24
|
+
tofu.plan
|
|
25
|
+
tofu.show_plan
|
|
26
|
+
when 'test_connection'
|
|
27
|
+
tofu.plan
|
|
28
|
+
when 'create'
|
|
29
|
+
tofu.plan
|
|
30
|
+
tofu.apply
|
|
31
|
+
attrs = tofu.output('vm_attrs')
|
|
32
|
+
ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.update(uuid: attrs['identity'])
|
|
33
|
+
attrs
|
|
34
|
+
when 'output'
|
|
35
|
+
tofu.output('vm_attrs')
|
|
36
|
+
when 'destroy'
|
|
37
|
+
tofu.destroy
|
|
38
|
+
else
|
|
39
|
+
raise "Please select one of the modes: 'new', 'test_connection', 'create' or 'destroy'"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def render_template
|
|
46
|
+
template = provision_template
|
|
47
|
+
scope = Foreman::Renderer.get_scope(source: template)
|
|
48
|
+
source = Foreman::Renderer.get_source(template: template)
|
|
49
|
+
scope.instance_variable_set(:@compute_resource, @compute_resource)
|
|
50
|
+
scope.instance_variable_set(:@cr_attrs, @cr_attrs) if @cr_attrs
|
|
51
|
+
scope.instance_variable_set(:@use_backend, @use_backend)
|
|
52
|
+
rendered_template = Foreman::Renderer::UnsafeModeRenderer.render(source, scope)
|
|
53
|
+
raise ::Foreman::Exception, N_('Unable to render provisioning template') unless rendered_template
|
|
54
|
+
|
|
55
|
+
rendered_template
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def provision_template
|
|
59
|
+
name = ''
|
|
60
|
+
provider = @compute_resource.opentofu_provider
|
|
61
|
+
case provider
|
|
62
|
+
when 'nutanix'
|
|
63
|
+
name = Setting[:provision_nutanix_host_template]
|
|
64
|
+
when 'ovirt'
|
|
65
|
+
name = Setting[:provision_ovirt_host_template]
|
|
66
|
+
when 'vsphere'
|
|
67
|
+
name = Setting[:provision_vsphere_host_template]
|
|
68
|
+
end
|
|
69
|
+
template = ProvisioningTemplate.unscoped.find_by(name: name)
|
|
70
|
+
unless template
|
|
71
|
+
raise ::Foreman::Exception.new(N_('Unable to find template specified by %s setting'),
|
|
72
|
+
name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
template
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class ProviderType
|
|
3
|
+
attr_reader :id, :name
|
|
4
|
+
|
|
5
|
+
def initialize(id)
|
|
6
|
+
@id = id.to_sym
|
|
7
|
+
@name = id.capitalize
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# returns hash of available-attributes with attr-name as key
|
|
11
|
+
def available_attributes
|
|
12
|
+
raise "No available-attributes found for #{name}" unless attributes?
|
|
13
|
+
|
|
14
|
+
attributes&.index_by { |e| e['name'] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def attributes?
|
|
18
|
+
CR_ATTRS.key? id.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def attributes(group = nil)
|
|
22
|
+
return nil unless CR_ATTRS.key? id.to_s
|
|
23
|
+
|
|
24
|
+
a = CR_ATTRS[id.to_s]
|
|
25
|
+
return a if group.nil?
|
|
26
|
+
|
|
27
|
+
a.select { |e| e['group'] == group }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class ProviderTypeManager
|
|
3
|
+
@defined_provider_types = {}
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
private :new
|
|
7
|
+
attr_reader :defined_provider_types
|
|
8
|
+
|
|
9
|
+
# Plugin constructor
|
|
10
|
+
def register(id, &block)
|
|
11
|
+
defined_prov_type = find_defined(id)
|
|
12
|
+
return if defined_prov_type.present?
|
|
13
|
+
|
|
14
|
+
defined_prov_type = ::ForemanOpentofu::ProviderType.new(id)
|
|
15
|
+
defined_prov_type.instance_eval(&block) if block_given?
|
|
16
|
+
@defined_provider_types[id.to_s] = defined_prov_type
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find(provider_type)
|
|
20
|
+
find_defined(provider_type)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_defined(provider_type)
|
|
24
|
+
@defined_provider_types[provider_type.to_s]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enabled_provider_type_names
|
|
28
|
+
@defined_provider_types.values.map(&:name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def enabled_provider_types
|
|
32
|
+
@defined_provider_types.values
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%= field_set_tag _("Common fields"), :id => "compute_ressource_common_field_set" do %>
|
|
2
|
+
<%= select_f f, :opentofu_provider, ForemanOpentofu::ProviderTypeManager.enabled_provider_types, :id, :name %>
|
|
3
|
+
<%= text_f f, :url, :help_block => _("127.0.0.1") %>
|
|
4
|
+
<%= text_f f, :user , :help_block => _("e.g. admin") %>
|
|
5
|
+
<%= password_f f, :password %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<div class="col-md-offset-2">
|
|
8
|
+
<%= test_connection_button_f(f, (true)) %>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% provider = compute_resource.tofu_provider %>
|
|
2
|
+
<% if provider.attributes.present? %>
|
|
3
|
+
<% vm_attrs = provider.attributes('vm') %>
|
|
4
|
+
<% disk_attrs = provider.attributes('disk') %>
|
|
5
|
+
<%= field_set_tag _("VM Config") do %>
|
|
6
|
+
<%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: vm_attrs } %>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<%= field_set_tag _("Storage") do %>
|
|
10
|
+
<%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: disk_attrs } %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<% attrs.each do |attr|
|
|
2
|
+
label = attr.fetch('label', attr['name'].humanize) %>
|
|
3
|
+
|
|
4
|
+
<% case attr["type"] %>
|
|
5
|
+
<% when 'string' %>
|
|
6
|
+
<%= text_f f, attr["name"].to_sym, label: _(label), label_help: attr['help'] %>
|
|
7
|
+
<% when 'bool' %>
|
|
8
|
+
<%= checkbox_f f, attr["name"].to_sym, label: _(label), label_help: attr['help'] %>
|
|
9
|
+
<% when 'number' %>
|
|
10
|
+
<%= counter_f f, attr["name"].to_sym, label: _(label), label_help: attr['help'] %>
|
|
11
|
+
<% when 'select' %>
|
|
12
|
+
<% if attr.key? 'options' %>
|
|
13
|
+
<%= select_f f, attr["name"].to_sym, attr.fetch('options', []), :to_s, :to_s, { :include_blank => true }, label: _(label), label_help: attr['help'] %>
|
|
14
|
+
<% else %>
|
|
15
|
+
<%= select_f f, attr["name"].to_sym, (options || []), :id, :name, { :include_blank => true }, label: _(label), label_help: attr['help'] %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% provider = compute_resource.opentofu_provider.to_s %>
|
|
2
|
+
<% if CR_ATTRS.key?(provider) %>
|
|
3
|
+
<% nic_attrs = CR_ATTRS[provider].select { |a| a["group"] == "nic" } %>
|
|
4
|
+
<%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: nic_attrs, options: ForemanOpentofu::ComputeFetcher.fetch_subnets(compute_resource) } %>
|
|
5
|
+
<% end %>
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
<%#
|
|
3
|
+
kind: script
|
|
4
|
+
name: Nutanix provision - host
|
|
5
|
+
model: ProvisioningTemplate
|
|
6
|
+
description: |
|
|
7
|
+
nutanix opentofu script to create vm resource
|
|
8
|
+
-%>
|
|
9
|
+
<%-
|
|
10
|
+
host_name = @cr_attrs['name'] || 'test'
|
|
11
|
+
%>
|
|
12
|
+
|
|
13
|
+
terraform {
|
|
14
|
+
required_providers {
|
|
15
|
+
nutanix = {
|
|
16
|
+
source = "nutanix/nutanix"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
<% if @use_backend %>
|
|
20
|
+
backend "http" {
|
|
21
|
+
address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= host_name %>"
|
|
22
|
+
username = "admin"
|
|
23
|
+
password = "changeme"
|
|
24
|
+
}
|
|
25
|
+
<% end %>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
provider "nutanix" {
|
|
29
|
+
username = "<%= @compute_resource['user'] %>"
|
|
30
|
+
password = "<%= @compute_resource['password'] %>"
|
|
31
|
+
endpoint = "<%= @compute_resource['url'] %>"
|
|
32
|
+
insecure = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data "nutanix_clusters" "clusters" {}
|
|
36
|
+
|
|
37
|
+
resource "nutanix_virtual_machine" "vm" {
|
|
38
|
+
cluster_uuid = data.nutanix_clusters.clusters.entities.0.metadata.uuid
|
|
39
|
+
name = "<%= host_name %>"
|
|
40
|
+
|
|
41
|
+
<%= vm_attributes(['name', 'cluster_uuid']) %>
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
output "vm_attrs" {
|
|
46
|
+
value = {
|
|
47
|
+
identity = nutanix_virtual_machine.vm.id
|
|
48
|
+
mac = nutanix_virtual_machine.vm.nic_list[0].mac_address
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
<%#
|
|
3
|
+
kind: script
|
|
4
|
+
name: Ovirt provision - host
|
|
5
|
+
model: ProvisioningTemplate
|
|
6
|
+
description: |
|
|
7
|
+
nutanix opentofu script to create vm resource
|
|
8
|
+
-%>
|
|
9
|
+
<%-
|
|
10
|
+
host_name = @cr_attrs['name'] || 'test'
|
|
11
|
+
%>
|
|
12
|
+
|
|
13
|
+
terraform {
|
|
14
|
+
required_providers {
|
|
15
|
+
ovirt = {
|
|
16
|
+
source = "ovirt/ovirt"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
<% if @use_backend %>
|
|
20
|
+
backend "http" {
|
|
21
|
+
address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= host_name %>"
|
|
22
|
+
username = "admin"
|
|
23
|
+
password = "changeme"
|
|
24
|
+
}
|
|
25
|
+
<% end %>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
provider "ovirt" {
|
|
29
|
+
username = "<%= @compute_resource['user'] %>"
|
|
30
|
+
password = "<%= @compute_resource['password'] %>"
|
|
31
|
+
url = "<%= @compute_resource['url'] %>"
|
|
32
|
+
tls_insecure = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data "ovirt_blank_template" "blank" {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resource "ovirt_vm" "vm" {
|
|
39
|
+
name = "<%= host_name %>"
|
|
40
|
+
cluster_id = "2ad616ea-4b66-11f0-964d-901b0ecbd584"
|
|
41
|
+
template_id = data.ovirt_blank_template.blank.id
|
|
42
|
+
cpu_cores = <%= @cr_attrs['cpu_cores'] || 1 %>
|
|
43
|
+
cpu_sockets = <%= @cr_attrs['cpu_sockets'] || 1 %>
|
|
44
|
+
cpu_threads = <%= @cr_attrs['cpu_threads'] || 1 %>
|
|
45
|
+
memory = <%= @cr_attrs['memory'] || 400000 %>
|
|
46
|
+
maximum_memory = <%= @cr_attrs['maximum_memory'] || 4000000 %>
|
|
47
|
+
memory_ballooning = <%= @cr_attrs['memory_ballooning'] || false %>
|
|
48
|
+
}
|
|
49
|
+
resource "ovirt_vm" "vm" {
|
|
50
|
+
name = "<%= host_name %>"
|
|
51
|
+
cluster_id = "2ad616ea-4b66-11f0-964d-901b0ecbd584"
|
|
52
|
+
template_id = data.ovirt_blank_template.blank.id
|
|
53
|
+
<%= "cpu_cores = #{@cr_attrs['cpu_cores'] || 1}" %>
|
|
54
|
+
<%= "cpu_sockets = #{@cr_attrs['cpu_sockets'] || 1}" %>
|
|
55
|
+
<%= "cpu_threads = #{@cr_attrs['cpu_threads'] || 1}" %>
|
|
56
|
+
<%= "cpu_mode = \"#{@cr_attrs['cpu_mode']}\"" if @cr_attrs['cpu_mode'].present? %>
|
|
57
|
+
<%= "memory = #{@cr_attrs['memory'] || 400000}" %>
|
|
58
|
+
<%= "maximum_memory = #{@cr_attrs['maximum_memory'] || 4000000}" %>
|
|
59
|
+
<%= "memory_ballooning = #{@cr_attrs['memory_ballooning']}" if @cr_attrs.key?('memory_ballooning') %>
|
|
60
|
+
<%= "id = \"#{@cr_attrs['id']}\"" if @cr_attrs['id'].present? %>
|
|
61
|
+
<%= "os_type = \"#{@cr_attrs['os_type']}\"" if @cr_attrs['os_type'].present? %>
|
|
62
|
+
<%= "soundcard_enabled = #{@cr_attrs['soundcard_enabled']}" if @cr_attrs.key?('soundcard_enabled') %>
|
|
63
|
+
<%= "vm_type = \"#{@cr_attrs['vm_type']}\"" if @cr_attrs['vm_type'].present? %>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
output "uuid" {
|
|
67
|
+
value = ovirt_vm.vm
|
|
68
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
CONFIG_DIR = File.expand_path(File.join(__dir__, '..'))
|
|
4
|
+
|
|
5
|
+
# rubocop:disable Style/MutableConstant
|
|
6
|
+
CR_ATTRS = {}
|
|
7
|
+
# rubocop:enable Style/MutableConstant
|
|
8
|
+
|
|
9
|
+
Dir["#{CONFIG_DIR}/*.{json,yaml,yml}"].each do |filename|
|
|
10
|
+
next unless File.exist?(filename)
|
|
11
|
+
|
|
12
|
+
file_content = File.read(filename)
|
|
13
|
+
provider = File.basename(filename, '.*')
|
|
14
|
+
CR_ATTRS[provider] = case File.extname(filename)
|
|
15
|
+
when /\.json/i then JSON.parse(file_content)
|
|
16
|
+
when /\.ya?ml/i then YAML.safe_load(file_content)
|
|
17
|
+
else next
|
|
18
|
+
end
|
|
19
|
+
end
|
data/config/nutanix.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "num_sockets", "type": "number", "group": "vm", "mandatory": false,
|
|
3
|
+
"label": "Sockets" },
|
|
4
|
+
{ "name": "num_vcpus_per_socket", "type": "number", "group": "vm", "mandatory": false,
|
|
5
|
+
"label": "Cores per socket" },
|
|
6
|
+
{ "name": "memory_size_mib", "type": "number", "group": "vm", "mandatory": false,
|
|
7
|
+
"label": "Memory (MB)" },
|
|
8
|
+
{ "name": "enable_cpu_passthrough", "label": "CPU Passthrough Enable", "type": "bool", "group": "vm", "mandatory": false },
|
|
9
|
+
{ "name": "num_vnuma_nodes", "type": "number", "group": "vm", "mandatory": false,
|
|
10
|
+
"label": "vNUMA Nodes", "help": "Number of vNUMA nodes. 0 means vNUMA is disabled." },
|
|
11
|
+
{ "name": "boot_type", "type": "select", "group": "vm", "mandatory": false,
|
|
12
|
+
"label": "Firmware", "options": [ "UEFI", "LEGACY", "SECURE_BOOT" ] },
|
|
13
|
+
{ "name": "power_state", "type": "select", "group": "vm", "mandatory": false,
|
|
14
|
+
"options": [ "ON" , "OFF" ] },
|
|
15
|
+
{ "name": "use_hot_add", "type": "bool", "group": "vm", "mandatory": false,
|
|
16
|
+
"help": "Use Hot Add when modifying VM resources. Passing value false will result in VM reboots. Default value is true." },
|
|
17
|
+
{ "name": "vga_console_enabled", "type": "bool", "group": "vm", "mandatory": false,
|
|
18
|
+
"label": "VGA Console Enable" },
|
|
19
|
+
{ "name": "disk_size_mib", "type": "number", "group": "disk", "mandatory": true,
|
|
20
|
+
"label": "Size (MB)" },
|
|
21
|
+
{ "name": "nic_type", "type": "select", "group": "nic", "mandatory": false,
|
|
22
|
+
"label": "NIC Type", "options": [ "NORMAL_NIC", "DIRECT_NIC", "NETWORK_FUNCTION_NIC" ] },
|
|
23
|
+
{ "name": "model", "type": "select", "group": "nic", "mandatory": true,
|
|
24
|
+
"options": [ "VIRTIO", "E1000" ] },
|
|
25
|
+
{ "name": "subnet_uuid", "type": "select", "group": "nic", "mandatory": true,
|
|
26
|
+
"label": "Subnet" }
|
|
27
|
+
]
|
data/config/ovirt.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "clone", "type": "bool", "group": "vm" },
|
|
3
|
+
{ "name": "cluster_id", "type": "string", "group": "vm" },
|
|
4
|
+
{ "name": "cpu_cores", "type": "number", "group": "vm" },
|
|
5
|
+
{ "name": "cpu_mode", "type": "string", "group": "vm" },
|
|
6
|
+
{ "name": "cpu_sockets", "type": "number", "group": "vm" },
|
|
7
|
+
{ "name": "cpu_threads", "type": "number", "group": "vm" },
|
|
8
|
+
{ "name": "id", "type": "string", "group": "vm" },
|
|
9
|
+
{ "name": "maximum_memory", "type": "number", "group": "vm" },
|
|
10
|
+
{ "name": "memory", "type": "number", "group": "vm" },
|
|
11
|
+
{ "name": "memory_ballooning", "type": "bool", "group": "vm" },
|
|
12
|
+
{ "name": "name", "type": "string", "group": "vm" },
|
|
13
|
+
{ "name": "os_type", "type": "string", "group": "vm" },
|
|
14
|
+
{ "name": "soundcard_enabled", "type": "bool", "group": "vm" },
|
|
15
|
+
{ "name": "template_id", "type": "string", "group": "vm" },
|
|
16
|
+
{ "name": "vm_type", "type": "string", "group": "vm" }
|
|
17
|
+
]
|
|
18
|
+
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Rails.application.routes.draw do
|
|
2
|
+
namespace :api do
|
|
3
|
+
scope '(:apiv)',
|
|
4
|
+
module: :v2,
|
|
5
|
+
defaults: { apiv: 'v2' },
|
|
6
|
+
apiv: /v1|v2/,
|
|
7
|
+
constraints: ApiConstraints.new(version: 2, default: true) do
|
|
8
|
+
match 'tf_states/:name', to: 'tf_states#create', via: :post, constraints: { name: %r{[^/]+} }
|
|
9
|
+
match 'tf_states/:name', to: 'tf_states#show', via: :get, constraints: { name: %r{[^/]+} }
|
|
10
|
+
match 'tf_states/:name', to: 'tf_states#destroy', via: :delete, constraints: { name: %r{[^/]+} }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace ForemanOpentofu
|
|
4
|
+
engine_name 'foreman_opentofu'
|
|
5
|
+
|
|
6
|
+
# Add any db migrations
|
|
7
|
+
initializer 'foreman_opentofu.load_app_instance_data' do |app|
|
|
8
|
+
ForemanOpentofu::Engine.paths['db/migrate'].existent.each do |path|
|
|
9
|
+
app.config.paths['db/migrate'] << path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
app.config.autoload_paths += Dir["#{config.root}/app/services/foreman_opentofu"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer 'foreman_opentofu.register_plugin', before: :finisher_hook do |app|
|
|
16
|
+
app.reloader.to_prepare do
|
|
17
|
+
Foreman::Plugin.register :foreman_opentofu do
|
|
18
|
+
requires_foreman '>= 3.0'
|
|
19
|
+
register_gettext
|
|
20
|
+
|
|
21
|
+
extend_template_helpers ForemanOpentofu::Concerns::BaseTemplateScopeExtensions
|
|
22
|
+
# Add Global files for extending foreman-core components and routes
|
|
23
|
+
# Register Nutanix compute resource in foreman
|
|
24
|
+
compute_resource ForemanOpentofu::Tofu
|
|
25
|
+
settings do
|
|
26
|
+
category :opentofu, N_('Opentofu') do
|
|
27
|
+
templates = lambda {
|
|
28
|
+
Hash[ProvisioningTemplate.where(template_kind: TemplateKind.where(name: 'script')).map do |temp|
|
|
29
|
+
[temp[:name], temp[:name]]
|
|
30
|
+
end ]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setting 'provision_nutanix_host_template',
|
|
34
|
+
type: :string,
|
|
35
|
+
collection: templates,
|
|
36
|
+
default: 'Nutanix provision - host',
|
|
37
|
+
full_name: N_('Nutanix Host provision template'),
|
|
38
|
+
description: N_('Opentofu script template to use for Nutanix based host provisioning')
|
|
39
|
+
setting 'provision_ovirt_host_template',
|
|
40
|
+
type: :string,
|
|
41
|
+
collection: templates,
|
|
42
|
+
default: 'Ovirt provision - host',
|
|
43
|
+
full_name: N_('Ovirt Host provision template'),
|
|
44
|
+
description: N_('Opentofu script template to use for Ovirt based host provisioning')
|
|
45
|
+
setting 'provision_vsphere_host_template',
|
|
46
|
+
type: :string,
|
|
47
|
+
collection: templates,
|
|
48
|
+
default: 'Vsphere provision - host',
|
|
49
|
+
full_name: N_('Vsphere Host provision template'),
|
|
50
|
+
description: N_('Opentofu script template to use for Vsphere based host provisioning')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
config.autoload_paths << File.expand_path('../lib', __dir__)
|
|
58
|
+
# Include concerns in this config.to_prepare block
|
|
59
|
+
config.to_prepare do
|
|
60
|
+
::ComputeResourcesController.include ForemanOpentofu::Controller::Parameters::ComputeResource
|
|
61
|
+
::ComputeResourcesVmsController.include ForemanOpentofu::ComputeResourcesVmsController
|
|
62
|
+
::Host::Managed.include Orchestration::Tofu::Compute
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
Rails.logger.warn "ForemanOpentofu: skipping engine hook (#{e})"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
load 'foreman_opentofu/provider_types.rb'
|
|
68
|
+
|
|
69
|
+
rake_tasks do
|
|
70
|
+
Rake::Task['db:seed'].enhance do
|
|
71
|
+
ForemanOpentofu::Engine.load_seed
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|