foreman_opentofu 0.0.5 → 0.0.6

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/app/controllers/api/v2/tf_states_controller.rb +13 -13
  4. data/app/lib/foreman_opentofu/concerns/base_template_scope_extensions.rb +11 -0
  5. data/app/models/concerns/foreman_opentofu/vm_command_collection_normalization.rb +26 -1
  6. data/app/models/foreman_opentofu/compute_vm.rb +28 -2
  7. data/app/models/foreman_opentofu/opentofu_vm_commands.rb +2 -1
  8. data/app/services/foreman_opentofu/opentofu_executer.rb +25 -4
  9. data/app/services/foreman_opentofu/provider_type.rb +6 -0
  10. data/app/services/foreman_opentofu/renderer/source/snapshot.rb +25 -0
  11. data/app/services/foreman_opentofu/template_snapshot_service.rb +68 -0
  12. data/app/views/api/v2/compute_resources/tofu.json.rabl +1 -0
  13. data/app/views/compute_resources_vms/form/tofu/_network.html.erb +4 -1
  14. data/app/views/compute_resources_vms/form/tofu/_volume.html.erb +4 -2
  15. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_interfaces_fields.html.erb +1 -0
  16. data/app/views/foreman_opentofu/compute_resources_vms/form/tofu/_volumes_fields.html.erb +8 -1
  17. data/app/views/templates/provisioning/hetzner_provision_host.erb +6 -21
  18. data/lib/foreman_opentofu/engine.rb +1 -0
  19. data/lib/foreman_opentofu/provider_types/hetzner.rb +5 -1
  20. data/lib/foreman_opentofu/provider_types/nutanix.rb +1 -1
  21. data/lib/foreman_opentofu/version.rb +1 -1
  22. data/lib/tasks/snapshots.rake +63 -0
  23. data/test/controllers/api/v2/tf_states_controller_test.rb +71 -2
  24. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.empty.snap.txt +72 -0
  25. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Hetzner_provision_default.hetzner_host.snap.txt +74 -0
  26. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.empty.snap.txt +56 -0
  27. data/test/fixtures/renderer/snapshots/ProvisioningTemplate/opentofu_script/Nutanix_provision_default.nutanix_host.snap.txt +62 -0
  28. data/test/fixtures/renderer/snapshots.yaml +6 -0
  29. data/test/lib/foreman_opentofu/concerns/base_template_scope_extensions_test.rb +33 -17
  30. data/test/models/foreman_opentofu/compute_vm_test.rb +32 -0
  31. data/test/models/foreman_opentofu/opentofu_vm_commands_test.rb +45 -3
  32. data/test/renderer/renderer_test.rb +60 -0
  33. data/test/services/foreman_opentofu/provider_type_test.rb +10 -0
  34. data/test/services/opentofu_executer_test.rb +28 -1
  35. data/test/services/tofu_key_pair_test.rb +4 -1
  36. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13aa21537aa49784d6b945e5e0169c20509c46e361e951b6db79a17d330559ef
4
- data.tar.gz: e0f23b3286a236bd161421d8464518a9b47878d61e7ebd03ab41b361cb0b513f
3
+ metadata.gz: aac98f7ab29438afb053493e3ef572e5374d5140eec49cc67e4f4d9787776a08
4
+ data.tar.gz: 85f7eb01a3b220b06b357c5ef9b0270bbf49fc19ea286f7e421964871f7bf726
5
5
  SHA512:
6
- metadata.gz: f1459d8aa7ad4e99b3d43b2725093f7cdc06d7473073432ca8386f46b1d2db4a5ac3acad3ec7c3580830e2d13a11e25354713c9a702a1f3ac669a527a504137f
7
- data.tar.gz: 4724e84ebc240ef60bdd95c65710fc3156f718628d38ba65570d8305df762be8883e00df13783002927589cdb2e7eed0304b98b414e5ac4e1edf9a48c5ca3d8c
6
+ metadata.gz: 3a8231fb351a2f809d768880ed32dd0be78b13333453f4d5fbb71fbcc4c801dc963c0b08beeed69ba916c307c669856d17e8513d1cdb4923f25e0c6b49487e73
7
+ data.tar.gz: c221f8852cfedf5d07bb40bb93832c2512b77b59b58c043dd8f23cfdf61ee102321be84825158e61981bcd1255b4bc2b6c18fa589d368c5c0d2df16dc2697994
data/README.md CHANGED
@@ -337,6 +337,23 @@ bundle exec foreman start
337
337
 
338
338
  See details in [foreman plugin development](https://projects.theforeman.org/projects/foreman/wiki/How_to_Create_a_Plugin)
339
339
 
340
+
341
+ ### Testing
342
+
343
+ #### Template Snapshots
344
+
345
+ The OpenTofu templates are tested against recorded snapshots similar to the Provisioning Templates in Foreman.
346
+ The snapshots are generated by rendering the templates with predefined fixture-data using the following command in the foreman-directory:
347
+
348
+ ```shell
349
+ bundle exec rake foreman_opentofu:snapshots:generate RAILS_ENV=test
350
+ ```
351
+
352
+ At the moment the fixture-data is defined in `app/services/foreman_opentofu/template_snapshot_service.rb`.
353
+
354
+ Templates to test must be added to `test/fixtures/renderer/snapshots.yaml`.
355
+
356
+
340
357
  ## Contributing
341
358
 
342
359
  Fork and send a Pull Request or create Issue. Thank you.
@@ -2,10 +2,8 @@ module Api
2
2
  module V2
3
3
  class TfStatesController < ::Api::V2::BaseController
4
4
  include ::Api::Version2
5
+ include ::Foreman::Controller::FilterParameters
5
6
 
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
7
  skip_before_action :set_taxonomy
10
8
 
11
9
  # Allow HTTP POST methods without CSRF
@@ -14,6 +12,11 @@ module Api
14
12
  # overwrite authorize with the local token-based authorization
15
13
  before_action :authorize, except: [:destroy]
16
14
 
15
+ # Don't log tfstate response in logs as it can have sensitive information
16
+ skip_after_action :log_response_body
17
+
18
+ filter_parameters :version, :terraform_version, :serial, :lineage, :outputs, :resources, :tf_state, :check_results
19
+
17
20
  resource_description do
18
21
  api_version 'v2'
19
22
  api_base_url '/foreman_opentofu/api'
@@ -22,15 +25,13 @@ module Api
22
25
  def show
23
26
  state = ForemanOpentofu::TfState.find_by(name: params[:name])
24
27
 
25
- if state
26
- render plain: state.state, content_type: 'application/json'
27
- else
28
- render plain: '', status: :not_found
29
- end
28
+ return head :not_found unless state
29
+
30
+ render plain: state.state, content_type: 'application/json'
30
31
  end
31
32
 
32
33
  def create
33
- raw_state = request.body.read
34
+ raw_state = request.raw_post
34
35
  if raw_state.blank?
35
36
  render plain: 'Missing state body', status: :unprocessable_entity
36
37
  return
@@ -41,10 +42,9 @@ module Api
41
42
  state = ForemanOpentofu::TfState.find_or_initialize_by(name: params[:name])
42
43
  state.state = raw_state
43
44
  state.save!
44
- render plain: '', status: :ok
45
- rescue JSON::ParserError => e
46
- Rails.logger.error("Invalid state JSON: #{e.message}")
47
- render plain: 'Invalid state format', status: :unprocessable_entity
45
+ head :ok
46
+ rescue JSON::ParserError
47
+ render plain: 'Invalid state format', status: :bad_request
48
48
  end
49
49
  end
50
50
 
@@ -97,6 +97,8 @@ module ForemanOpentofu
97
97
  end
98
98
 
99
99
  def build_disks
100
+ return rendered_disk(0, disk_render_source.first).to_s if disk_renderer_collection?
101
+
100
102
  disk_render_source.each_with_index.filter_map do |disk, index|
101
103
  # drop removed disks; tofu will drop them automatically, if they are no longer defined
102
104
  next if disk.respond_to?(:[]) && disk['_delete'].to_i == 1
@@ -151,6 +153,15 @@ module ForemanOpentofu
151
153
  to_hcl(data, snippet: true)
152
154
  end
153
155
  end
156
+
157
+ def disk_renderer_collection?
158
+ return false unless @compute_resource.respond_to?(:tofu_provider)
159
+
160
+ provider = @compute_resource.tofu_provider
161
+ provider.respond_to?(:disk_renderer_collection?) && provider.disk_renderer_collection?
162
+ rescue StandardError
163
+ false
164
+ end
154
165
  end
155
166
  end
156
167
  end
@@ -29,13 +29,38 @@ module ForemanOpentofu
29
29
  end
30
30
 
31
31
  def normalize_collection_input(collection, value)
32
- return nested_attributes_for(collection, value) if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
32
+ return normalize_nested_hash_collection(collection, value) if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
33
33
 
34
34
  Array(value).map do |entry|
35
35
  entry.respond_to?(:deep_symbolize_keys) ? entry.deep_symbolize_keys : entry
36
36
  end
37
37
  end
38
38
 
39
+ def normalize_nested_hash_collection(collection, value)
40
+ values = (value.respond_to?(:to_unsafe_h) ? value.to_unsafe_h : value.to_h).deep_stringify_keys
41
+ values.delete("new_#{collection}")
42
+
43
+ values.sort_by { |key, _| key.to_s.sub('new_', '').to_i }.filter_map do |_, val|
44
+ normalized = normalize_nested_collection_value(val)
45
+ next if normalized.is_a?(Hash) && normalized[:_delete] == '1' && normalized[:id].blank?
46
+
47
+ normalized
48
+ end
49
+ end
50
+
51
+ def normalize_nested_collection_value(value)
52
+ normalized =
53
+ if value.is_a?(ActionController::Parameters) && value.respond_to?(:to_unsafe_h)
54
+ value.to_unsafe_h
55
+ elsif value.respond_to?(:to_h)
56
+ value.to_h
57
+ else
58
+ value
59
+ end
60
+
61
+ normalized.respond_to?(:deep_symbolize_keys) ? normalized.deep_symbolize_keys : normalized
62
+ end
63
+
39
64
  def prefill_mandatory_attributes(args)
40
65
  # FIXME: add volume/nic attributes
41
66
  res = {}
@@ -14,7 +14,7 @@ module ForemanOpentofu
14
14
  end
15
15
 
16
16
  def to_h
17
- unwrap(@attributes.to_dup)
17
+ unwrap(@attributes.deep_dup)
18
18
  end
19
19
 
20
20
  def power
@@ -30,6 +30,21 @@ module ForemanOpentofu
30
30
  self['name']
31
31
  end
32
32
 
33
+ def vm_description
34
+ return nil unless @provider.respond_to?(:tofu_provider)
35
+
36
+ vm_attrs = @provider.tofu_provider.attributes('vm')
37
+ values = vm_attrs.filter_map do |attr|
38
+ key = attr['name'].to_s
39
+ value = attribute_value(key)
40
+ next if value.blank?
41
+
42
+ label = attr['label'].presence || key.humanize
43
+ "#{label}: #{value}"
44
+ end
45
+ values.join(', ') if values.present?
46
+ end
47
+
33
48
  def persisted?
34
49
  self['identity'].present? || self['id'].present?
35
50
  end
@@ -119,6 +134,17 @@ module ForemanOpentofu
119
134
  end
120
135
  end
121
136
 
137
+ def unwrap(value)
138
+ case value
139
+ when Hash
140
+ value.transform_values { |v| unwrap(v) }
141
+ when Array
142
+ value.map { |v| unwrap(v) }
143
+ else
144
+ value
145
+ end
146
+ end
147
+
122
148
  def flatten_attrs(attrs)
123
149
  result = {}
124
150
 
@@ -144,7 +170,7 @@ module ForemanOpentofu
144
170
  key = method_name.to_s
145
171
  return attribute_value(key) if dynamic_attribute_keys.include?(key)
146
172
 
147
- super
173
+ nil
148
174
  end
149
175
  end
150
176
  end
@@ -18,7 +18,8 @@ module ForemanOpentofu
18
18
  executor = client(args)
19
19
  data = executor.run_new
20
20
  attrs = data['resource_changes'].first['change']['after'] || {}
21
- OpenStruct.new(attrs)
21
+ attrs = args.deep_stringify_keys.merge(attrs.to_h.deep_stringify_keys)
22
+ ComputeVM.new(self, attrs)
22
23
  end
23
24
  end
24
25
 
@@ -80,10 +80,14 @@ module ForemanOpentofu
80
80
  tofu.plan
81
81
  raise 'OpenTofu planned to re-create a resource; action aborted (check logs for details)!' if plan_wants_recreate? tofu.show_plan
82
82
  end
83
- tofu.apply
84
- attrs = tofu.output('vm_attrs')
85
- ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.update(uuid: attrs['identity'])
86
- attrs
83
+ begin
84
+ tofu.apply
85
+ attrs = tofu.output('vm_attrs')
86
+ ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.update(uuid: attrs['identity'])
87
+ attrs
88
+ rescue StandardError => e
89
+ handle_failed_create(tofu, e)
90
+ end
87
91
  end
88
92
  end
89
93
 
@@ -177,5 +181,22 @@ module ForemanOpentofu
177
181
  end
178
182
  false
179
183
  end
184
+
185
+ def handle_failed_create(tofu, error)
186
+ tofu.destroy
187
+ ForemanOpentofu::TfState.find_by(name: @cr_attrs['name'])&.destroy
188
+ raise error
189
+ rescue StandardError => e
190
+ raise if e.equal?(error)
191
+
192
+ Rails.logger.error(
193
+ "Removing OpenTofu resource after host create for #{@cr_attrs['name']} failed: #{e.message}"
194
+ )
195
+ wrapped_error = error.exception(
196
+ "#{error.message}\n Removing resource after failed host creation also failed: #{e.message}"
197
+ )
198
+ wrapped_error.set_backtrace(error.backtrace)
199
+ raise wrapped_error
200
+ end
180
201
  end
181
202
  end
@@ -119,5 +119,11 @@ module ForemanOpentofu
119
119
 
120
120
  result
121
121
  end
122
+
123
+ # Whether disk renderer emits one collection resource for all disks (for_each)
124
+ # instead of one resource block per disk entry.
125
+ def disk_renderer_collection?
126
+ false
127
+ end
122
128
  end
123
129
  end
@@ -0,0 +1,25 @@
1
+ module ForemanOpentofu
2
+ module Renderer
3
+ module Source
4
+ class Snapshot < Foreman::Renderer::Source::Snapshot
5
+ SNAPSHOTS_DIRECTORY = ForemanOpentofu::Engine.root.join('test', 'fixtures', 'renderer', 'snapshots')
6
+
7
+ class << self
8
+ def snapshot_path(template, suffix = 'host4dhcp')
9
+ File.join(SNAPSHOTS_DIRECTORY, "#{template_path(template).tr(' ', '_')}.#{suffix}.snap.txt")
10
+ end
11
+
12
+ def hosts(template)
13
+ ['empty', "#{template.name.split(' ').first.downcase}_host"]
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def templates_directory
20
+ ForemanOpentofu::TemplateSnapshotService::TEMPLATES_DIRECTORY
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ module ForemanOpentofu
2
+ # < Foreman::TemplateSnapshotService
3
+ class TemplateSnapshotService
4
+ TEMPLATES_DIRECTORY = ForemanOpentofu::Engine.root.join('app', 'views', 'templates', 'provisioning')
5
+
6
+ def self.templates
7
+ new.templates
8
+ end
9
+
10
+ def self.render_template(template, host_name = :empty)
11
+ raise 'TemplateSnapshotService can only be used in test environment' unless Rails.env.test?
12
+ require 'factory_bot_rails'
13
+
14
+ cr_attrs = new.send(host_name.to_sym)
15
+ provider_type = template.name.split(' ').first.downcase
16
+ variables = {
17
+ compute_resource: FactoryBot.build("opentofu_#{provider_type}_cr".to_sym),
18
+ cr_attrs: cr_attrs || {},
19
+ use_backend: true,
20
+ token: 'very_secret_token',
21
+ host_name: host_name,
22
+ resource: nil,
23
+ }
24
+ variables[:ssh_key] = variables[:compute_resource].key_pair if variables[:compute_resource].key_pair
25
+ source = ForemanOpentofu::Renderer::Source::Snapshot.new(template)
26
+ scope = Foreman::Renderer.get_scope(source: source, variables: variables)
27
+ Foreman::Renderer::UnsafeModeRenderer.render(source, scope)
28
+ end
29
+
30
+ def templates
31
+ files.map { |path| ForemanOpentofu::Renderer::Source::Snapshot.load_file(path) }
32
+ end
33
+
34
+ def hetzner_host
35
+ {
36
+ server_type: 'cx23',
37
+ }
38
+ end
39
+
40
+ def nutanix_host
41
+ {
42
+ 'cluster_uuid' => 'my_cluster_uuid',
43
+ 'volumes_attributes' => [
44
+ {
45
+ size_gb: 40,
46
+ container_uuid: 'my_container_uuid',
47
+ },
48
+ ],
49
+ 'interfaces_attributes' => [
50
+ {
51
+ model: 'E1000',
52
+ subnet_uuid: 'my_subnet_uuid',
53
+ },
54
+ ],
55
+ }
56
+ end
57
+
58
+ def empty
59
+ {}
60
+ end
61
+
62
+ private
63
+
64
+ def files
65
+ @files ||= YAML.safe_load_file(ForemanOpentofu::Engine.root.join('test', 'fixtures', 'renderer', 'snapshots.yaml')).fetch('files', []).map { |path| File.join(TEMPLATES_DIRECTORY, path) }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1 @@
1
+ attributes :user, :opentofu_provider, :opentofu_template_id
@@ -1 +1,4 @@
1
- <%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: compute_resource.tofu_provider.attributes('nic'), compute_resource: compute_resource } %>
1
+ <% index = f.index.present? ? f.index : 0 %>
2
+ <%= field_set_tag _("Network"), :id => "network_#{index}" do %>
3
+ <%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: compute_resource.tofu_provider.attributes('nic'), compute_resource: compute_resource } %>
4
+ <% end %>
@@ -1,2 +1,4 @@
1
- <%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: compute_resource.tofu_provider.attributes('disk'), compute_resource: compute_resource } %>
2
-
1
+ <% index = f.index.present? ? f.index : 0 %>
2
+ <%= field_set_tag _("Volume"), :id => "volume_#{index}" do %>
3
+ <%= render :partial => provider_partial(compute_resource, "dynamic_attrs"), :locals => { f: f, attrs: compute_resource.tofu_provider.attributes('disk'), compute_resource: compute_resource } %>
4
+ <% end %>
@@ -5,6 +5,7 @@
5
5
  interfaces = [compute_resource.new_interface].compact if interfaces.empty?
6
6
  %>
7
7
  <% interfaces.each_with_index do |interface_obj, idx| %>
8
+ <% interface_obj = compute_resource.new_interface(interface_obj.to_h) if interface_obj.is_a?(Hash) %>
8
9
  <%= fields_for "#{f.object_name}[#{association_attributes}][#{idx}]", interface_obj do |i| %>
9
10
  <%= render :partial => provider_partial(compute_resource, 'network'),
10
11
  :locals => { :f => i, :compute_resource => compute_resource, :new_host => new_host, :new_vm => new_vm, :remove_title => _('remove network interface'), :selected_cluster => selected_cluster },
@@ -1,5 +1,12 @@
1
1
  <%
2
- volumes = f.object.volumes_attributes
2
+ volumes =
3
+ if f.object.respond_to?(:volumes_attributes)
4
+ f.object.volumes_attributes
5
+ elsif f.object.respond_to?(:volumes)
6
+ f.object.volumes
7
+ else
8
+ {}
9
+ end
3
10
  volumes =
4
11
  if volumes.is_a?(Hash)
5
12
  volumes.sort_by { |key, _| key.to_s.sub('new_', '').to_i }
@@ -30,11 +30,9 @@ data "hcloud_ssh_key" "deploy_key" {
30
30
  }
31
31
  <%- end %>
32
32
 
33
- # Create a new server running debian
33
+ # Create a new server
34
34
  resource "hcloud_server" "node1" {
35
35
  name = "<%= @host_name %>"
36
- # image = "debian-12"
37
- # server_type = "cx23"
38
36
 
39
37
  <%= vm_attributes(['name', 'image']) %>
40
38
 
@@ -68,27 +66,14 @@ output "vm_attrs" {
68
66
  vm_ip6_address = hcloud_server.node1.ipv6_address
69
67
  vm = hcloud_server.node1
70
68
  volumes_attributes = try({
71
- for v in values(hcloud_volume.volumes) : tostring(v.id) => {
72
- id = v.id
73
- name = v.name
74
- size = v.size
69
+ for k, v in try(local.disks, {}) : tostring(try(v.id, k)) => {
70
+ id = try(v.id, null)
71
+ name = try(v.name, "volume-${k}")
72
+ size = try(v.size, 20)
75
73
  automount = try(v.automount, null)
76
74
  format = try(v.format, null)
77
- }
75
+ } if try(v["_delete"], "0") != "1"
78
76
  }, {})
79
77
  }
80
78
  }
81
-
82
- <% else %>
83
- # # get available images
84
- # data "hcloud_images" "all" {}
85
- # output "images" {
86
- # value = data.hcloud_images.all.images
87
- # }
88
- #
89
- # # get available server types
90
- # data "hcloud_server_types" "all" {}
91
- # output "server_types" {
92
- # value = data.hcloud_server_types.all.server_types
93
- # }
94
79
  <% end %>
@@ -47,6 +47,7 @@ module ForemanOpentofu
47
47
  # Include concerns in this config.to_prepare block
48
48
  config.to_prepare do
49
49
  ::ComputeResourcesController.include ForemanOpentofu::Controller::Parameters::ComputeResource
50
+ ::Api::V2::ComputeResourcesController.include ForemanOpentofu::Controller::Parameters::ComputeResource
50
51
  ::ComputeResourcesVmsController.include ForemanOpentofu::ComputeResourcesVmsController
51
52
  ::Host::Managed.include Orchestration::Tofu::Compute
52
53
  ::Authorizer.prepend ForemanOpentofu::AuthorizerPowerOverride unless ::Authorizer < ForemanOpentofu::AuthorizerPowerOverride
@@ -5,6 +5,10 @@ require "#{ForemanOpentofu::Engine.root}/app/services/foreman_opentofu/provider_
5
5
  ForemanOpentofu::ProviderTypeManager.register('hetzner') do
6
6
  @capabilities = [:image, :key_pair, :power_status_only]
7
7
 
8
+ def disk_renderer_collection?
9
+ true
10
+ end
11
+
8
12
  def provided_attributes
9
13
  {
10
14
  ip: :vm_ip_address,
@@ -125,7 +129,7 @@ ForemanOpentofu::ProviderTypeManager.register('hetzner') do
125
129
  }
126
130
 
127
131
  resource "hcloud_volume" "volumes" {
128
- for_each = { for k, d in local.disks : tostring(try(d.id, k)) => d if try(d["_delete"], "0") != "1" }
132
+ for_each = { for k, d in local.disks : tostring(k) => d if try(d["_delete"], "0") != "1" }
129
133
  name = try(each.value.name, "volume-${each.key}")
130
134
  size = try(each.value.size, 20)
131
135
  server_id = hcloud_server.node1.id
@@ -55,7 +55,7 @@ ForemanOpentofu::ProviderTypeManager.register('nutanix') do
55
55
  self.disk_renderer = proc do |disk|
56
56
  {
57
57
  disk_list: {
58
- disk_size_mib: disk[:size_gb] * 1024,
58
+ disk_size_mib: disk[:size_gb].to_i * 1024,
59
59
  storage_config: {
60
60
  storage_container_reference: {
61
61
  kind: 'storage_container',
@@ -1,3 +1,3 @@
1
1
  module ForemanOpentofu
2
- VERSION = '0.0.5'.freeze
2
+ VERSION = '0.0.6'.freeze
3
3
  end
@@ -0,0 +1,63 @@
1
+ namespace :foreman_opentofu do
2
+ namespace :snapshots do
3
+ desc 'Generate snapshots'
4
+ task generate: :environment do
5
+ unless Rails.env.test?
6
+ puts 'This task can only be run in test environment'
7
+ exit
8
+ end
9
+
10
+ require 'database_cleaner'
11
+ require 'factory_bot_rails'
12
+
13
+ # Add plugin to FactoryBot's paths
14
+ FactoryBot.definition_file_paths << ForemanOpentofu::Engine.root.join('test', 'factories')
15
+ FactoryBot.reload
16
+
17
+ module BaseMacrosStub
18
+ def dns_lookup(_name_or_ip)
19
+ 'foreman.example.com'
20
+ end
21
+ end
22
+
23
+ # ::Foreman::Plugin.singleton_class.send :prepend, PluginSnapshotStub
24
+ ::Foreman::Renderer::Scope::Base.prepend BaseMacrosStub
25
+
26
+ # clean the snapshot directory in order to delete renamed ones and keep it clean
27
+ FileUtils.rm_rf(Dir.glob(File.join(
28
+ ::ForemanOpentofu::Renderer::Source::Snapshot::SNAPSHOTS_DIRECTORY, '*'
29
+ )))
30
+
31
+ DatabaseCleaner.cleaning do
32
+ Foreman.settings.load
33
+ Setting[:foreman_url] = 'http://foreman.example.com'
34
+
35
+ User.current = FactoryBot.build(:user, :admin)
36
+ admin = FactoryBot.create(:user, :admin, password: 'password123', auth_source: FactoryBot.create(:auth_source_ldap))
37
+
38
+ failed_snapshots = []
39
+
40
+ User.as(admin.login) do
41
+ ForemanOpentofu::TemplateSnapshotService.templates.each do |template|
42
+ ForemanOpentofu::Renderer::Source::Snapshot.hosts(template).each do |host|
43
+ snapshot_path = ForemanOpentofu::Renderer::Source::Snapshot.snapshot_path(template, host)
44
+ dir = File.dirname(snapshot_path)
45
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
46
+
47
+ begin
48
+ snapshot = ForemanOpentofu::TemplateSnapshotService.render_template(template, host)
49
+ puts "Writing #{snapshot_path}"
50
+ File.write(snapshot_path, snapshot)
51
+ rescue StandardError => e
52
+ warn "Snapshot #{snapshot_path} failed with #{e}"
53
+ failed_snapshots << snapshot_path
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ raise "#{failed_snapshots.count} snapshots failed generation or validation. Please inspect the output to see the details." unless failed_snapshots.empty?
60
+ end
61
+ end
62
+ end
63
+ end