foreman_nutanix 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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +32 -0
  4. data/Rakefile +49 -0
  5. data/app/assets/javascripts/foreman_nutanix/locale/en/foreman_nutanix.js +55 -0
  6. data/app/controllers/concerns/foreman/controller/parameters/compute_resource_extension.rb +17 -0
  7. data/app/controllers/foreman_nutanix/api/v2/apipie_extensions.rb +16 -0
  8. data/app/controllers/foreman_nutanix/api/v2/compute_resources_extensions.rb +29 -0
  9. data/app/controllers/foreman_nutanix/api/v2/hosts_controller_extensions.rb +50 -0
  10. data/app/lib/foreman_nutanix/nutanix_adapter.rb +105 -0
  11. data/app/lib/nutanix_compute/compute_collection.rb +23 -0
  12. data/app/lib/nutanix_extensions/attached_disk.rb +22 -0
  13. data/app/models/concerns/foreman_nutanix/host_managed_extensions.rb +38 -0
  14. data/app/models/foreman_nutanix/nutanix.rb +471 -0
  15. data/app/models/foreman_nutanix/nutanix_compute.rb +370 -0
  16. data/app/views/compute_resources/form/_nutanix.html.erb +9 -0
  17. data/app/views/compute_resources/show/_nutanix.html.erb +238 -0
  18. data/app/views/compute_resources_vms/form/nutanix/_base.html.erb +72 -0
  19. data/app/views/compute_resources_vms/form/nutanix/_volume.html.erb +1 -0
  20. data/app/views/compute_resources_vms/index/_gce.html.erb +41 -0
  21. data/app/views/compute_resources_vms/index/_nutanix.html.erb +41 -0
  22. data/app/views/compute_resources_vms/show/_gce.html.erb +18 -0
  23. data/app/views/compute_resources_vms/show/_nutanix.html.erb +81 -0
  24. data/config/initializers/zeitwerk.rb +1 -0
  25. data/config/routes.rb +2 -0
  26. data/lib/foreman_nutanix/engine.rb +56 -0
  27. data/lib/foreman_nutanix/version.rb +3 -0
  28. data/lib/foreman_nutanix.rb +4 -0
  29. data/lib/tasks/foreman_nutanix_tasks.rake +31 -0
  30. data/locale/foreman_nutanix.pot +131 -0
  31. data/package.json +41 -0
  32. data/webpack/global_index.js +6 -0
  33. data/webpack/global_test_setup.js +11 -0
  34. data/webpack/index.js +7 -0
  35. data/webpack/legacy.js +16 -0
  36. data/webpack/src/Extends/index.js +15 -0
  37. data/webpack/src/Router/routes.js +5 -0
  38. data/webpack/src/reducers.js +7 -0
  39. data/webpack/test_setup.js +17 -0
  40. metadata +100 -0
@@ -0,0 +1,370 @@
1
+ module ForemanNutanix
2
+ class NutanixCompute
3
+ attr_reader :identity, :name, :hostname, :cluster, :args
4
+ attr_accessor :zone, :machine_type, :network, :image_id, :associate_external_ip, :cpus, :memory, :power_state, :subnet_ext_id, :storage_container_ext_id, :num_sockets, :num_cores_per_socket, :disk_size_bytes, :description, :network_id, :storage_container, :disk_size_gb, :power_on, :mac_address, :vm_ip_addresses, :create_time
5
+
6
+ def initialize(cluster = nil, args = {})
7
+ Rails.logger.info "=== NUTANIX: NutanixCompute::initialize cluster=#{cluster} args=#{args} ==="
8
+ @cluster = cluster
9
+ @args = args
10
+ @name = args[:name] || "nutanix-vm-#{Time.now.to_i}"
11
+ @identity = args[:identity] || args[:uuid] || @name
12
+ @hostname = args[:hostname] || @name
13
+ @zone = args[:zone] || 'default-zone'
14
+ @machine_type = args[:machine_type] || args[:flavor_id] || 'small'
15
+ @network = args[:network] || args[:network_id] || 'default-network'
16
+ @image_id = args[:image_id]
17
+ @associate_external_ip = args[:associate_external_ip] || true
18
+ @cpus = args[:cpus] || 2
19
+ @memory = args[:memory] || 4
20
+ @power_state = args[:power_state]
21
+ @persisted = false
22
+
23
+ # Provisioning-specific attributes
24
+ @network_id = args[:network_id] || args[:network]
25
+ @storage_container = args[:storage_container]
26
+ @disk_size_gb = args[:disk_size_gb] || 50
27
+ @power_on = args.key?(:power_on) ? args[:power_on] : true # Default to true
28
+ @subnet_ext_id = args[:subnet_ext_id] || @network_id
29
+ @storage_container_ext_id = args[:storage_container_ext_id] || @storage_container
30
+ @num_sockets = args[:num_sockets] || 1
31
+ @num_cores_per_socket = args[:num_cores_per_socket] || @cpus
32
+ @disk_size_bytes = args[:disk_size_bytes] || (@disk_size_gb.to_i * 1024**3)
33
+ @description = args[:description] || ''
34
+
35
+ # VM details from Nutanix
36
+ @mac_address = args[:mac_address]
37
+ @vm_ip_addresses = args[:ip_addresses] || []
38
+ @create_time = args[:create_time]
39
+ end
40
+
41
+ # Required by Foreman - indicates if VM exists
42
+ def persisted?
43
+ Rails.logger.info '=== NUTANIX: NutanixCompute::persisted? called ==='
44
+ @persisted
45
+ end
46
+
47
+ # Required by Foreman - save the VM (actually create it)
48
+ def save
49
+ Rails.logger.info '=== NUTANIX: NutanixCompute::save called ==='
50
+ Rails.logger.info "=== NUTANIX: VM attributes - network_id: #{@network_id}, storage_container: #{@storage_container}, subnet_ext_id: #{@subnet_ext_id}, storage_container_ext_id: #{@storage_container_ext_id} ==="
51
+
52
+ # Build the provision request payload
53
+ # Convert memory from GB to bytes (1 GB = 1024^3 bytes)
54
+ memory_bytes = (@memory || 4).to_i * 1024**3
55
+
56
+ # Use form values, falling back to internal values
57
+ actual_subnet = @subnet_ext_id || @network_id
58
+ actual_storage = @storage_container_ext_id || @storage_container
59
+ actual_disk_bytes = @disk_size_bytes || (@disk_size_gb.to_i * 1024**3)
60
+
61
+ # Validate required fields
62
+ if actual_subnet.nil? || actual_subnet.to_s.strip.empty?
63
+ raise StandardError, 'Network/Subnet is required for VM provisioning'
64
+ end
65
+ if actual_storage.nil? || actual_storage.to_s.strip.empty?
66
+ raise StandardError, 'Storage Container is required for VM provisioning'
67
+ end
68
+
69
+ provision_request = {
70
+ name: @name,
71
+ cluster_ext_id: @cluster,
72
+ subnet_ext_id: actual_subnet,
73
+ storage_container_ext_id: actual_storage,
74
+ num_sockets: @num_sockets.to_i,
75
+ num_cores_per_socket: @num_cores_per_socket.to_i,
76
+ memory_size_bytes: memory_bytes,
77
+ disk_size_bytes: actual_disk_bytes.to_i,
78
+ description: @description || '',
79
+ power_on: @power_on.nil? || @power_on, # Default to true if not set
80
+ }
81
+
82
+ Rails.logger.info "=== NUTANIX: Provisioning VM with request: #{provision_request} ==="
83
+
84
+ # Call the shim server to provision the VM
85
+ base = ENV['NUTANIX_SHIM_SERVER_ADDR'] || 'http://localhost:8000'
86
+ uri = URI("#{base.chomp('/')}/api/v1/vmm/provision-vm")
87
+
88
+ http = Net::HTTP.new(uri.host, uri.port)
89
+ http.use_ssl = uri.scheme == 'https'
90
+
91
+ request = Net::HTTP::Post.new(uri.path)
92
+ request['Content-Type'] = 'application/json'
93
+ request.body = provision_request.to_json
94
+
95
+ response = http.request(request)
96
+
97
+ if response.is_a?(Net::HTTPSuccess)
98
+ result = JSON.parse(response.body)
99
+ Rails.logger.info "=== NUTANIX: VM provisioned successfully: #{result} ==="
100
+
101
+ # Update identity with the real ext_id from Nutanix
102
+ @identity = result['ext_id']
103
+ @persisted = true
104
+ true
105
+ else
106
+ error_message = "Failed to provision VM: #{response.code} - #{response.body}"
107
+ Rails.logger.error "=== NUTANIX: #{error_message} ==="
108
+ raise StandardError, error_message
109
+ end
110
+ rescue StandardError => e
111
+ Rails.logger.error "=== NUTANIX: Error in save: #{e.message} ==="
112
+ raise e
113
+ end
114
+
115
+ # Required by Foreman - VM status
116
+ # Returns true if VM is powered on and ready to use
117
+ def ready?
118
+ is_ready = persisted? && @power_state == 'ON'
119
+ Rails.logger.info "=== NUTANIX: NutanixCompute::ready? called, persisted=#{persisted?}, power_state=#{@power_state}, returning #{is_ready} ==="
120
+ is_ready
121
+ end
122
+
123
+ # Power state accessor that Foreman might call directly
124
+ def power_state
125
+ Rails.logger.info "=== NUTANIX: NutanixCompute::power_state called, returning #{@power_state} ==="
126
+ @power_state
127
+ end
128
+
129
+ # Required by Foreman - VM status
130
+ def state
131
+ Rails.logger.info "=== NUTANIX: NutanixCompute::state called, power_state=#{@power_state} ==="
132
+ return 'pending' unless persisted?
133
+
134
+ # Map Nutanix power states to Foreman-friendly states
135
+ result = case @power_state
136
+ when 'ON'
137
+ 'running'
138
+ when 'OFF'
139
+ 'stopped'
140
+ when 'PAUSED'
141
+ 'paused'
142
+ else
143
+ 'unknown'
144
+ end
145
+ Rails.logger.info "=== NUTANIX: NutanixCompute::state returning '#{result}' ==="
146
+ result
147
+ end
148
+ alias_method :status, :state
149
+
150
+ # Required by Foreman - reload VM state
151
+ def reload
152
+ Rails.logger.info '=== NUTANIX: NutanixCompute::reload called ==='
153
+ self
154
+ end
155
+
156
+ # Required by Foreman - start VM
157
+ def start(args = {})
158
+ Rails.logger.info "=== NUTANIX: NutanixCompute::start called with args: #{args} ==="
159
+ return false unless persisted?
160
+
161
+ # Extract actual UUID (handle ZXJnb24=:uuid format)
162
+ actual_uuid = @identity.to_s.include?(':') ? @identity.to_s.split(':').last : @identity.to_s
163
+
164
+ # Call the shim server to power on the VM
165
+ base = ENV['NUTANIX_SHIM_SERVER_ADDR'] || 'http://localhost:8000'
166
+ uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
167
+
168
+ http = Net::HTTP.new(uri.host, uri.port)
169
+ http.use_ssl = uri.scheme == 'https'
170
+
171
+ request = Net::HTTP::Post.new(uri.path)
172
+ request['Content-Type'] = 'application/json'
173
+ request.body = { action: 'POWER_ON' }.to_json
174
+
175
+ response = http.request(request)
176
+
177
+ if response.is_a?(Net::HTTPSuccess)
178
+ Rails.logger.info "=== NUTANIX: VM #{actual_uuid} powered on successfully ==="
179
+ @power_state = 'ON'
180
+ true
181
+ else
182
+ error_message = "Failed to power on VM: #{response.code} - #{response.body}"
183
+ Rails.logger.error "=== NUTANIX: #{error_message} ==="
184
+ raise StandardError, error_message
185
+ end
186
+ rescue StandardError => e
187
+ Rails.logger.error "=== NUTANIX: Error in start: #{e.message} ==="
188
+ raise e
189
+ end
190
+
191
+ # Required by Foreman - stop VM
192
+ def stop(args = {})
193
+ Rails.logger.info "=== NUTANIX: NutanixCompute::stop called with args: #{args} ==="
194
+ return false unless persisted?
195
+
196
+ # Extract actual UUID (handle ZXJnb24=:uuid format)
197
+ actual_uuid = @identity.to_s.include?(':') ? @identity.to_s.split(':').last : @identity.to_s
198
+
199
+ # Call the shim server to power off the VM
200
+ base = ENV['NUTANIX_SHIM_SERVER_ADDR'] || 'http://localhost:8000'
201
+ uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
202
+
203
+ http = Net::HTTP.new(uri.host, uri.port)
204
+ http.use_ssl = uri.scheme == 'https'
205
+
206
+ request = Net::HTTP::Post.new(uri.path)
207
+ request['Content-Type'] = 'application/json'
208
+ request.body = { action: 'POWER_OFF' }.to_json
209
+
210
+ response = http.request(request)
211
+
212
+ if response.is_a?(Net::HTTPSuccess)
213
+ Rails.logger.info "=== NUTANIX: VM #{actual_uuid} powered off successfully ==="
214
+ @power_state = 'OFF'
215
+ true
216
+ else
217
+ error_message = "Failed to power off VM: #{response.code} - #{response.body}"
218
+ Rails.logger.error "=== NUTANIX: #{error_message} ==="
219
+ raise StandardError, error_message
220
+ end
221
+ rescue StandardError => e
222
+ Rails.logger.error "=== NUTANIX: Error in stop: #{e.message} ==="
223
+ raise e
224
+ end
225
+
226
+ # Required by Foreman - CPU count
227
+ def cpu
228
+ Rails.logger.info '=== NUTANIX: NutanixCompute::cpu called ==='
229
+ @cpus.to_s
230
+ end
231
+
232
+ # Required by Foreman - memory in GB
233
+ def memory
234
+ Rails.logger.info '=== NUTANIX: NutanixCompute::memory called ==='
235
+ @memory
236
+ end
237
+
238
+ # Required by Foreman - string representation
239
+ def to_s
240
+ @name
241
+ end
242
+
243
+ # Required by Foreman - creation timestamp
244
+ def creation_timestamp
245
+ Rails.logger.info '=== NUTANIX: NutanixCompute::creation_timestamp called ==='
246
+ return nil unless @create_time
247
+
248
+ begin
249
+ if @create_time.is_a?(String)
250
+ Time.parse(@create_time)
251
+ else
252
+ @create_time
253
+ end
254
+ rescue StandardError => e
255
+ Rails.logger.error "=== NUTANIX: Error parsing create_time: #{e.message} ==="
256
+ nil
257
+ end
258
+ end
259
+
260
+ # Required by Foreman - image name for display
261
+ def pretty_image_name
262
+ Rails.logger.info '=== NUTANIX: NutanixCompute::pretty_image_name called ==='
263
+ # We don't track the source image yet
264
+ nil
265
+ end
266
+
267
+ # Required by Foreman - public IP address
268
+ def vm_ip_address
269
+ Rails.logger.info '=== NUTANIX: NutanixCompute::vm_ip_address called ==='
270
+ return nil unless persisted?
271
+ @vm_ip_addresses&.first
272
+ end
273
+ alias_method :public_ip_address, :vm_ip_address
274
+
275
+ # Required by Foreman - private IP address
276
+ def private_ip_address
277
+ Rails.logger.info '=== NUTANIX: NutanixCompute::private_ip_address called ==='
278
+ return nil unless persisted?
279
+ # Return second IP if available, otherwise same as public
280
+ (@vm_ip_addresses&.length.to_i > 1) ? @vm_ip_addresses[1] : @vm_ip_addresses&.first
281
+ end
282
+
283
+ # Required by Foreman - all IP addresses
284
+ def ip_addresses
285
+ Rails.logger.info '=== NUTANIX: NutanixCompute::ip_addresses called ==='
286
+ persisted? ? (@vm_ip_addresses || []) : []
287
+ end
288
+
289
+ # Required by Foreman - MAC address
290
+ def mac
291
+ Rails.logger.info "=== NUTANIX: NutanixCompute::mac called, persisted=#{persisted?}, mac_address=#{@mac_address} ==="
292
+ persisted? ? @mac_address : nil
293
+ end
294
+
295
+ # Required by Foreman - MAC addresses hash for VM association
296
+ def mac_addresses
297
+ Rails.logger.info "=== NUTANIX: NutanixCompute::mac_addresses called, mac_address=#{@mac_address} ==="
298
+ return {} unless @mac_address
299
+ { 'nic0' => @mac_address }
300
+ end
301
+
302
+ # Required by Foreman - VM description
303
+ def vm_description
304
+ Rails.logger.info '=== NUTANIX: NutanixCompute::vm_description called ==='
305
+ pretty_machine_type
306
+ end
307
+
308
+ # Required by Foreman - pretty machine type
309
+ def pretty_machine_type
310
+ Rails.logger.info '=== NUTANIX: NutanixCompute::pretty_machine_type called ==='
311
+ "#{@cpus} CPUs, #{memory}GB RAM"
312
+ end
313
+
314
+ # Required by Foreman - volumes/disks
315
+ def volumes
316
+ Rails.logger.info '=== NUTANIX: NutanixCompute::volumes called ==='
317
+ [OpenStruct.new({ name: 'disk-1', size_gb: 20 })]
318
+ end
319
+
320
+ # Required by Foreman - volumes_attributes setter
321
+ def volumes_attributes=(_attrs)
322
+ Rails.logger.info '=== NUTANIX: NutanixCompute::volumes_attributes= called ==='
323
+ end
324
+
325
+ # Required by Foreman - network interfaces
326
+ def interfaces
327
+ Rails.logger.info '=== NUTANIX: NutanixCompute::interfaces called ==='
328
+ [OpenStruct.new({
329
+ name: 'eth0',
330
+ network: @network,
331
+ mac: @mac_address,
332
+ ip: @vm_ip_addresses&.first,
333
+ })]
334
+ end
335
+
336
+ # Required by Foreman - network interfaces access
337
+ def network_interfaces
338
+ Rails.logger.info '=== NUTANIX: NutanixCompute::network_interfaces called ==='
339
+ interfaces
340
+ end
341
+
342
+ # Required by Foreman - select matching NIC from compute resource
343
+ # This is called by Foreman's match_macs_to_nics to assign MAC addresses
344
+ def select_nic(fog_nics, nic)
345
+ Rails.logger.info "=== NUTANIX: NutanixCompute::select_nic called with fog_nics=#{fog_nics.inspect}, nic=#{nic.inspect} ==="
346
+ # Return the first available NIC (we only have one for now)
347
+ fog_nics.shift
348
+ end
349
+
350
+ # Required by Foreman - console/serial output
351
+ def serial_port_output
352
+ Rails.logger.info '=== NUTANIX: NutanixCompute::serial_port_output called ==='
353
+ "Mock serial console output for #{@name}"
354
+ end
355
+
356
+ # Required by Foreman - wait for condition
357
+ def wait_for
358
+ Rails.logger.info '=== NUTANIX: NutanixCompute::wait_for called ==='
359
+ yield if block_given?
360
+ end
361
+
362
+ # Required by Foreman - destroy VM
363
+ def destroy
364
+ Rails.logger.info '=== NUTANIX: NutanixCompute::destroy called ==='
365
+ @persisted = false
366
+ true
367
+ end
368
+ end
369
+ end
370
+
@@ -0,0 +1,9 @@
1
+ <%= javascript "foreman_nutanix", "compute_resource" %>
2
+
3
+ <%= select_f f,
4
+ :cluster,
5
+ @compute_resource.available_clusters,
6
+ :ext_id,
7
+ :name,
8
+ { include_blank: _("Select Cluster") },
9
+ { help_inline: _("Nutanix cluster to use"), label: _("Cluster") } %>
@@ -0,0 +1,238 @@
1
+ <h4>Cluster Information</h4>
2
+ <table class="table table-striped table-condensed">
3
+ <% if @compute_resource.cluster_details %>
4
+ <% @compute_resource.cluster_details.to_h.each do |key, value| %>
5
+ <tr>
6
+ <td><%= key %></td>
7
+ <td><%= value %></td>
8
+ </tr>
9
+ <% end %>
10
+ <% else %>
11
+ <tr>
12
+ <td colspan="2">No cluster details available</td>
13
+ </tr>
14
+ <% end %>
15
+ </table>
16
+
17
+ <hr style="margin: 30px 0;">
18
+
19
+ <h4>Debug Information</h4>
20
+ <table class="table table-striped table-condensed">
21
+ <tr>
22
+ <td>Provider</td>
23
+ <td><%= @compute_resource.provider_friendly_name %></td>
24
+ </tr>
25
+ <tr>
26
+ <td>Capabilities</td>
27
+ <td><%= @compute_resource.capabilities.join(", ") %></td>
28
+ </tr>
29
+ <tr>
30
+ <td>User Data Support</td>
31
+ <td><%= @compute_resource.user_data_supported? ? "Yes" : "No" %></td>
32
+ </tr>
33
+ <tr>
34
+ <td>Shim Server URL</td>
35
+ <td><code><%= @compute_resource.shim_server_url %></code></td>
36
+ </tr>
37
+ <tr>
38
+ <td>Cluster ID</td>
39
+ <td><code><%= @compute_resource.cluster || "Not set" %></code></td>
40
+ </tr>
41
+ <tr>
42
+ <td>Cluster Name</td>
43
+ <td><%= @compute_resource.cluster_details&.name || "N/A" %></td>
44
+ </tr>
45
+ <tr>
46
+ <td>Resource Counts</td>
47
+ <td>
48
+ <strong>Networks:</strong>
49
+ <%= @compute_resource.available_networks.count %>
50
+ |
51
+ <strong>Storage Containers:</strong>
52
+ <%= @compute_resource.available_storage_containers.count %>
53
+ |
54
+ <strong>Images:</strong>
55
+ <%= @compute_resource.available_images.count %>
56
+ |
57
+ <strong>Flavors:</strong>
58
+ <%= @compute_resource.available_flavors.count %>
59
+ </td>
60
+ </tr>
61
+ </table>
62
+
63
+ <hr style="margin: 30px 0;">
64
+
65
+ <h4>Available Resources</h4>
66
+ <div class="row">
67
+ <div class="col-md-6">
68
+ <h5>Networks</h5>
69
+ <table class="table table-striped table-condensed">
70
+ <thead>
71
+ <tr>
72
+ <th>Name</th>
73
+ <th>Type</th>
74
+ <th>Subnet</th>
75
+ <th>Gateway</th>
76
+ </tr>
77
+ </thead>
78
+ <tbody>
79
+ <% @compute_resource.available_networks.each do |network| %>
80
+ <tr>
81
+ <td><%= network.name %></td>
82
+ <td><%= network.subnet_type %></td>
83
+ <td><%= network.ipv4_subnet %></td>
84
+ <td><%= network.ipv4_gateway %></td>
85
+ </tr>
86
+ <% end %>
87
+ <% if @compute_resource.available_networks.empty? %>
88
+ <tr>
89
+ <td colspan="4">No networks available</td>
90
+ </tr>
91
+ <% end %>
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ <div class="col-md-6">
96
+ <h5>Storage Containers</h5>
97
+ <table class="table table-striped table-condensed">
98
+ <thead>
99
+ <tr>
100
+ <th>Name</th>
101
+ <th>Capacity (GB)</th>
102
+ <th>Replication</th>
103
+ <th>Compression</th>
104
+ </tr>
105
+ </thead>
106
+ <tbody>
107
+ <% @compute_resource.available_storage_containers.each do |container| %>
108
+ <tr>
109
+ <td><%= container.name %></td>
110
+ <td><%= (container.max_capacity_bytes.to_f / 1024**3).round(2) %></td>
111
+ <td><%= container.replication_factor %>x</td>
112
+ <td><%= container.is_compression_enabled ? "Yes" : "No" %></td>
113
+ </tr>
114
+ <% end %>
115
+ <% if @compute_resource.available_storage_containers.empty? %>
116
+ <tr>
117
+ <td colspan="4">No storage containers available</td>
118
+ </tr>
119
+ <% end %>
120
+ </tbody>
121
+ </table>
122
+ </div>
123
+ </div>
124
+
125
+ <hr style="margin: 30px 0;">
126
+
127
+ <h4>Cluster Resource Availability</h4>
128
+ <% stats = @compute_resource.cluster_resource_stats %>
129
+ <% if stats %>
130
+ <table class="table table-striped table-condensed">
131
+ <thead>
132
+ <tr>
133
+ <th>Resource</th>
134
+ <th>Capacity</th>
135
+ <th>Used</th>
136
+ <th>Available</th>
137
+ <th>Usage</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody>
141
+ <tr>
142
+ <td><strong>CPU</strong></td>
143
+ <td><%= (stats.cpu_capacity_hz.to_f / 1_000_000_000).round(2) %>
144
+ GHz /
145
+ <%= stats.cpu_cores_total %>
146
+ cores</td>
147
+ <td><%= (stats.cpu_usage_hz.to_f / 1_000_000_000).round(2) %>
148
+ GHz /
149
+ <%= stats.cpu_cores_usage %>
150
+ cores</td>
151
+ <td><%= ((stats.cpu_capacity_hz - stats.cpu_usage_hz).to_f / 1_000_000_000).round(2) %>
152
+ GHz /
153
+ <%= stats.cpu_cores_total - stats.cpu_cores_usage %>
154
+ cores</td>
155
+ <td>
156
+ <div class="progress" style="margin-bottom: 0;">
157
+ <div
158
+ class="progress-bar <%= stats.cpu_usage_percent > 80 ? 'progress-bar-danger' : stats.cpu_usage_percent > 60 ? 'progress-bar-warning' : 'progress-bar-success' %>"
159
+ role="progressbar"
160
+ aria-valuenow="<%= stats.cpu_usage_percent %>"
161
+ aria-valuemin="0"
162
+ aria-valuemax="100"
163
+ style="width: <%= stats.cpu_usage_percent %>%"
164
+ >
165
+ <%= stats.cpu_usage_percent %>%
166
+ </div>
167
+ </div>
168
+ </td>
169
+ </tr>
170
+ <tr>
171
+ <td><strong>Memory</strong></td>
172
+ <td><%= (stats.memory_capacity_bytes.to_f / 1024**3).round(2) %>
173
+ GB</td>
174
+ <td><%= (stats.memory_usage_bytes.to_f / 1024**3).round(2) %>
175
+ GB</td>
176
+ <td><%= ((stats.memory_capacity_bytes - stats.memory_usage_bytes).to_f / 1024**3).round(
177
+ 2,
178
+ ) %>
179
+ GB</td>
180
+ <td>
181
+ <div class="progress" style="margin-bottom: 0;">
182
+ <div
183
+ class="progress-bar <%= stats.memory_usage_percent > 80 ? 'progress-bar-danger' : stats.memory_usage_percent > 60 ? 'progress-bar-warning' : 'progress-bar-success' %>"
184
+ role="progressbar"
185
+ aria-valuenow="<%= stats.memory_usage_percent %>"
186
+ aria-valuemin="0"
187
+ aria-valuemax="100"
188
+ style="width: <%= stats.memory_usage_percent %>%"
189
+ >
190
+ <%= stats.memory_usage_percent %>%
191
+ </div>
192
+ </div>
193
+ </td>
194
+ </tr>
195
+ <tr>
196
+ <td><strong>Storage</strong></td>
197
+ <td><%= (stats.storage_capacity_bytes.to_f / 1024**4).round(2) %>
198
+ TB</td>
199
+ <td><%= (stats.storage_usage_bytes.to_f / 1024**4).round(2) %>
200
+ TB</td>
201
+ <td><%= (
202
+ (stats.storage_capacity_bytes - stats.storage_usage_bytes).to_f / 1024**4
203
+ ).round(2) %>
204
+ TB</td>
205
+ <td>
206
+ <div class="progress" style="margin-bottom: 0;">
207
+ <div
208
+ class="progress-bar <%= stats.storage_usage_percent > 80 ? 'progress-bar-danger' : stats.storage_usage_percent > 60 ? 'progress-bar-warning' : 'progress-bar-success' %>"
209
+ role="progressbar"
210
+ aria-valuenow="<%= stats.storage_usage_percent %>"
211
+ aria-valuemin="0"
212
+ aria-valuemax="100"
213
+ style="width: <%= stats.storage_usage_percent %>%"
214
+ >
215
+ <%= stats.storage_usage_percent %>%
216
+ </div>
217
+ </div>
218
+ </td>
219
+ </tr>
220
+ </tbody>
221
+ </table>
222
+ <% else %>
223
+ <p class="text-muted">Unable to fetch cluster resource statistics</p>
224
+ <% end %>
225
+
226
+ <hr style="margin: 30px 0;">
227
+
228
+ <h4>Images</h4>
229
+ <% if @compute_resource.available_images.empty? %>
230
+ <p class="text-muted">No images available</p>
231
+ <% else %>
232
+ <ul>
233
+ <% @compute_resource.available_images.each do |image| %>
234
+ <li><%= image.name %>
235
+ (<%= image.id %>)</li>
236
+ <% end %>
237
+ </ul>
238
+ <% end %>
@@ -0,0 +1,72 @@
1
+ <%= compute_specific_js(compute_resource, "host_edit") %>
2
+ <%= javascript "hosts", "host_edit", "host_edit_interfaces" %>
3
+
4
+ <%= select_f f,
5
+ :image_id,
6
+ compute_resource.available_images,
7
+ :id,
8
+ :name,
9
+ { include_blank: _("Select Image") },
10
+ {
11
+ label: _("Image"),
12
+ help_inline: _("Operating system image to use (optional)"),
13
+ } %>
14
+
15
+ <%= select_f f,
16
+ :network_id,
17
+ compute_resource.available_networks,
18
+ :id,
19
+ :name,
20
+ { include_blank: _("Select Network") },
21
+ { label: _("Network"), help_inline: _("Network/subnet for the VM") } %>
22
+
23
+ <%= select_f f,
24
+ :storage_container,
25
+ compute_resource.available_storage_containers,
26
+ :id,
27
+ :name,
28
+ { include_blank: _("Select Storage Container") },
29
+ {
30
+ label: _("Storage Container"),
31
+ help_inline: _("Storage container for VM disks"),
32
+ } %>
33
+
34
+ <%= select_f f,
35
+ :machine_type,
36
+ compute_resource.available_flavors,
37
+ :id,
38
+ :name,
39
+ { include_blank: _("Select Flavor") },
40
+ { label: _("Machine Type"), help_inline: _("VM size template (optional)") } %>
41
+
42
+ <%= counter_f f,
43
+ :cpus,
44
+ { label: _("CPUs"), help_inline: _("Number of CPU cores"), min: 1, max: 32 } %>
45
+
46
+ <%= counter_f f,
47
+ :memory,
48
+ {
49
+ label: _("Memory (GB)"),
50
+ help_inline: _("Amount of memory in GB"),
51
+ min: 1,
52
+ max: 64,
53
+ } %>
54
+
55
+ <%= text_f f,
56
+ :disk_size_gb,
57
+ {
58
+ label: _("Disk Size (GB)"),
59
+ help_inline: _("Size of the boot disk in GB"),
60
+ value: 50,
61
+ } %>
62
+
63
+ <%= checkbox_f f,
64
+ :power_on,
65
+ {
66
+ label: _("Power On After Creation"),
67
+ help_inline:
68
+ _(
69
+ "Automatically power on the VM after it is created. If power-on fails, the VM will be deleted.",
70
+ ),
71
+ checked: true,
72
+ } %>
@@ -0,0 +1 @@
1
+ <%= text_f f, :size_gb, class: "col-md-2", label: _("Size (GB)"), label_size: "col-md-2", onchange: 'tfm.computeResource.capacityEdit(this)' %>