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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +134 -0
  4. data/Rakefile +30 -0
  5. data/app/controllers/api/v2/tf_states_controller.rb +52 -0
  6. data/app/controllers/concerns/foreman_opentofu/compute_resources_vms_controller.rb +17 -0
  7. data/app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb +42 -0
  8. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +89 -0
  9. data/app/models/concerns/orchestration/tofu/compute.rb +24 -0
  10. data/app/models/foreman_opentofu/compute_vm.rb +99 -0
  11. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +88 -0
  12. data/app/models/foreman_opentofu/tf_state.rb +8 -0
  13. data/app/models/foreman_opentofu/tofu.rb +77 -0
  14. data/app/overrides/compute_resources/remove_virtual_machines_tab.rb +8 -0
  15. data/app/services/foreman_opentofu/app_wrapper.rb +107 -0
  16. data/app/services/foreman_opentofu/compute_fetcher.rb +51 -0
  17. data/app/services/foreman_opentofu/opentofu_executer.rb +78 -0
  18. data/app/services/foreman_opentofu/provider_type.rb +30 -0
  19. data/app/services/foreman_opentofu/provider_type_manager.rb +36 -0
  20. data/app/views/compute_resources/form/_tofu.html.erb +9 -0
  21. data/app/views/compute_resources/show/_tofu.html.erb +8 -0
  22. data/app/views/compute_resources_vms/form/tofu/_base.html.erb +13 -0
  23. data/app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb +18 -0
  24. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +5 -0
  25. data/app/views/compute_resources_vms/index/_tofu.html.erb +0 -0
  26. data/app/views/templates/provisioning/nutanix_provision_host.erb +51 -0
  27. data/app/views/templates/provisioning/ovirt_provision_host.erb +68 -0
  28. data/config/initializers/compute_attrs.rb +19 -0
  29. data/config/nutanix.json +27 -0
  30. data/config/ovirt.json +18 -0
  31. data/config/routes.rb +13 -0
  32. data/db/migrate/20250625192757_create_tf_state.foreman_opentofu.rb +10 -0
  33. data/db/seeds.d/71_provisioning_templates.rb +9 -0
  34. data/lib/foreman_opentofu/engine.rb +75 -0
  35. data/lib/foreman_opentofu/provider_types/nutanix.rb +6 -0
  36. data/lib/foreman_opentofu/provider_types/ovirt.rb +2 -0
  37. data/lib/foreman_opentofu/provider_types.rb +3 -0
  38. data/lib/foreman_opentofu/version.rb +3 -0
  39. data/lib/foreman_opentofu.rb +6 -0
  40. data/lib/tasks/foreman_opentofu_tasks.rake +30 -0
  41. data/locale/Makefile +73 -0
  42. data/locale/en/foreman_opentofu.po +19 -0
  43. data/locale/foreman_opentofu.pot +19 -0
  44. data/locale/gemspec.rb +2 -0
  45. data/test/controllers/api/v2/tf_states_controller_test.rb +67 -0
  46. data/test/factories/compute_resources.rb +26 -0
  47. data/test/factories/foreman_opentofu_factories.rb +7 -0
  48. data/test/factories/tf_state_factories.rb +7 -0
  49. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +147 -0
  50. data/test/models/foreman_opentofu_test.rb +43 -0
  51. data/test/services/app_wrapper_test.rb +31 -0
  52. data/test/services/opentofu_executer_test.rb +62 -0
  53. data/test/test_plugin_helper.rb +6 -0
  54. metadata +119 -0
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ [![Ruby Tests](https://github.com/ATIX-AG/foreman_opentofu/actions/workflows/ruby.yml/badge.svg)](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,8 @@
1
+ module ForemanOpentofu
2
+ class TfState < ApplicationRecord
3
+ # DO we need this name or change it to foreman_opentofu_tf_states
4
+ self.table_name = 'tf_states'
5
+
6
+ validates :name, presence: true, uniqueness: true
7
+ end
8
+ 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
+ )