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
data/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
[](https://github.com/ATIX-AG/foreman_opentofu/actions/workflows/ruby.yml)
|
|
2
|
+
|
|
3
|
+
# ForemanOpenTOFU
|
|
4
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
Create a openTofu compute resource and set:
|
|
16
|
+
* Provider: openTofu
|
|
17
|
+
* Opentofu Provider: Select desired hypervisor supported by openTofu plugin
|
|
18
|
+
* URL: Hypervisor specific URL
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Then add all necessary information to the form.
|
|
22
|
+
|
|
23
|
+
Provisioning workflow:
|
|
24
|
+
|
|
25
|
+
* Create a host in Foreman using the openTOFU based compute resource
|
|
26
|
+
|
|
27
|
+
* Foreman passes host parameters to the plugin
|
|
28
|
+
|
|
29
|
+
* The plugin renders and executes openTOFU plans
|
|
30
|
+
|
|
31
|
+
* openTOFU provisions the infrastructure
|
|
32
|
+
|
|
33
|
+
* Foreman continues with OS provisioning and configuration
|
|
34
|
+
|
|
35
|
+
Provider-specific details (for example Nutanix, Hetzner) are handled entirely through openTOFU scripts.
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
### Dev prerequisites
|
|
40
|
+
|
|
41
|
+
> See [Foreman dev setup](https://github.com/theforeman/foreman/blob/develop/developer_docs/foreman_dev_setup.asciidoc)
|
|
42
|
+
|
|
43
|
+
* You need a openTOFU installed on your machine.
|
|
44
|
+
* You need ruby 2.7. You can install it with [asdf-vm](https://asdf-vm.com).
|
|
45
|
+
|
|
46
|
+
### Platform
|
|
47
|
+
|
|
48
|
+
* Fork this github repo.
|
|
49
|
+
* Clone it on your local machine
|
|
50
|
+
* Install foreman v2.5+ on your machine:
|
|
51
|
+
|
|
52
|
+
```shell
|
|
53
|
+
git clone https://github.com/theforeman/foreman -b develop
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
* Create a Gemfile.local.rb file in foreman/bundler.d/
|
|
57
|
+
* Add this line:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
gem 'foreman_opentofu', :path => '../../theforeman/foreman_opentofu'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
* In foreman directory, install dependencies:
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
gem install bundler
|
|
67
|
+
# prerequisites libraries on Ubuntu OS:
|
|
68
|
+
bundle install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
* You can reset and change your admin password if needed:
|
|
72
|
+
|
|
73
|
+
```shell
|
|
74
|
+
RAILS_ENV=development bundle exec bin/rake permissions:reset password=changeme
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
* In foreman_openTofu source directory, check code syntax with rubocop and foreman rules:
|
|
78
|
+
|
|
79
|
+
```shell
|
|
80
|
+
bundle exec rubocop
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
safe autocorrect:
|
|
84
|
+
|
|
85
|
+
```shell
|
|
86
|
+
bundle exec rubocop -a
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Temporary ignore offenses:
|
|
90
|
+
|
|
91
|
+
```shell
|
|
92
|
+
bundle exec rubocop --auto-gen-config
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
* See deface overrides result:
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
bundle exec bin/rake deface:get_result['hosts/_compute_detail']
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
* In foreman directory, after you modify foreman_opentofu translations (language, texts in new files, etc) you have to compile it:
|
|
102
|
+
|
|
103
|
+
Prerequisites: [Transifex CLI](https://github.com/transifex/cli)
|
|
104
|
+
|
|
105
|
+
```shell
|
|
106
|
+
bundle exec bin/rake plugin:gettext\[foreman_opentofu\]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
* In foreman directory, run rails server:
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
bundle exec bin/rails server
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
* Or you can launch all together:
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
bundle exec foreman start
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See details in [foreman plugin development](https://projects.theforeman.org/projects/foreman/wiki/How_to_Create_a_Plugin)
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Fork and send a Pull Request or create Issue. Thank you.
|
|
126
|
+
|
|
127
|
+
## Copyright
|
|
128
|
+
Copyright (c) 2026 ATIX AG - http://www.atix.de
|
|
129
|
+
|
|
130
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
131
|
+
|
|
132
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
133
|
+
|
|
134
|
+
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env rake
|
|
2
|
+
begin
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
rescue LoadError
|
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
Bundler::GemHelper.install_tasks
|
|
9
|
+
|
|
10
|
+
require 'rake/testtask'
|
|
11
|
+
|
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
|
13
|
+
t.libs << 'lib'
|
|
14
|
+
t.libs << 'test'
|
|
15
|
+
t.pattern = 'test/**/*_test.rb'
|
|
16
|
+
t.verbose = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
task default: :test
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
require 'rubocop/rake_task'
|
|
23
|
+
RuboCop::RakeTask.new
|
|
24
|
+
rescue LoadError
|
|
25
|
+
puts 'Rubocop not loaded.'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
task :default do
|
|
29
|
+
Rake::Task['rubocop'].execute
|
|
30
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
module V2
|
|
3
|
+
class TfStatesController < ::Api::V2::BaseController
|
|
4
|
+
include ::Api::Version2
|
|
5
|
+
|
|
6
|
+
resource_description do
|
|
7
|
+
api_version 'v2'
|
|
8
|
+
api_base_url '/foreman_opentofu/api'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
skip_before_action :verify_authenticity_token
|
|
12
|
+
def show
|
|
13
|
+
state = ForemanOpentofu::TfState.find_by(name: params[:name])
|
|
14
|
+
if state
|
|
15
|
+
render plain: state.state, content_type: 'application/json'
|
|
16
|
+
else
|
|
17
|
+
render plain: '', status: :not_found
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
state = ForemanOpentofu::TfState.find_or_create_by(name: params[:name])
|
|
23
|
+
|
|
24
|
+
raw_state = request.body.read
|
|
25
|
+
if raw_state.blank?
|
|
26
|
+
render plain: 'Missing state body', status: :unprocessable_entity
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
begin
|
|
30
|
+
JSON.parse(raw_state)
|
|
31
|
+
|
|
32
|
+
state.state = raw_state
|
|
33
|
+
state.save!
|
|
34
|
+
render plain: '', status: :ok
|
|
35
|
+
rescue JSON::ParserError => e
|
|
36
|
+
Rails.logger.error("Invalid state JSON: #{e.message}")
|
|
37
|
+
render plain: 'Invalid state format', status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
state = ForemanOpentofu::TfState.find_by(name: params[:name])
|
|
43
|
+
state&.destroy
|
|
44
|
+
render plain: '', status: :ok
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resource_class
|
|
48
|
+
@resource_class ||= ForemanOpentofu::TfState
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
module ComputeResourcesVmsController
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
included do
|
|
5
|
+
prepend Overrides
|
|
6
|
+
end
|
|
7
|
+
module Overrides
|
|
8
|
+
def load_vms
|
|
9
|
+
if @compute_resource.is_a?(ForemanOpentofu::Tofu)
|
|
10
|
+
@vms = []
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
module ForemanOpentofu
|
|
21
|
+
module Controller
|
|
22
|
+
module Parameters
|
|
23
|
+
module ComputeResource
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
class_methods do
|
|
27
|
+
def compute_resource_params_filter
|
|
28
|
+
super.tap do |filter|
|
|
29
|
+
filter.permit :endpoint,
|
|
30
|
+
:opentofu_provider,
|
|
31
|
+
:username
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def compute_resource_params
|
|
36
|
+
self.class.compute_resource_params_filter.filter_params(params, parameter_filter_context)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
module Concerns
|
|
3
|
+
module BaseTemplateScopeExtensions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
extend ApipieDSL::Module
|
|
6
|
+
|
|
7
|
+
apipie :class, 'Base macros related to Opentofu templates' do
|
|
8
|
+
name 'Base Content'
|
|
9
|
+
sections only: %w[all provisioning]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
apipie :method, 'Returns all VM parameters' do
|
|
13
|
+
required :skip_list, Array, desc: 'List of parameters to skip'
|
|
14
|
+
returns String, desc: '"key = value" lines'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def vm_attributes(skip_list = [])
|
|
18
|
+
available_attributes = @compute_resource.available_attributes
|
|
19
|
+
res = ''
|
|
20
|
+
@cr_attrs.each do |key, value|
|
|
21
|
+
next if skip_list.include? key
|
|
22
|
+
|
|
23
|
+
conf = available_attributes[key]
|
|
24
|
+
if conf.nil?
|
|
25
|
+
Rails.logger.warn("Attribute #{key.inspect} is not supported.")
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
next if conf['group'] != 'vm'
|
|
29
|
+
next if value.blank? && !conf['mandatory']
|
|
30
|
+
|
|
31
|
+
res << "#{key} = #{format_value(value, conf['type'])}\n"
|
|
32
|
+
end
|
|
33
|
+
res << nic_attributes(available_attributes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def nic_attributes(available_attributes)
|
|
37
|
+
interfaces = @cr_attrs['interfaces'] || @cr_attrs['interfaces_attributes']
|
|
38
|
+
return '' if interfaces.blank?
|
|
39
|
+
|
|
40
|
+
interfaces = normalize_interfaces(interfaces)
|
|
41
|
+
nic_defs = available_attributes.values.select do |attrs|
|
|
42
|
+
attrs['group'] == 'nic'
|
|
43
|
+
end
|
|
44
|
+
res = ''
|
|
45
|
+
interfaces.each do |iface|
|
|
46
|
+
next if iface['subnet_uuid'].blank?
|
|
47
|
+
|
|
48
|
+
res << build_attribute_block('nic_list', iface, nic_defs)
|
|
49
|
+
end
|
|
50
|
+
res
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_interfaces(interfaces)
|
|
54
|
+
if interfaces.is_a?(Hash)
|
|
55
|
+
if interfaces.keys.all? { |k| k.to_s =~ /^\d+$/ }
|
|
56
|
+
interfaces.values
|
|
57
|
+
else
|
|
58
|
+
[interfaces]
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
Array(interfaces)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_attribute_block(block_name, attrs, nic_defs)
|
|
66
|
+
res = "#{block_name} {\n"
|
|
67
|
+
attrs.each do |k, v|
|
|
68
|
+
next if v.blank?
|
|
69
|
+
conf = nic_defs.find { |a| (a['name'] || a[:name]) == k }
|
|
70
|
+
next unless conf
|
|
71
|
+
res << " #{k} = #{format_value(v, conf['type'])}\n" if conf
|
|
72
|
+
end
|
|
73
|
+
res << "}\n"
|
|
74
|
+
res
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def format_value(val, type)
|
|
80
|
+
case type
|
|
81
|
+
when 'string', 'select' then "\"#{val}\""
|
|
82
|
+
when 'bool' then Foreman::Cast.to_bool(val)
|
|
83
|
+
when 'number' then val.to_i
|
|
84
|
+
else val
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Orchestration
|
|
2
|
+
module Tofu
|
|
3
|
+
module Compute
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def computeValue(_foreman_attr, fog_attr)
|
|
7
|
+
value = ''
|
|
8
|
+
value += vm.send(fog_attr).to_s
|
|
9
|
+
value
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def match_macs_to_nics(fog_attr)
|
|
13
|
+
interfaces.select(&:physical?).each do |nic|
|
|
14
|
+
mac = vm.send(fog_attr)
|
|
15
|
+
logger.debug "Orchestration::Compute: nic #{nic.inspect} assigned to #{vm.inspect}"
|
|
16
|
+
nic.mac = mac
|
|
17
|
+
nic.reset_dhcp_record_cache if nic.respond_to?(:reset_dhcp_record_cache) # delete the cached dhcp_record with old MAC on managed nics
|
|
18
|
+
return false unless validate_required_foreman_attr(mac, Nic::Base.physical, :mac)
|
|
19
|
+
end
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
class ComputeVM
|
|
3
|
+
include ActiveModel::Model
|
|
4
|
+
include ActiveModel::Attributes
|
|
5
|
+
|
|
6
|
+
def initialize(provider, attrs = {})
|
|
7
|
+
@attributes = flatten_attrs(attrs.deep_stringify_keys)
|
|
8
|
+
@provider = provider
|
|
9
|
+
define_dynamic_readers!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@attributes[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
unwrap(@attributes.to_dup)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def power
|
|
21
|
+
self['power'] || self['power_state']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# TODO: add definitions for different power on/off values
|
|
25
|
+
def ready?
|
|
26
|
+
power.to_s == 'on'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def name
|
|
30
|
+
self['name']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start
|
|
34
|
+
@provider.start_vm(name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stop
|
|
38
|
+
@provider.stop_vm(name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reboot
|
|
42
|
+
stop
|
|
43
|
+
start
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset
|
|
47
|
+
reboot
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def define_dynamic_readers!
|
|
53
|
+
@attributes.each_key do |key|
|
|
54
|
+
next if respond_to?(key)
|
|
55
|
+
|
|
56
|
+
define_singleton_method(key) do
|
|
57
|
+
@attributes[key]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def deep_wrap(value)
|
|
63
|
+
case value
|
|
64
|
+
when Hash
|
|
65
|
+
value.transform_values { |v| deep_wrap(v) }
|
|
66
|
+
when Array
|
|
67
|
+
value.map { |v| deep_wrap(v) }
|
|
68
|
+
else
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flatten_attrs(attrs)
|
|
74
|
+
result = {}
|
|
75
|
+
|
|
76
|
+
attrs.each do |key, value|
|
|
77
|
+
if key.to_s == 'vm' && value.is_a?(Hash)
|
|
78
|
+
# Merge the "vm" hash into the top-level
|
|
79
|
+
result.merge!(value)
|
|
80
|
+
else
|
|
81
|
+
result[key] = value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
89
|
+
@attributes.key?(method_name.to_s) || super
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def method_missing(method_name, *args)
|
|
93
|
+
key = method_name.to_s
|
|
94
|
+
return @attributes[key] if @attributes.key?(key)
|
|
95
|
+
|
|
96
|
+
super
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module ForemanOpentofu
|
|
2
|
+
module OpentofuVMCommands
|
|
3
|
+
def find_vm_by_uuid(uuid)
|
|
4
|
+
vm_command_errors('find vm') do
|
|
5
|
+
tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
|
|
6
|
+
data = client({ 'name' => tf_state&.name }).run('output')
|
|
7
|
+
ComputeVM.new(self, data)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def new_vm(args = {})
|
|
12
|
+
vm_command_errors('new vm') do
|
|
13
|
+
args = default_attributes.merge(args)
|
|
14
|
+
executor = client(args)
|
|
15
|
+
data = executor.run('new')
|
|
16
|
+
OpenStruct.new(data['resource_changes'].first['change']['after'])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create_vm(args = {})
|
|
21
|
+
vm_command_errors('create vm') do
|
|
22
|
+
args = default_attributes.merge(args)
|
|
23
|
+
executor = client(args)
|
|
24
|
+
output = executor.run('create')
|
|
25
|
+
ComputeVM.new(self, output)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def destroy_vm(uuid)
|
|
30
|
+
tf_state = ForemanOpentofu::TfState.find_by(uuid: uuid)
|
|
31
|
+
client({ 'name' => tf_state&.name }).run('destroy')
|
|
32
|
+
return unless tf_state
|
|
33
|
+
|
|
34
|
+
Rails.logger.info "Deleting tfstate for #{tf_state&.name}"
|
|
35
|
+
tf_state.destroy
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def start_vm(name)
|
|
39
|
+
output = client({ 'name' => name, 'power_state' => 'on' }).run('create')
|
|
40
|
+
output['vm']['power_state'] == 'on'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop_vm(name)
|
|
44
|
+
output = client({ 'name' => name, 'power_state' => 'off' }).run('create')
|
|
45
|
+
output['vm']['power_state'] == 'off'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def save_vm(uuid, attrs)
|
|
49
|
+
tf_state = TfState.find_by(uuid: uuid)
|
|
50
|
+
raise StandardError, "VM with UUID #{uuid} does not exist" unless tf_state
|
|
51
|
+
vm_command_errors('update vm') do
|
|
52
|
+
attrs = attrs.empty? ? {} : attrs.first
|
|
53
|
+
data = client({ 'name' => tf_state.name }.merge(attrs)).run('create')
|
|
54
|
+
ComputeVM.new(self, data)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_connection(options = {})
|
|
59
|
+
super
|
|
60
|
+
begin
|
|
61
|
+
client.run('test_connection')
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Rails.logger.error("OpenTofu test connection failed: #{e.message}")
|
|
64
|
+
errors.add(:base, e.message)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def vm_command_errors(method_name)
|
|
71
|
+
yield
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
Foreman::Logging.exception("Caught #{provider} error", e)
|
|
74
|
+
raise ::Foreman::WrappedException.new(
|
|
75
|
+
e,
|
|
76
|
+
N_(
|
|
77
|
+
"Foreman could not find a required %<provider>s resource in #{method_name}. " \
|
|
78
|
+
'Check if Foreman has the required permissions and the resource exists. Reason: %<error>s'
|
|
79
|
+
),
|
|
80
|
+
{ provider: provider, error: e.message }
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def client(args = {})
|
|
85
|
+
OpentofuExecuter.new(self, args)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is part of ForemanOpentofu.
|
|
4
|
+
|
|
5
|
+
# ForemanOpentofu is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
# ForemanOpentofu is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with ForemanOpentofu. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
module ForemanOpentofu
|
|
19
|
+
class Tofu < ComputeResource
|
|
20
|
+
include OpentofuVMCommands
|
|
21
|
+
validates :provider, presence: true, inclusion: { in: %w[Tofu] }
|
|
22
|
+
validates :url, presence: true
|
|
23
|
+
validates :user, presence: true
|
|
24
|
+
validates :password, presence: true
|
|
25
|
+
|
|
26
|
+
# alias_attribute :username, :user
|
|
27
|
+
# alias_attribute :endpoint, :url
|
|
28
|
+
|
|
29
|
+
delegate :available_attributes, to: :tofu_provider
|
|
30
|
+
|
|
31
|
+
def provided_attributes
|
|
32
|
+
super.merge(
|
|
33
|
+
mac: :mac
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def opentofu_provider
|
|
38
|
+
attrs[:opentofu_provider]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def opentofu_provider=(value)
|
|
42
|
+
attrs[:opentofu_provider] = value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.provider_friendly_name
|
|
46
|
+
'OpenTofu'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def capabilities
|
|
50
|
+
[:build]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.model_name
|
|
54
|
+
ComputeResource.model_name
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def default_attributes
|
|
58
|
+
{}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def supports_update?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def tofu_provider
|
|
66
|
+
ProviderTypeManager.find(opentofu_provider)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def new_interface
|
|
70
|
+
{ compute_attributes: {} }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def editable_network_interfaces?
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Deface::Override.new(
|
|
2
|
+
virtual_path: 'compute_resources/show',
|
|
3
|
+
name: 'remove_virtual_machines_tab',
|
|
4
|
+
replace: "li:has(a[href='#vms'])",
|
|
5
|
+
text: '<% if @compute_resource.class != ForemanOpentofu::Tofu %><li><a href="#vms" data-toggle="tab"><%= _("Virtual Machines") %></a></li> <% end %>',
|
|
6
|
+
original: '15da4ffe56b9d3155f0d037ddffb7653479ee0c8',
|
|
7
|
+
namespaced: true
|
|
8
|
+
)
|