foreman_opentofu 0.0.1 → 0.0.2

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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -0
  3. data/app/assets/javascripts/foreman_opentofu/locale/en/foreman_opentofu.js +58 -0
  4. data/app/controllers/api/v2/tf_states_controller.rb +43 -5
  5. data/app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb +1 -19
  6. data/app/models/foreman_opentofu/tofu.rb +15 -1
  7. data/app/models/foreman_opentofu/token.rb +16 -0
  8. data/app/services/foreman_opentofu/opentofu_executer.rb +26 -11
  9. data/app/services/foreman_opentofu/provider_type.rb +1 -1
  10. data/app/views/compute_resources/form/_tofu.html.erb +2 -1
  11. data/app/views/compute_resources/show/_tofu.html.erb +4 -0
  12. data/app/views/templates/provisioning/{nutanix_provision_host.erb → nutanix_provision_default.erb} +8 -11
  13. data/app/views/templates/provisioning/{ovirt_provision_host.erb → ovirt_provision_default.erb} +9 -12
  14. data/db/migrate/20260127160904_add_table_token.foreman_opentofu.rb +10 -0
  15. data/db/seeds.d/71_provisioning_templates.rb +9 -1
  16. data/lib/foreman_opentofu/engine.rb +8 -24
  17. data/lib/foreman_opentofu/version.rb +1 -1
  18. data/lib/tasks/foreman_opentofu_tasks.rake +2 -2
  19. data/locale/en/LC_MESSAGES/foreman_opentofu.mo +0 -0
  20. data/locale/en/foreman_opentofu.po +44 -9
  21. data/locale/foreman_opentofu.pot +56 -8
  22. data/test/controllers/api/v2/tf_states_controller_test.rb +33 -8
  23. data/test/factories/token.rb +10 -0
  24. data/test/models/foreman_opentofu/tofu_test.rb +46 -0
  25. data/test/models/foreman_opentofu/token_test.rb +36 -0
  26. data/test/services/foreman_opentofu/provider_type_test.rb +68 -0
  27. data/test/services/opentofu_executer_test.rb +1 -1
  28. metadata +17 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1a821d0e6f43b76c17c48f1a84cb7bc753f9bbebd6c31c532fea9fcf6cf843b
4
- data.tar.gz: 515d5f582f9911cbdaf339b9854baf644437171695f4a7062ac44eb2eb203105
3
+ metadata.gz: f6bff811eebb374eb3983de89c83faf991c8021e416c677bbfda90837cbeeb5a
4
+ data.tar.gz: 8741e8b977ba0a078c13ac4642a790e11646f719358f22b69c299368d698a8f0
5
5
  SHA512:
6
- metadata.gz: 3d0b019416cc34fd4f9c17ca83c08ef7f5861ea88d9d50905508a878be1e05ad2c7a998dacd8a58209eb494845923843015820c1acb20227e6f59b40b15d92c6
7
- data.tar.gz: 6286eb806d3b5afca40e28e6d064512b5477b698eb531ab1b4063d18d66ae4b5644536372511c40d7cccac8e0d26ff534625c3dd242c6d19534a949796ed50c6
6
+ metadata.gz: 67e87113d04444061daa977fca5721d26122035fd821f86284667eb5ad247c7a2002d5bd1d451ec0d4cea84ade9f530eb1dcffa3afb6b931b7a0b78f1f57c5ca
7
+ data.tar.gz: d2627a46149453ae41fa6f5463bd792501ee51bc0760693a6bafdd3f0da7357ab9e8b8093c98149b5c4b4c7b2e8e03698c8d97996bb1ec3d1ac0990bc719a455
data/README.md CHANGED
@@ -34,6 +34,91 @@ Provisioning workflow:
34
34
 
35
35
  Provider-specific details (for example Nutanix, Hetzner) are handled entirely through openTOFU scripts.
36
36
 
37
+ ### Create new ProviderType
38
+
39
+ This Plugin empowers you to add support of a new backend VM- or Cloud-Platform yourself.
40
+ Follow these simple steps to do so:
41
+
42
+ #### Find OpenTofu Provider
43
+
44
+ Visit the OpenTofu Registry to find a suitable [provider supported by OpenTofu](https://search.opentofu.org/providers).
45
+ The Registry supplies the necessary Data Sources to read information from the Backend as well as Resources to create/update/destroy resources on the Backend.
46
+
47
+ #### Create Template
48
+
49
+ You may use the UI-Editor in Hosts -> Templates -> Provisioning Templates to create a new Template.
50
+ Either clone a pre-installed template or create one from scratch.
51
+ In the latter case be sure to select the correct Template Type: OpenTofu Script template.
52
+
53
+ #### Create Parameter Config
54
+
55
+ To define which Virtual Machine parameters can be set for a new Host a new config file under `/config` must be added.
56
+ Feel free to use either YAML or JSON (be sure to end the filename with `.json` or `.yaml`).
57
+ The config file defines an array of dicts, where each dict represents a configuration-parameter.
58
+
59
+ A config-parameter has the following values:
60
+
61
+ * `name`: the OpenTofu Provider Resource Arguments as stated on the OpenTofu Registry
62
+ * `label`: the label shown in the Foreman UI
63
+ * `type`: data-type of the value, supported values:
64
+ * `string`
65
+ * `number`
66
+ * `bool`
67
+ * `select`: requires setting `options`
68
+ * `help`: Tooltip describing what that value does and what values are allowed
69
+ * `mandatory`: `true`/`false` defines if omitting the value triggers an error
70
+ * `options`: array of strings representing the possible values
71
+ * `group`: define where the value should be configured
72
+ * `vm`: ones per Host in the 'Virtual Machine' tab,
73
+ * `disk`: for each defined disk/volume in the 'Virtual Machine' tab
74
+ * `nic`: for each defined network-interface on the 'Interfaces' tab
75
+
76
+ A short config file might look like this:
77
+
78
+ ```json
79
+ [
80
+ { "name": "memory_size_mib", "type": "number", "group": "vm", "mandatory": false,
81
+ "label": "Memory (MB)" },
82
+ { "name": "boot_type", "type": "select", "group": "vm", "mandatory": false,
83
+ "label": "Firmware", "options": [ "UEFI", "LEGACY", "SECURE_BOOT" ] },
84
+ { "name": "disk_size_mib", "type": "number", "group": "disk", "mandatory": true,
85
+ "label": "Size (MB)" },
86
+ { "name": "model", "type": "select", "group": "nic", "mandatory": true,
87
+ "options": [ "VIRTIO", "E1000" ] }
88
+ ]
89
+ ```
90
+
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
+
93
+ #### Create Provider Type
94
+
95
+ 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/`.
96
+
97
+ A very simple ProviderType file to add a new Provider named `nutanix` has to be located in `lib/foreman_opentofu/provider_types/nutanix.rb` and might look like this:
98
+
99
+ ```ruby
100
+ ForemanOpentofu::ProviderTypeManager.register('nutanix') do
101
+ end
102
+ ```
103
+
104
+ Additional informations about the ProviderType can be set within the `register`-block:
105
+
106
+ ##### `default_attributes`
107
+
108
+ Define values that should be set as default for attributes.
109
+ The values do not have to be defined in the config-file.
110
+ If attributes are also defined in the config-file and therefore set during Host creation, the default\_attribute values will be overwritten.
111
+
112
+ ```ruby
113
+ ForemanOpentofu::ProviderTypeManager.register('nutanix') do
114
+ @default_attributes = {
115
+ 'enable_cpu_passthrough' => true,
116
+ 'num_threads_per_core' => 2,
117
+ }
118
+ end
119
+ ```
120
+
121
+
37
122
  ## Development
38
123
 
39
124
  ### Dev prerequisites
@@ -0,0 +1,58 @@
1
+ locales['foreman_opentofu'] = locales['foreman_opentofu'] || {}; locales['foreman_opentofu']['en'] = {
2
+ "domain": "foreman_opentofu",
3
+ "locale_data": {
4
+ "foreman_opentofu": {
5
+ "": {
6
+ "Project-Id-Version": "foreman_opentofu 1.0.0",
7
+ "Report-Msgid-Bugs-To": "",
8
+ "PO-Revision-Date": "2026-02-16 18:46+0000",
9
+ "Last-Translator": "FULL NAME <EMAIL@ADDRESS>",
10
+ "Language-Team": "LANGUAGE <LL@li.org>",
11
+ "Language": "",
12
+ "MIME-Version": "1.0",
13
+ "Content-Type": "text/plain; charset=UTF-8",
14
+ "Content-Transfer-Encoding": "8bit",
15
+ "Plural-Forms": "nplurals=INTEGER; plural=EXPRESSION;",
16
+ "lang": "en",
17
+ "domain": "foreman_opentofu",
18
+ "plural_forms": "nplurals=INTEGER; plural=EXPRESSION;"
19
+ },
20
+ "127.0.0.1": [
21
+ ""
22
+ ],
23
+ "Common fields": [
24
+ ""
25
+ ],
26
+ "OpenTofu Provider": [
27
+ ""
28
+ ],
29
+ "OpenTofu Script template": [
30
+ ""
31
+ ],
32
+ "OpenTofu Template": [
33
+ ""
34
+ ],
35
+ "Storage": [
36
+ ""
37
+ ],
38
+ "TODO: Description of ForemanPluginTemplate.": [
39
+ ""
40
+ ],
41
+ "URL": [
42
+ ""
43
+ ],
44
+ "Unable to find template specified by %s setting": [
45
+ ""
46
+ ],
47
+ "Unable to render provisioning template": [
48
+ ""
49
+ ],
50
+ "VM Config": [
51
+ ""
52
+ ],
53
+ "e.g. admin": [
54
+ ""
55
+ ]
56
+ }
57
+ }
58
+ };
@@ -3,14 +3,25 @@ module Api
3
3
  class TfStatesController < ::Api::V2::BaseController
4
4
  include ::Api::Version2
5
5
 
6
+ # TODO: verify this
7
+ # We don't require any of these methods for provisioning
8
+ # skip_before_action :require_login, :check_user_enabled, :session_expiry, :update_activity_time, :set_taxonomy, :authorize, unless: -> { preview? }
9
+ skip_before_action :set_taxonomy
10
+
11
+ # Allow HTTP POST methods without CSRF
12
+ skip_before_action :verify_authenticity_token
13
+
14
+ # overwrite authorize with the local token-based authorization
15
+ before_action :authorize, except: [:destroy]
16
+
6
17
  resource_description do
7
18
  api_version 'v2'
8
19
  api_base_url '/foreman_opentofu/api'
9
20
  end
10
21
 
11
- skip_before_action :verify_authenticity_token
12
22
  def show
13
23
  state = ForemanOpentofu::TfState.find_by(name: params[:name])
24
+
14
25
  if state
15
26
  render plain: state.state, content_type: 'application/json'
16
27
  else
@@ -19,8 +30,6 @@ module Api
19
30
  end
20
31
 
21
32
  def create
22
- state = ForemanOpentofu::TfState.find_or_create_by(name: params[:name])
23
-
24
33
  raw_state = request.body.read
25
34
  if raw_state.blank?
26
35
  render plain: 'Missing state body', status: :unprocessable_entity
@@ -29,6 +38,7 @@ module Api
29
38
  begin
30
39
  JSON.parse(raw_state)
31
40
 
41
+ state = ForemanOpentofu::TfState.find_or_initialize_by(name: params[:name])
32
42
  state.state = raw_state
33
43
  state.save!
34
44
  render plain: '', status: :ok
@@ -39,14 +49,42 @@ module Api
39
49
  end
40
50
 
41
51
  def destroy
42
- state = ForemanOpentofu::TfState.find_by(name: params[:name])
43
- state&.destroy
52
+ # TODO: at the moment we want to get 200 OK, if the TfState does not exist.
53
+ # normally, this would fail with 401, because of no valid token.
54
+ # Needs re-evaluation, if this is a security risk.
55
+ state = ForemanOpentofu::TfState.where(name: params[:name])
56
+ if state.any?
57
+ authorize
58
+ state.first.destroy
59
+ end
60
+
44
61
  render plain: '', status: :ok
45
62
  end
46
63
 
47
64
  def resource_class
48
65
  @resource_class ||= ForemanOpentofu::TfState
49
66
  end
67
+
68
+ private
69
+
70
+ def authorize
71
+ authenticate_or_request_with_http_token do |token, _options|
72
+ token = ForemanOpentofu::Token.find_by(name: params[:name], token: token)
73
+ unless token
74
+ Rails.logger.warn('TfState-Auth-Token not found')
75
+ render_error('unauthorized', status: :unauthorized)
76
+ return false
77
+ end
78
+
79
+ if token.expired?
80
+ Rails.logger.warn 'TfState token expired, if this keeps happening increase the validity of the token'
81
+ render_error('unauthorized', status: :unauthorized)
82
+ return false
83
+ end
84
+
85
+ return true
86
+ end
87
+ end
50
88
  end
51
89
  end
52
90
  end
@@ -1,22 +1,3 @@
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
1
  module ForemanOpentofu
21
2
  module Controller
22
3
  module Parameters
@@ -28,6 +9,7 @@ module ForemanOpentofu
28
9
  super.tap do |filter|
29
10
  filter.permit :endpoint,
30
11
  :opentofu_provider,
12
+ :opentofu_template_id,
31
13
  :username
32
14
  end
33
15
  end
@@ -42,6 +42,20 @@ module ForemanOpentofu
42
42
  attrs[:opentofu_provider] = value
43
43
  end
44
44
 
45
+ def opentofu_template
46
+ return ProvisioningTemplate.find(attrs[:opentofu_template_id]) if attrs.key? :opentofu_template_id
47
+ # or default-template for this opentofu_provider
48
+ nil
49
+ end
50
+
51
+ def opentofu_template_id
52
+ attrs[:opentofu_template_id]
53
+ end
54
+
55
+ def opentofu_template_id=(value)
56
+ attrs[:opentofu_template_id] = value
57
+ end
58
+
45
59
  def self.provider_friendly_name
46
60
  'OpenTofu'
47
61
  end
@@ -55,7 +69,7 @@ module ForemanOpentofu
55
69
  end
56
70
 
57
71
  def default_attributes
58
- {}
72
+ tofu_provider&.default_attributes || {}
59
73
  end
60
74
 
61
75
  def supports_update?
@@ -0,0 +1,16 @@
1
+ module ForemanOpentofu
2
+ class Token < ApplicationRecord
3
+ validates :name, presence: true, uniqueness: true
4
+
5
+ def expired?
6
+ !expire.respond_to?(:<=) || (expire <= Time.current)
7
+ end
8
+
9
+ def generate
10
+ self.expire = Time.current + Setting[:tfstate_token_timeout]
11
+ self.token = SecureRandom.alphanumeric(255)
12
+ save!
13
+ token
14
+ end
15
+ end
16
+ end
@@ -5,20 +5,42 @@ module ForemanOpentofu
5
5
  def initialize(*args)
6
6
  @compute_resource = args[0]
7
7
  @cr_attrs = args[1] || {}
8
+ @host_name = @cr_attrs['name'] || 'test'
8
9
  end
9
10
 
10
11
  def run(mode = 'create')
11
12
  Dir.mktmpdir('opentofu_') do |dir|
12
13
  tofu = AppWrapper.new(dir)
13
14
  @use_backend = %w[create destroy output].include?(mode)
15
+ @token = create_token(@host_name) if @use_backend
14
16
  tofu.main_configuration = render_template
15
17
  tofu.init
16
18
  run_mode(tofu, mode)
17
19
  end
18
20
  end
19
21
 
22
+ # creates a new authentication token for the TfState API-controller
23
+ # needed for tofu command to send it's state-file to the database.
24
+ # returns the created token
25
+ def create_token(host_name)
26
+ new_token = nil
27
+ # This construct makes sure the token is created outside of the current transaction
28
+ # which is necessary for the API-controller to check the token, while the current transaction still runs
29
+ # see https://stackoverflow.com/a/11675647
30
+ Thread.new do
31
+ ActiveRecord::Base.connection_pool.with_connection do
32
+ token = ForemanOpentofu::Token.find_or_create_by(name: host_name)
33
+ new_token = if token.expired?
34
+ token.generate
35
+ else
36
+ token.token
37
+ end
38
+ end
39
+ end.join
40
+ new_token
41
+ end
42
+
20
43
  def run_mode(tofu, mode = 'new')
21
- @use_backend = true
22
44
  case mode
23
45
  when 'new'
24
46
  tofu.plan
@@ -49,6 +71,8 @@ module ForemanOpentofu
49
71
  scope.instance_variable_set(:@compute_resource, @compute_resource)
50
72
  scope.instance_variable_set(:@cr_attrs, @cr_attrs) if @cr_attrs
51
73
  scope.instance_variable_set(:@use_backend, @use_backend)
74
+ scope.instance_variable_set(:@token, @token) if @use_backend
75
+ scope.instance_variable_set(:@host_name, @host_name)
52
76
  rendered_template = Foreman::Renderer::UnsafeModeRenderer.render(source, scope)
53
77
  raise ::Foreman::Exception, N_('Unable to render provisioning template') unless rendered_template
54
78
 
@@ -56,16 +80,7 @@ module ForemanOpentofu
56
80
  end
57
81
 
58
82
  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
83
+ name = @compute_resource.opentofu_template.name
69
84
  template = ProvisioningTemplate.unscoped.find_by(name: name)
70
85
  unless template
71
86
  raise ::Foreman::Exception.new(N_('Unable to find template specified by %s setting'),
@@ -1,6 +1,6 @@
1
1
  module ForemanOpentofu
2
2
  class ProviderType
3
- attr_reader :id, :name
3
+ attr_reader :id, :name, :default_attributes
4
4
 
5
5
  def initialize(id)
6
6
  @id = id.to_sym
@@ -1,8 +1,9 @@
1
1
  <%= field_set_tag _("Common fields"), :id => "compute_ressource_common_field_set" do %>
2
2
  <%= select_f f, :opentofu_provider, ForemanOpentofu::ProviderTypeManager.enabled_provider_types, :id, :name %>
3
+ <%= select_f f, :opentofu_template_id, ProvisioningTemplate.of_kind('opentofu_script'), :id, :name %>
3
4
  <%= text_f f, :url, :help_block => _("127.0.0.1") %>
4
5
  <%= text_f f, :user , :help_block => _("e.g. admin") %>
5
- <%= password_f f, :password %>
6
+ <%= password_f f, :password, :unset => unset_password? %>
6
7
  <% end %>
7
8
  <div class="col-md-offset-2">
8
9
  <%= test_connection_button_f(f, (true)) %>
@@ -2,6 +2,10 @@
2
2
  <td><%= _('OpenTofu Provider') %></td>
3
3
  <td><%= @compute_resource.opentofu_provider %></td>
4
4
  </tr>
5
+ <tr>
6
+ <td><%= _('OpenTofu Template') %></td>
7
+ <td><%= link_to @compute_resource.opentofu_template, edit_provisioning_template_path(@compute_resource.opentofu_template) %></td>
8
+ </tr>
5
9
  <tr>
6
10
  <td><%= _('URL') %></td>
7
11
  <td><%= @compute_resource.url %></td>
@@ -1,14 +1,10 @@
1
- #!/bin/bash
2
1
  <%#
3
- kind: script
4
- name: Nutanix provision - host
2
+ kind: opentofu_script
3
+ name: Nutanix provision default
5
4
  model: ProvisioningTemplate
6
5
  description: |
7
- nutanix opentofu script to create vm resource
6
+ Nutanix OpenTofu script to create vm resource
8
7
  -%>
9
- <%-
10
- host_name = @cr_attrs['name'] || 'test'
11
- %>
12
8
 
13
9
  terraform {
14
10
  required_providers {
@@ -18,9 +14,10 @@ terraform {
18
14
  }
19
15
  <% if @use_backend %>
20
16
  backend "http" {
21
- address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= host_name %>"
22
- username = "admin"
23
- password = "changeme"
17
+ address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= @host_name %>"
18
+ headers = {
19
+ Authorization = "Token <%= @token %>"
20
+ }
24
21
  }
25
22
  <% end %>
26
23
  }
@@ -36,7 +33,7 @@ data "nutanix_clusters" "clusters" {}
36
33
 
37
34
  resource "nutanix_virtual_machine" "vm" {
38
35
  cluster_uuid = data.nutanix_clusters.clusters.entities.0.metadata.uuid
39
- name = "<%= host_name %>"
36
+ name = "<%= @host_name %>"
40
37
 
41
38
  <%= vm_attributes(['name', 'cluster_uuid']) %>
42
39
 
@@ -1,14 +1,10 @@
1
- #!/bin/bash
2
1
  <%#
3
- kind: script
4
- name: Ovirt provision - host
2
+ kind: opentofu_script
3
+ name: oVirt provision default
5
4
  model: ProvisioningTemplate
6
5
  description: |
7
- nutanix opentofu script to create vm resource
6
+ oVirt OpenTofu script to create vm resource
8
7
  -%>
9
- <%-
10
- host_name = @cr_attrs['name'] || 'test'
11
- %>
12
8
 
13
9
  terraform {
14
10
  required_providers {
@@ -18,9 +14,10 @@ terraform {
18
14
  }
19
15
  <% if @use_backend %>
20
16
  backend "http" {
21
- address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= host_name %>"
22
- username = "admin"
23
- password = "changeme"
17
+ address = "<%= Setting[:foreman_url] %>/api/v2/tf_states/<%= @host_name %>"
18
+ headers = {
19
+ Authorization = "Token <%= @token %>"
20
+ }
24
21
  }
25
22
  <% end %>
26
23
  }
@@ -36,7 +33,7 @@ data "ovirt_blank_template" "blank" {
36
33
  }
37
34
 
38
35
  resource "ovirt_vm" "vm" {
39
- name = "<%= host_name %>"
36
+ name = "<%= @host_name %>"
40
37
  cluster_id = "2ad616ea-4b66-11f0-964d-901b0ecbd584"
41
38
  template_id = data.ovirt_blank_template.blank.id
42
39
  cpu_cores = <%= @cr_attrs['cpu_cores'] || 1 %>
@@ -47,7 +44,7 @@ resource "ovirt_vm" "vm" {
47
44
  memory_ballooning = <%= @cr_attrs['memory_ballooning'] || false %>
48
45
  }
49
46
  resource "ovirt_vm" "vm" {
50
- name = "<%= host_name %>"
47
+ name = "<%= @host_name %>"
51
48
  cluster_id = "2ad616ea-4b66-11f0-964d-901b0ecbd584"
52
49
  template_id = data.ovirt_blank_template.blank.id
53
50
  <%= "cpu_cores = #{@cr_attrs['cpu_cores'] || 1}" %>
@@ -0,0 +1,10 @@
1
+ class AddTableToken < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :foreman_opentofu_tokens do |t|
4
+ t.column :name, :string, limit: 255, index: true
5
+ t.column :token, :string, limit: 512, index: true
6
+ t.column :expire, :datetime
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -2,8 +2,16 @@
2
2
 
3
3
  User.as_anonymous_admin do
4
4
  ProvisioningTemplate.without_auditing do
5
+ # Create TemplateKind
6
+ Foreman::Plugin.find(:foreman_opentofu).get_template_labels.keys.map(&:to_sym).each do |type|
7
+ kind ||= TemplateKind.unscoped.find_or_create_by(name: type)
8
+ # kind.description = TemplateKind.default_template_descriptions[kind.name]
9
+ kind.save!
10
+ end
11
+
5
12
  SeedHelper.import_templates(
6
- Dir[File.join("#{ForemanOpentofu::Engine.root}/app/views/templates/provisioning/**/*.erb")]
13
+ Dir[File.join("#{ForemanOpentofu::Engine.root}/app/views/templates/provisioning/**/*.erb")],
14
+ 'ForemanOpentofu'
7
15
  )
8
16
  end
9
17
  end
@@ -22,32 +22,16 @@ module ForemanOpentofu
22
22
  # Add Global files for extending foreman-core components and routes
23
23
  # Register Nutanix compute resource in foreman
24
24
  compute_resource ForemanOpentofu::Tofu
25
+
26
+ template_labels 'opentofu_script' => N_('OpenTofu Script template')
27
+
25
28
  settings do
26
29
  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')
30
+ setting 'tfstate_token_timeout',
31
+ type: :integer,
32
+ default: 600,
33
+ full_name: N_('TfState Token Timeout'),
34
+ description: N_('Number of seconds a run of Opentofu command is allowed to report tf-state back to the plugin.')
51
35
  end
52
36
  end
53
37
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanOpentofu
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.0.2'.freeze
3
3
  end
@@ -7,8 +7,8 @@ namespace :foreman_opentofu do
7
7
  # details are handled in .rubocop.yml
8
8
  task.patterns = [ForemanOpentofu::Engine.root.to_s]
9
9
  end
10
- rescue LoadError => e
11
- raise e unless Rails.env.production?
10
+ rescue LoadError
11
+ # Rubocop not loaded
12
12
  end
13
13
 
14
14
  # Tests
@@ -1,19 +1,54 @@
1
- # foreman_opentofu
2
- #
3
- # This file is distributed under the same license as foreman_opentofu.
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the foreman_opentofu package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4
5
  #
5
6
  #, fuzzy
6
7
  msgid ""
7
8
  msgstr ""
8
- "Project-Id-Version: version 0.0.1\n"
9
+ "Project-Id-Version: foreman_opentofu 1.0.0\n"
9
10
  "Report-Msgid-Bugs-To: \n"
10
- "POT-Creation-Date: 2014-08-20 08:46+0100\n"
11
- "PO-Revision-Date: 2014-08-20 08:54+0100\n"
12
- "Last-Translator: Foreman Team <foreman-dev@googlegroups.com>\n"
13
- "Language-Team: Foreman Team <foreman-dev@googlegroups.com>\n"
11
+ "PO-Revision-Date: 2026-02-16 18:46+0000\n"
12
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
+ "Language-Team: LANGUAGE <LL@li.org>\n"
14
14
  "Language: \n"
15
15
  "MIME-Version: 1.0\n"
16
16
  "Content-Type: text/plain; charset=UTF-8\n"
17
17
  "Content-Transfer-Encoding: 8bit\n"
18
- "Plural-Forms: nplurals=2; plural=(n != 1);\n"
18
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
19
+
20
+ msgid "127.0.0.1"
21
+ msgstr ""
22
+
23
+ msgid "Common fields"
24
+ msgstr ""
25
+
26
+ msgid "OpenTofu Provider"
27
+ msgstr ""
28
+
29
+ msgid "OpenTofu Script template"
30
+ msgstr ""
31
+
32
+ msgid "OpenTofu Template"
33
+ msgstr ""
34
+
35
+ msgid "Storage"
36
+ msgstr ""
37
+
38
+ msgid "TODO: Description of ForemanPluginTemplate."
39
+ msgstr ""
40
+
41
+ msgid "URL"
42
+ msgstr ""
43
+
44
+ msgid "Unable to find template specified by %s setting"
45
+ msgstr ""
46
+
47
+ msgid "Unable to render provisioning template"
48
+ msgstr ""
19
49
 
50
+ msgid "VM Config"
51
+ msgstr ""
52
+
53
+ msgid "e.g. admin"
54
+ msgstr ""
@@ -1,19 +1,67 @@
1
- # foreman_opentofu
2
- #
3
- # This file is distributed under the same license as foreman_opentofu.
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the foreman_opentofu package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
4
5
  #
5
6
  #, fuzzy
6
7
  msgid ""
7
8
  msgstr ""
8
- "Project-Id-Version: version 0.0.1\n"
9
+ "Project-Id-Version: foreman_opentofu 1.0.0\n"
9
10
  "Report-Msgid-Bugs-To: \n"
10
- "POT-Creation-Date: 2014-08-20 08:46+0100\n"
11
- "PO-Revision-Date: 2014-08-20 08:46+0100\n"
12
- "Last-Translator: Foreman Team <foreman-dev@googlegroups.com>\n"
13
- "Language-Team: Foreman Team <foreman-dev@googlegroups.com>\n"
11
+ "POT-Creation-Date: 2026-02-16 18:46+0000\n"
12
+ "PO-Revision-Date: 2026-02-16 18:46+0000\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
14
15
  "Language: \n"
15
16
  "MIME-Version: 1.0\n"
16
17
  "Content-Type: text/plain; charset=UTF-8\n"
17
18
  "Content-Transfer-Encoding: 8bit\n"
18
19
  "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
19
20
 
21
+ #: ../app/services/foreman_opentofu/opentofu_executer.rb:53
22
+ msgid "Unable to render provisioning template"
23
+ msgstr ""
24
+
25
+ #: ../app/services/foreman_opentofu/opentofu_executer.rb:62
26
+ msgid "Unable to find template specified by %s setting"
27
+ msgstr ""
28
+
29
+ #: ../app/views/compute_resources/form/_tofu.html.erb:1
30
+ msgid "Common fields"
31
+ msgstr ""
32
+
33
+ #: ../app/views/compute_resources/form/_tofu.html.erb:4
34
+ msgid "127.0.0.1"
35
+ msgstr ""
36
+
37
+ #: ../app/views/compute_resources/form/_tofu.html.erb:5
38
+ msgid "e.g. admin"
39
+ msgstr ""
40
+
41
+ #: ../app/views/compute_resources/show/_tofu.html.erb:2
42
+ msgid "OpenTofu Provider"
43
+ msgstr ""
44
+
45
+ #: ../app/views/compute_resources/show/_tofu.html.erb:6
46
+ msgid "OpenTofu Template"
47
+ msgstr ""
48
+
49
+ #: ../app/views/compute_resources/show/_tofu.html.erb:10
50
+ msgid "URL"
51
+ msgstr ""
52
+
53
+ #: ../app/views/compute_resources_vms/form/tofu/_base.html.erb:5
54
+ msgid "VM Config"
55
+ msgstr ""
56
+
57
+ #: ../app/views/compute_resources_vms/form/tofu/_base.html.erb:9
58
+ msgid "Storage"
59
+ msgstr ""
60
+
61
+ #: ../lib/foreman_opentofu/engine.rb:26
62
+ msgid "OpenTofu Script template"
63
+ msgstr ""
64
+
65
+ #: gemspec.rb:2
66
+ msgid "TODO: Description of ForemanPluginTemplate."
67
+ msgstr ""
@@ -6,9 +6,14 @@ module Api
6
6
  setup do
7
7
  @tf_one = FactoryBot.create(:tf_state)
8
8
  @tf_two = FactoryBot.create(:tf_state)
9
+ @token_one = FactoryBot.create(:foreman_opentofu_token, name: @tf_one.name)
10
+ @token_two = FactoryBot.create(:foreman_opentofu_token, name: @tf_two.name)
11
+ @authorization_one = ActionController::HttpAuthentication::Token.encode_credentials(@token_one.token)
12
+ @authorization_two = ActionController::HttpAuthentication::Token.encode_credentials(@token_two.token)
9
13
  end
10
14
 
11
15
  test 'should show tf_state when present' do
16
+ request.headers['HTTP_AUTHORIZATION'] = @authorization_one
12
17
  get :show, params: { name: @tf_one.name }
13
18
 
14
19
  assert_response :success
@@ -18,19 +23,34 @@ module Api
18
23
  assert_equal 'bar', body['foo']
19
24
  end
20
25
 
21
- test 'should return 404 when tf_state is missing' do
26
+ test 'should return 401 when tf_state is missing' do
27
+ request.headers['HTTP_AUTHORIZATION'] = @authorization_one
22
28
  get :show, params: { name: 'missing-vm' }
23
29
 
24
- assert_response :not_found
25
- assert_equal '', @response.body
30
+ assert_response :unauthorized
31
+ end
32
+
33
+ test 'should return 401 when token expired' do
34
+ @token_one.expire = Time.current - 3600
35
+ @token_one.save!
36
+
37
+ assert @token_one.expired?
38
+
39
+ get :show, params: { name: @tf_one.name }
40
+
41
+ assert_response :unauthorized
42
+ assert_not_equal @tf_one.state, @response.body
26
43
  end
27
44
 
28
45
  test 'should create tf_state with valid json body' do
46
+ token = FactoryBot.create(:foreman_opentofu_token, name: 'new-vm')
47
+
29
48
  attrs = { hello: 'world' }
30
- assert_difference('ForemanOpentofu::TfState.count', 1) do
31
- @request.env['CONTENT_TYPE'] = 'application/json'
32
- post :create, params: { name: 'new-vm' }, body: attrs.to_json
33
- end
49
+ request.headers['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Token.encode_credentials(token.token)
50
+ @request.env['CONTENT_TYPE'] = 'application/json'
51
+ post :create,
52
+ params: { name: 'new-vm' },
53
+ body: attrs.to_json
34
54
 
35
55
  assert_response :success
36
56
 
@@ -40,7 +60,10 @@ module Api
40
60
  end
41
61
 
42
62
  test 'should update tf_state when already exists' do
43
- post :create, params: { name: @tf_one.name }, body: { updated: true }.to_json
63
+ request.headers['HTTP_AUTHORIZATION'] = @authorization_one
64
+ post :create,
65
+ params: { name: @tf_one.name },
66
+ body: { updated: true }.to_json
44
67
 
45
68
  assert_response :success
46
69
 
@@ -49,6 +72,7 @@ module Api
49
72
 
50
73
  test 'should destroy tf_state when present' do
51
74
  assert_difference('ForemanOpentofu::TfState.count', -1) do
75
+ request.headers['HTTP_AUTHORIZATION'] = @authorization_two
52
76
  delete :destroy, params: { name: @tf_two.name }
53
77
  end
54
78
 
@@ -57,6 +81,7 @@ module Api
57
81
 
58
82
  test 'should return ok when destroying missing tf_state' do
59
83
  assert_no_difference('ForemanOpentofu::TfState.count') do
84
+ request.headers['HTTP_AUTHORIZATION'] = @authorization_two
60
85
  delete :destroy, params: { name: 'missing-vm' }
61
86
  end
62
87
 
@@ -0,0 +1,10 @@
1
+ FactoryBot.define do
2
+ factory :foreman_opentofu_token, class: 'ForemanOpentofu::Token' do
3
+ sequence(:name) { |n| "vm-#{n}" }
4
+ sequence(:token) { |n| "secret#{n}" }
5
+ expire { Time.current + 3600 }
6
+ trait :expired do
7
+ expire { Time.current - 3600 }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,46 @@
1
+ require 'test_helper'
2
+
3
+ class TofuTest < ActiveSupport::TestCase
4
+ # FIXME: be more generic!
5
+ let(:subject) { FactoryBot.create :opentofu_nutanix_cr }
6
+
7
+ should validate_presence_of :provider
8
+ should validate_presence_of :url
9
+ should validate_presence_of :user
10
+ should validate_presence_of :password
11
+ should delegate_method(:available_attributes).to(:tofu_provider)
12
+
13
+ test 'validates provider is Tofu' do
14
+ subject.provider = 'Unknown'
15
+ assert_not subject.valid?
16
+
17
+ subject.provider = 'Tofu'
18
+ assert subject.valid?
19
+ end
20
+
21
+ test 'responds to opentofu_template' do
22
+ assert_respond_to subject, :opentofu_template
23
+ end
24
+
25
+ test 'not assigned template returns nil' do
26
+ assert_not_include subject.attrs, :opentofu_template_id
27
+
28
+ assert_nil subject.opentofu_template
29
+ end
30
+
31
+ test 'has tofu-template' do
32
+ template = FactoryBot.create(:provisioning_template) # , template_kind: FactoryBot.create(:template_kind, name: 'opentofu_script'))
33
+ subject.opentofu_template_id = template.id
34
+
35
+ assert_equal template, subject.opentofu_template
36
+ end
37
+
38
+ test 'has tofu provider' do
39
+ assert_instance_of Symbol, subject.opentofu_provider
40
+ assert_instance_of ForemanOpentofu::ProviderType, subject.tofu_provider
41
+ end
42
+
43
+ test 'delegates available_attributes to opentofu-provider' do
44
+ assert_equal subject.tofu_provider.available_attributes, subject.available_attributes
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ class TokenTest < ActiveSupport::TestCase
4
+ let(:subject) { FactoryBot.create(:foreman_opentofu_token, :expired) }
5
+
6
+ should validate_presence_of :name
7
+
8
+ test 'expires' do
9
+ assert subject.expired?
10
+
11
+ subject.expire = nil
12
+ assert subject.expired?
13
+
14
+ subject.expire = ''
15
+ assert subject.expired?
16
+
17
+ subject.expire = Time.zone.now
18
+ assert subject.expired?
19
+ end
20
+
21
+ test 'generates new token' do
22
+ old_token = subject.token
23
+
24
+ subject.generate
25
+ assert_not_equal old_token, subject.token
26
+ assert_not_empty subject.token
27
+
28
+ subject.save!
29
+ assert_not_equal old_token, subject.reload.token
30
+ end
31
+
32
+ test 'generates valid token' do
33
+ subject.generate
34
+ assert_not subject.expired?
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ module ForemanOpentofu
2
+ class ProviderTypeTest < ActiveSupport::TestCase
3
+ # FIXME: use a non-existing ProviderType and stub the CR_ATTRS instead
4
+ let(:provider_type) { ProviderType.new('nutanix') }
5
+
6
+ test 'has name' do
7
+ assert_not_empty provider_type.name
8
+ end
9
+
10
+ test 'has attributes' do
11
+ assert provider_type.attributes?
12
+ end
13
+
14
+ test 'finds all attributes' do
15
+ attributes = provider_type.attributes
16
+
17
+ assert_not_empty attributes
18
+ assert_not_empty(attributes.select { |a| a['group'] == 'vm' })
19
+ assert_not_empty(attributes.select { |a| a['group'] == 'nic' })
20
+ assert_not_empty(attributes.select { |a| a['group'] == 'disk' })
21
+ end
22
+
23
+ test 'finds group attributes' do
24
+ attributes = provider_type.attributes('vm')
25
+
26
+ assert_not_empty attributes
27
+ assert_not_empty(attributes.select { |a| a['group'] == 'vm' })
28
+ assert_empty(attributes.select { |a| a['group'] == 'nic' })
29
+ assert_empty(attributes.select { |a| a['group'] == 'disk' })
30
+ end
31
+
32
+ test 'has available_attributes' do
33
+ attr_hash = provider_type.available_attributes
34
+
35
+ assert_instance_of Hash, attr_hash
36
+ assert_include attr_hash.keys, 'num_sockets'
37
+ assert_equal 'num_sockets', attr_hash['num_sockets']['name']
38
+ end
39
+
40
+ test 'no available_attributes raises' do
41
+ provider_type.expects(:attributes?).returns(false)
42
+
43
+ assert_raises(RuntimeError) do
44
+ provider_type.available_attributes
45
+ end
46
+ end
47
+
48
+ test 'no default_attributes returns nil' do
49
+ assert_nil provider_type.default_attributes
50
+ end
51
+
52
+ test 'returns default_attributes, if any' do
53
+ def_attr = {
54
+ 'server_type' => 'cx23',
55
+ 'image' => 'debian-13',
56
+ }
57
+
58
+ provider_type.instance_eval do
59
+ @default_attributes = def_attr
60
+ end
61
+
62
+ assert_not_nil provider_type.default_attributes
63
+ assert_instance_of Hash, provider_type.default_attributes
64
+ assert_not_empty provider_type.default_attributes
65
+ assert_equal def_attr, provider_type.default_attributes
66
+ end
67
+ end
68
+ end
@@ -7,7 +7,7 @@ module ForemanOpentofu
7
7
  @cr_attrs = { 'name' => 'vm-1' }
8
8
  @executor = OpentofuExecuter.new(@compute_resource, @cr_attrs)
9
9
 
10
- @template = FactoryBot.create(:provisioning_template, name: Setting[:provision_nutanix_host_template])
10
+ @template = FactoryBot.create(:provisioning_template, name: 'Nutanix test script')
11
11
  @executor.stubs(:provision_template).returns(@template)
12
12
 
13
13
  @app_mock = mock('AppWrapper')
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_opentofu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ATIX-AG
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-02-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deface
@@ -33,6 +33,7 @@ files:
33
33
  - LICENSE
34
34
  - README.md
35
35
  - Rakefile
36
+ - app/assets/javascripts/foreman_opentofu/locale/en/foreman_opentofu.js
36
37
  - app/controllers/api/v2/tf_states_controller.rb
37
38
  - app/controllers/concerns/foreman_opentofu/compute_resources_vms_controller.rb
38
39
  - app/controllers/concerns/foreman_opentofu/controller/parameters/compute_resource.rb
@@ -42,6 +43,7 @@ files:
42
43
  - app/models/foreman_opentofu/opentofu_vm_commands.rb
43
44
  - app/models/foreman_opentofu/tf_state.rb
44
45
  - app/models/foreman_opentofu/tofu.rb
46
+ - app/models/foreman_opentofu/token.rb
45
47
  - app/overrides/compute_resources/remove_virtual_machines_tab.rb
46
48
  - app/services/foreman_opentofu/app_wrapper.rb
47
49
  - app/services/foreman_opentofu/compute_fetcher.rb
@@ -54,13 +56,14 @@ files:
54
56
  - app/views/compute_resources_vms/form/tofu/_dynamic_attrs.html.erb
55
57
  - app/views/compute_resources_vms/form/tofu/_network.html.erb
56
58
  - app/views/compute_resources_vms/index/_tofu.html.erb
57
- - app/views/templates/provisioning/nutanix_provision_host.erb
58
- - app/views/templates/provisioning/ovirt_provision_host.erb
59
+ - app/views/templates/provisioning/nutanix_provision_default.erb
60
+ - app/views/templates/provisioning/ovirt_provision_default.erb
59
61
  - config/initializers/compute_attrs.rb
60
62
  - config/nutanix.json
61
63
  - config/ovirt.json
62
64
  - config/routes.rb
63
65
  - db/migrate/20250625192757_create_tf_state.foreman_opentofu.rb
66
+ - db/migrate/20260127160904_add_table_token.foreman_opentofu.rb
64
67
  - db/seeds.d/71_provisioning_templates.rb
65
68
  - lib/foreman_opentofu.rb
66
69
  - lib/foreman_opentofu/engine.rb
@@ -70,6 +73,7 @@ files:
70
73
  - lib/foreman_opentofu/version.rb
71
74
  - lib/tasks/foreman_opentofu_tasks.rake
72
75
  - locale/Makefile
76
+ - locale/en/LC_MESSAGES/foreman_opentofu.mo
73
77
  - locale/en/foreman_opentofu.po
74
78
  - locale/foreman_opentofu.pot
75
79
  - locale/gemspec.rb
@@ -77,9 +81,13 @@ files:
77
81
  - test/factories/compute_resources.rb
78
82
  - test/factories/foreman_opentofu_factories.rb
79
83
  - test/factories/tf_state_factories.rb
84
+ - test/factories/token.rb
80
85
  - test/models/foreman_opentofu/opentofu_vm_commands_test.rb
86
+ - test/models/foreman_opentofu/tofu_test.rb
87
+ - test/models/foreman_opentofu/token_test.rb
81
88
  - test/models/foreman_opentofu_test.rb
82
89
  - test/services/app_wrapper_test.rb
90
+ - test/services/foreman_opentofu/provider_type_test.rb
83
91
  - test/services/opentofu_executer_test.rb
84
92
  - test/test_plugin_helper.rb
85
93
  homepage: https://github.com/ATIX-AG/foreman_opentofu
@@ -104,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
112
  - !ruby/object:Gem::Version
105
113
  version: '0'
106
114
  requirements: []
107
- rubygems_version: 3.6.3
115
+ rubygems_version: 4.0.3
108
116
  specification_version: 4
109
117
  summary: Plugin to provision host using opentofu
110
118
  test_files:
@@ -112,8 +120,12 @@ test_files:
112
120
  - test/factories/compute_resources.rb
113
121
  - test/factories/foreman_opentofu_factories.rb
114
122
  - test/factories/tf_state_factories.rb
123
+ - test/factories/token.rb
115
124
  - test/models/foreman_opentofu/opentofu_vm_commands_test.rb
125
+ - test/models/foreman_opentofu/tofu_test.rb
126
+ - test/models/foreman_opentofu/token_test.rb
116
127
  - test/models/foreman_opentofu_test.rb
117
128
  - test/services/app_wrapper_test.rb
129
+ - test/services/foreman_opentofu/provider_type_test.rb
118
130
  - test/services/opentofu_executer_test.rb
119
131
  - test/test_plugin_helper.rb