foreman_nutanix 0.1.4 → 0.1.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.
- checksums.yaml +4 -4
- data/Rakefile +0 -6
- data/app/lib/foreman_nutanix/nutanix_adapter.rb +14 -10
- data/app/models/foreman_nutanix/nutanix.rb +69 -63
- data/app/models/foreman_nutanix/nutanix_compute.rb +90 -42
- data/app/views/compute_resources_vms/form/nutanix/_base.html.erb +35 -0
- data/app/views/compute_resources_vms/show/_nutanix.html.erb +2 -1
- data/lib/foreman_nutanix/gpu_profile.rb +54 -0
- data/lib/foreman_nutanix/shim_client.rb +93 -0
- data/lib/foreman_nutanix/version.rb +1 -1
- data/lib/foreman_nutanix.rb +2 -0
- data/test/README.md +46 -0
- data/test/gpu_profile_test.rb +98 -0
- data/test/nutanix_compute_gpu_test.rb +207 -0
- data/test/nutanix_compute_test.rb +62 -0
- data/test/shim_client_test.rb +257 -0
- data/test/test_helper.rb +15 -0
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20eff365f58c477410d10f5b140961e4825738e49601442bec1740613aaf5704
|
|
4
|
+
data.tar.gz: bcefb95f0276c18905b569b187436a59a5e33276320b84238d4d6204e4ea3e6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 461cc7ed6c08843073891195b480c8356c148fadf2d2825aacfb0c8284167967abe1e30e37ac37a93c8cb102d86dc75cbab37ab93bca3789b4707cf87f63365e
|
|
7
|
+
data.tar.gz: bfea4f00a1a9148d6568014c9904b1afaef15172fb70033f386132fe83fd438b89896c3ecc8e6139104874f961908f9f5154cf0f7263caa0a112b9e5fea9acd6
|
data/Rakefile
CHANGED
|
@@ -20,8 +20,6 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
|
20
20
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
|
24
|
-
|
|
25
23
|
Bundler::GemHelper.install_tasks
|
|
26
24
|
|
|
27
25
|
require 'rake/testtask'
|
|
@@ -43,7 +41,3 @@ rescue StandardError => _e
|
|
|
43
41
|
rescue LoadError
|
|
44
42
|
puts 'Rubocop not loaded.'
|
|
45
43
|
end
|
|
46
|
-
|
|
47
|
-
task :default do
|
|
48
|
-
Rake::Task['rubocop'].execute
|
|
49
|
-
end
|
|
@@ -33,15 +33,13 @@ module ForemanNutanix
|
|
|
33
33
|
Rails.logger.info "=== NUTANIX: ServersCollection::get called with uuid: #{uuid} ==="
|
|
34
34
|
|
|
35
35
|
# Extract the actual UUID if it has a prefix
|
|
36
|
-
actual_uuid =
|
|
36
|
+
actual_uuid = ShimClient.normalize_uuid(uuid)
|
|
37
37
|
|
|
38
38
|
# Fetch full VM details from shim server
|
|
39
|
-
|
|
40
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}")
|
|
41
|
-
response = Net::HTTP.get_response(uri)
|
|
39
|
+
response = shim.get("/api/v1/vmm/vms/#{actual_uuid}")
|
|
42
40
|
|
|
43
|
-
if response.
|
|
44
|
-
data =
|
|
41
|
+
if response.success?
|
|
42
|
+
data = response.json
|
|
45
43
|
|
|
46
44
|
total_cpus = (data['num_sockets'] || 1) * (data['num_cores_per_socket'] || 1)
|
|
47
45
|
memory_gb = data['memory_size_bytes'] ? (data['memory_size_bytes'].to_f / 1024**3).round : 4
|
|
@@ -82,10 +80,7 @@ module ForemanNutanix
|
|
|
82
80
|
Rails.logger.info "=== NUTANIX: ServersCollection::all called with opts: #{opts} ==="
|
|
83
81
|
|
|
84
82
|
# Fetch VMs from shim server (now includes MAC and IP)
|
|
85
|
-
|
|
86
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/list-vms")
|
|
87
|
-
response = Net::HTTP.get_response(uri)
|
|
88
|
-
data = JSON.parse(response.body)
|
|
83
|
+
data = shim.get('/api/v1/vmm/list-vms').json
|
|
89
84
|
|
|
90
85
|
# Filter VMs by cluster
|
|
91
86
|
filtered_data = data.select { |vm| vm['cluster_ext_id'] == @cluster }
|
|
@@ -106,6 +101,7 @@ module ForemanNutanix
|
|
|
106
101
|
ip_addresses: vm_data['ip_addresses'] || [],
|
|
107
102
|
create_time: vm_data['create_time'],
|
|
108
103
|
disk_size_gb: vm_data['disk_size_bytes'].to_i / (1024**3),
|
|
104
|
+
gpus: vm_data['gpus'],
|
|
109
105
|
})
|
|
110
106
|
vm.instance_variable_set(:@persisted, true)
|
|
111
107
|
vm
|
|
@@ -114,6 +110,14 @@ module ForemanNutanix
|
|
|
114
110
|
Rails.logger.error "=== NUTANIX: Error fetching VMs: #{e.message} ==="
|
|
115
111
|
[]
|
|
116
112
|
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# ServersCollection holds no reference to the ComputeResource, so it builds
|
|
117
|
+
# its own client off the shim server address in the environment.
|
|
118
|
+
def shim
|
|
119
|
+
@shim ||= ShimClient.new
|
|
120
|
+
end
|
|
117
121
|
end
|
|
118
122
|
end
|
|
119
123
|
end
|
|
@@ -40,8 +40,9 @@ module ForemanNutanix
|
|
|
40
40
|
"#{name} (#{provider_friendly_name})"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Kept public: app/views/compute_resources/show/_nutanix.html.erb renders it.
|
|
43
44
|
def shim_server_url
|
|
44
|
-
|
|
45
|
+
ShimClient.default_base_url
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def provided_attributes
|
|
@@ -56,10 +57,7 @@ module ForemanNutanix
|
|
|
56
57
|
|
|
57
58
|
# Available clusters for selection
|
|
58
59
|
def available_clusters
|
|
59
|
-
|
|
60
|
-
uri = URI("#{base.chomp('/')}/api/v1/clustermgmt/list-clusters")
|
|
61
|
-
response = Net::HTTP.get_response(uri)
|
|
62
|
-
data = JSON.parse(response.body)
|
|
60
|
+
data = shim.get('/api/v1/clustermgmt/list-clusters').json
|
|
63
61
|
|
|
64
62
|
data.map do |cluster|
|
|
65
63
|
cluster[:name] = "#{cluster['name']} (#{cluster['arch']})"
|
|
@@ -80,10 +78,7 @@ module ForemanNutanix
|
|
|
80
78
|
# Available networks for VMs
|
|
81
79
|
def available_networks
|
|
82
80
|
Rails.logger.info '=== NUTANIX: Fetching available networks from shim server ==='
|
|
83
|
-
|
|
84
|
-
uri = URI("#{base.chomp('/')}/api/v1/networking/list-networks")
|
|
85
|
-
response = Net::HTTP.get_response(uri)
|
|
86
|
-
data = JSON.parse(response.body)
|
|
81
|
+
data = shim.get('/api/v1/networking/list-networks').json
|
|
87
82
|
|
|
88
83
|
# Filter networks by the cluster associated with this compute resource
|
|
89
84
|
cluster_ext_id = cluster
|
|
@@ -114,10 +109,7 @@ module ForemanNutanix
|
|
|
114
109
|
# Available storage containers for VMs
|
|
115
110
|
def available_storage_containers
|
|
116
111
|
Rails.logger.info '=== NUTANIX: Fetching available storage containers from shim server ==='
|
|
117
|
-
|
|
118
|
-
uri = URI("#{base.chomp('/')}/api/v1/clustermgmt/list-storage-containers")
|
|
119
|
-
response = Net::HTTP.get_response(uri)
|
|
120
|
-
data = JSON.parse(response.body)
|
|
112
|
+
data = shim.get('/api/v1/clustermgmt/list-storage-containers').json
|
|
121
113
|
|
|
122
114
|
# Filter storage containers by the cluster associated with this compute resource
|
|
123
115
|
cluster_ext_id = cluster
|
|
@@ -143,16 +135,55 @@ module ForemanNutanix
|
|
|
143
135
|
[]
|
|
144
136
|
end
|
|
145
137
|
|
|
138
|
+
# Physical GPU profiles on this cluster, as options for the VM form's single
|
|
139
|
+
# GPU select.
|
|
140
|
+
#
|
|
141
|
+
# One select can only post one value, so the option id is a composite of the
|
|
142
|
+
# three things the shim's provision call needs:
|
|
143
|
+
# "<device_id>:<vendor_name>:<gpu_type>", e.g. "7864:NVIDIA:PASSTHROUGH_GRAPHICS".
|
|
144
|
+
# NutanixCompute#save splits it back apart.
|
|
145
|
+
#
|
|
146
|
+
# A profile ext_id is deliberately NOT the value: it pins the VM to one
|
|
147
|
+
# physical card, which fails the moment that card is busy. A device_id names
|
|
148
|
+
# a GPU model, so Nutanix schedules any free matching card.
|
|
149
|
+
def available_gpus
|
|
150
|
+
Rails.logger.info '=== NUTANIX: Fetching available GPU profiles from shim server ==='
|
|
151
|
+
cluster_ext_id = cluster
|
|
152
|
+
return [] unless cluster_ext_id
|
|
153
|
+
|
|
154
|
+
data = shim.get("/api/v1/clustermgmt/clusters/#{cluster_ext_id}/gpu-profiles").json
|
|
155
|
+
|
|
156
|
+
# Passthrough only, and only profiles whose composite round-trips. See
|
|
157
|
+
# ForemanNutanix::GpuProfile.usable? - an unusable profile offered here
|
|
158
|
+
# would provision a VM with no GPU and still report success.
|
|
159
|
+
filtered_data = data.select { |profile| GpuProfile.usable?(profile) }
|
|
160
|
+
Rails.logger.info "=== NUTANIX: GPU profiles - total: #{data.count}, usable: #{filtered_data.count} ==="
|
|
161
|
+
|
|
162
|
+
filtered_data.map do |profile|
|
|
163
|
+
OpenStruct.new({
|
|
164
|
+
id: GpuProfile.build(profile),
|
|
165
|
+
ext_id: profile['ext_id'],
|
|
166
|
+
name: GpuProfile.label(profile),
|
|
167
|
+
device_id: profile['device_id'],
|
|
168
|
+
device_name: profile['device_name'],
|
|
169
|
+
vendor_name: profile['vendor_name'],
|
|
170
|
+
gpu_type: profile['gpu_type'],
|
|
171
|
+
assignable: profile['assignable'],
|
|
172
|
+
is_in_use: profile['is_in_use'],
|
|
173
|
+
})
|
|
174
|
+
end
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
Rails.logger.error "=== NUTANIX: Error fetching GPU profiles: #{e.message} ==="
|
|
177
|
+
[]
|
|
178
|
+
end
|
|
179
|
+
|
|
146
180
|
# Cluster resource statistics (CPU, memory, storage usage)
|
|
147
181
|
def cluster_resource_stats
|
|
148
182
|
Rails.logger.info '=== NUTANIX: Fetching cluster resource stats from shim server ==='
|
|
149
|
-
base = ENV['NUTANIX_SHIM_SERVER_ADDR'] || 'http://localhost:8000'
|
|
150
183
|
cluster_id = cluster
|
|
151
184
|
return nil unless cluster_id
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
response = Net::HTTP.get_response(uri)
|
|
155
|
-
data = JSON.parse(response.body)
|
|
186
|
+
data = shim.get("/api/v1/clustermgmt/clusters/#{cluster_id}/stats").json
|
|
156
187
|
|
|
157
188
|
OpenStruct.new(data)
|
|
158
189
|
rescue StandardError => e
|
|
@@ -163,10 +194,7 @@ module ForemanNutanix
|
|
|
163
194
|
# Available images
|
|
164
195
|
def available_images(_opts = {})
|
|
165
196
|
Rails.logger.info '=== NUTANIX: Fetching available images from shim server ==='
|
|
166
|
-
|
|
167
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/list-images")
|
|
168
|
-
response = Net::HTTP.get_response(uri)
|
|
169
|
-
data = JSON.parse(response.body)
|
|
197
|
+
data = shim.get('/api/v1/vmm/list-images').json
|
|
170
198
|
|
|
171
199
|
# Filter images by cluster if cluster_location_ext_ids is available
|
|
172
200
|
cluster_ext_id = cluster
|
|
@@ -286,20 +314,10 @@ module ForemanNutanix
|
|
|
286
314
|
# Start VM - called by Foreman for power on
|
|
287
315
|
def start_vm(uuid)
|
|
288
316
|
Rails.logger.info "=== NUTANIX: START_VM CALLED with uuid: #{uuid} ==="
|
|
289
|
-
actual_uuid =
|
|
317
|
+
actual_uuid = ShimClient.normalize_uuid(uuid)
|
|
290
318
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
295
|
-
http.use_ssl = uri.scheme == 'https'
|
|
296
|
-
|
|
297
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
298
|
-
request['Content-Type'] = 'application/json'
|
|
299
|
-
request.body = { action: 'POWER_ON' }.to_json
|
|
300
|
-
|
|
301
|
-
response = http.request(request)
|
|
302
|
-
response.is_a?(Net::HTTPSuccess)
|
|
319
|
+
response = shim.post("/api/v1/vmm/vms/#{actual_uuid}/power-state", { action: 'POWER_ON' })
|
|
320
|
+
response.success?
|
|
303
321
|
rescue StandardError => e
|
|
304
322
|
Rails.logger.error "=== NUTANIX: START_VM ERROR: #{e.message} ==="
|
|
305
323
|
raise e
|
|
@@ -308,20 +326,10 @@ module ForemanNutanix
|
|
|
308
326
|
# Stop VM - called by Foreman for power off
|
|
309
327
|
def stop_vm(uuid)
|
|
310
328
|
Rails.logger.info "=== NUTANIX: STOP_VM CALLED with uuid: #{uuid} ==="
|
|
311
|
-
actual_uuid =
|
|
312
|
-
|
|
313
|
-
base = shim_server_url
|
|
314
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
|
|
315
|
-
|
|
316
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
317
|
-
http.use_ssl = uri.scheme == 'https'
|
|
318
|
-
|
|
319
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
320
|
-
request['Content-Type'] = 'application/json'
|
|
321
|
-
request.body = { action: 'POWER_OFF' }.to_json
|
|
329
|
+
actual_uuid = ShimClient.normalize_uuid(uuid)
|
|
322
330
|
|
|
323
|
-
response =
|
|
324
|
-
response.
|
|
331
|
+
response = shim.post("/api/v1/vmm/vms/#{actual_uuid}/power-state", { action: 'POWER_OFF' })
|
|
332
|
+
response.success?
|
|
325
333
|
rescue StandardError => e
|
|
326
334
|
Rails.logger.error "=== NUTANIX: STOP_VM ERROR: #{e.message} ==="
|
|
327
335
|
raise e
|
|
@@ -331,14 +339,12 @@ module ForemanNutanix
|
|
|
331
339
|
def vm_power_state(vm)
|
|
332
340
|
Rails.logger.info "=== NUTANIX: VM_POWER_STATE CALLED for vm: #{vm} ==="
|
|
333
341
|
uuid = vm.respond_to?(:identity) ? vm.identity : vm.to_s
|
|
334
|
-
actual_uuid =
|
|
342
|
+
actual_uuid = ShimClient.normalize_uuid(uuid)
|
|
335
343
|
|
|
336
|
-
|
|
337
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
|
|
338
|
-
response = Net::HTTP.get_response(uri)
|
|
344
|
+
response = shim.get("/api/v1/vmm/vms/#{actual_uuid}/power-state")
|
|
339
345
|
|
|
340
|
-
if response.
|
|
341
|
-
data =
|
|
346
|
+
if response.success?
|
|
347
|
+
data = response.json
|
|
342
348
|
state = data['power_state']
|
|
343
349
|
Rails.logger.info "=== NUTANIX: VM_POWER_STATE returning: #{state} ==="
|
|
344
350
|
# Return hash that Foreman expects
|
|
@@ -375,19 +381,12 @@ module ForemanNutanix
|
|
|
375
381
|
return true if uuid.nil? || uuid.to_s.strip.empty?
|
|
376
382
|
|
|
377
383
|
# Extract the actual UUID if it has a prefix
|
|
378
|
-
actual_uuid =
|
|
384
|
+
actual_uuid = ShimClient.normalize_uuid(uuid)
|
|
379
385
|
|
|
380
386
|
# Call the shim server to delete the VM
|
|
381
|
-
|
|
382
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}")
|
|
387
|
+
response = shim.delete("/api/v1/vmm/vms/#{actual_uuid}")
|
|
383
388
|
|
|
384
|
-
|
|
385
|
-
http.use_ssl = uri.scheme == 'https'
|
|
386
|
-
|
|
387
|
-
request = Net::HTTP::Delete.new(uri.path)
|
|
388
|
-
response = http.request(request)
|
|
389
|
-
|
|
390
|
-
if response.is_a?(Net::HTTPNoContent) || response.is_a?(Net::HTTPSuccess)
|
|
389
|
+
if response.no_content? || response.success?
|
|
391
390
|
Rails.logger.info "=== NUTANIX: VM #{actual_uuid} deleted successfully ==="
|
|
392
391
|
true
|
|
393
392
|
else
|
|
@@ -455,6 +454,13 @@ module ForemanNutanix
|
|
|
455
454
|
|
|
456
455
|
private
|
|
457
456
|
|
|
457
|
+
# "NVIDIA Tesla T4 (2 assignable)". Every part is optional in the shim's
|
|
458
|
+
# payload, so degrade instead of rendering a blank option.
|
|
459
|
+
|
|
460
|
+
def shim
|
|
461
|
+
@shim ||= ShimClient.new(shim_server_url)
|
|
462
|
+
end
|
|
463
|
+
|
|
458
464
|
def client
|
|
459
465
|
Rails.logger.info "=== NUTANIX: Creating client for cluster #{cluster} ==="
|
|
460
466
|
@client ||= NutanixAdapter.new(cluster)
|
|
@@ -7,7 +7,7 @@ module ForemanNutanix
|
|
|
7
7
|
:storage_container_ext_id, :num_sockets, :num_cores_per_socket,
|
|
8
8
|
:disk_size_bytes, :description, :network_id, :storage_container,
|
|
9
9
|
:disk_size_gb, :mac_address, :vm_ip_addresses, :create_time,
|
|
10
|
-
:boot_method, :secure_boot, :gpus
|
|
10
|
+
:boot_method, :secure_boot, :gpus, :gpu, :gpu_count
|
|
11
11
|
|
|
12
12
|
def initialize(cluster = nil, args = {})
|
|
13
13
|
Rails.logger.info "=== NUTANIX: NutanixCompute::initialize cluster=#{cluster} args=#{args} ==="
|
|
@@ -46,8 +46,17 @@ module ForemanNutanix
|
|
|
46
46
|
@boot_method = args[:boot_method]
|
|
47
47
|
@secure_boot = args[:secure_boot]
|
|
48
48
|
|
|
49
|
-
# GPUs
|
|
50
|
-
|
|
49
|
+
# GPUs. Defaults to [] rather than nil: the shim's /list-vms response
|
|
50
|
+
# carries no gpus field, so VMs built by ServersCollection#all never
|
|
51
|
+
# receive one, and the index view calls #gpus.empty? on every row.
|
|
52
|
+
@gpus = args[:gpus] || []
|
|
53
|
+
|
|
54
|
+
# GPU passthrough selection. `gpu` is the composite the form's single
|
|
55
|
+
# select carries: "<device_id>:<vendor_name>:<gpu_type>". Both stay nil
|
|
56
|
+
# when nothing was selected, which is what keeps #save's payload
|
|
57
|
+
# byte-identical to a GPU-less provision.
|
|
58
|
+
@gpu = args[:gpu]
|
|
59
|
+
@gpu_count = args[:gpu_count]
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
# Required by Foreman - indicates if VM exists
|
|
@@ -96,23 +105,17 @@ module ForemanNutanix
|
|
|
96
105
|
boot_method: @boot_method,
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
# Adds nothing at all unless a GPU was actually selected, so a GPU-less
|
|
109
|
+
# provision posts exactly the request it always did.
|
|
110
|
+
provision_request.merge!(gpu_provision_fields)
|
|
111
|
+
|
|
99
112
|
Rails.logger.info "=== NUTANIX: Provisioning VM with request: #{provision_request} ==="
|
|
100
113
|
|
|
101
114
|
# Call the shim server to provision the VM
|
|
102
|
-
|
|
103
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/provision-vm")
|
|
104
|
-
|
|
105
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
106
|
-
http.use_ssl = uri.scheme == 'https'
|
|
107
|
-
|
|
108
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
109
|
-
request['Content-Type'] = 'application/json'
|
|
110
|
-
request.body = provision_request.to_json
|
|
115
|
+
response = shim.post('/api/v1/vmm/provision-vm', provision_request)
|
|
111
116
|
|
|
112
|
-
response
|
|
113
|
-
|
|
114
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
115
|
-
result = JSON.parse(response.body)
|
|
117
|
+
if response.success?
|
|
118
|
+
result = response.json
|
|
116
119
|
Rails.logger.info "=== NUTANIX: VM provisioned successfully: #{result} ==="
|
|
117
120
|
|
|
118
121
|
# Update identity with the real ext_id from Nutanix
|
|
@@ -176,22 +179,12 @@ module ForemanNutanix
|
|
|
176
179
|
return false unless persisted?
|
|
177
180
|
|
|
178
181
|
# Extract actual UUID (handle ZXJnb24=:uuid format)
|
|
179
|
-
actual_uuid =
|
|
182
|
+
actual_uuid = ShimClient.normalize_uuid(@identity)
|
|
180
183
|
|
|
181
184
|
# Call the shim server to power on the VM
|
|
182
|
-
|
|
183
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
|
|
184
|
-
|
|
185
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
186
|
-
http.use_ssl = uri.scheme == 'https'
|
|
187
|
-
|
|
188
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
189
|
-
request['Content-Type'] = 'application/json'
|
|
190
|
-
request.body = { action: 'POWER_ON' }.to_json
|
|
185
|
+
response = shim.post("/api/v1/vmm/vms/#{actual_uuid}/power-state", { action: 'POWER_ON' })
|
|
191
186
|
|
|
192
|
-
response
|
|
193
|
-
|
|
194
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
187
|
+
if response.success?
|
|
195
188
|
Rails.logger.info "=== NUTANIX: VM #{actual_uuid} powered on successfully ==="
|
|
196
189
|
@power_state = 'ON'
|
|
197
190
|
true
|
|
@@ -211,22 +204,12 @@ module ForemanNutanix
|
|
|
211
204
|
return false unless persisted?
|
|
212
205
|
|
|
213
206
|
# Extract actual UUID (handle ZXJnb24=:uuid format)
|
|
214
|
-
actual_uuid =
|
|
207
|
+
actual_uuid = ShimClient.normalize_uuid(@identity)
|
|
215
208
|
|
|
216
209
|
# Call the shim server to power off the VM
|
|
217
|
-
|
|
218
|
-
uri = URI("#{base.chomp('/')}/api/v1/vmm/vms/#{actual_uuid}/power-state")
|
|
219
|
-
|
|
220
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
221
|
-
http.use_ssl = uri.scheme == 'https'
|
|
222
|
-
|
|
223
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
224
|
-
request['Content-Type'] = 'application/json'
|
|
225
|
-
request.body = { action: 'POWER_OFF' }.to_json
|
|
210
|
+
response = shim.post("/api/v1/vmm/vms/#{actual_uuid}/power-state", { action: 'POWER_OFF' })
|
|
226
211
|
|
|
227
|
-
response
|
|
228
|
-
|
|
229
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
212
|
+
if response.success?
|
|
230
213
|
Rails.logger.info "=== NUTANIX: VM #{actual_uuid} powered off successfully ==="
|
|
231
214
|
@power_state = 'OFF'
|
|
232
215
|
true
|
|
@@ -240,6 +223,20 @@ module ForemanNutanix
|
|
|
240
223
|
raise e
|
|
241
224
|
end
|
|
242
225
|
|
|
226
|
+
# Human-readable GPU labels for the views, e.g. "NVIDIA Tesla T4".
|
|
227
|
+
# The shim returns each GPU as a hash; a shim older than 0.1.5 omits the
|
|
228
|
+
# field on /list-vms entirely, which #initialize turns into [].
|
|
229
|
+
def gpu_labels
|
|
230
|
+
@gpus.map do |gpu|
|
|
231
|
+
next gpu.to_s unless gpu.is_a?(Hash)
|
|
232
|
+
|
|
233
|
+
name = gpu['name']
|
|
234
|
+
name = gpu['device_id'] if blank_gpu_field?(name)
|
|
235
|
+
label = [gpu['vendor'], name].reject { |part| blank_gpu_field?(part) }.join(' ')
|
|
236
|
+
label.empty? ? 'GPU' : label
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
243
240
|
# Required by Foreman - CPU count
|
|
244
241
|
def cpu
|
|
245
242
|
Rails.logger.info '=== NUTANIX: NutanixCompute::cpu called ==='
|
|
@@ -377,11 +374,62 @@ module ForemanNutanix
|
|
|
377
374
|
yield if block_given?
|
|
378
375
|
end
|
|
379
376
|
|
|
377
|
+
# Deliberately not ActiveSupport's #blank?: this class is loaded without
|
|
378
|
+
# Rails in test/nutanix_compute_test.rb.
|
|
379
|
+
def blank_gpu_field?(value)
|
|
380
|
+
value.nil? || value.to_s.strip.empty?
|
|
381
|
+
end
|
|
382
|
+
private :blank_gpu_field?
|
|
383
|
+
|
|
380
384
|
# Required by Foreman - destroy VM
|
|
381
385
|
def destroy
|
|
382
386
|
Rails.logger.info '=== NUTANIX: NutanixCompute::destroy called ==='
|
|
383
387
|
@persisted = false
|
|
384
388
|
true
|
|
385
389
|
end
|
|
390
|
+
|
|
391
|
+
private
|
|
392
|
+
|
|
393
|
+
# The form's GPU select carries a single composite value,
|
|
394
|
+
# "<device_id>:<vendor_name>:<gpu_type>" (e.g. "7864:NVIDIA:PASSTHROUGH_GRAPHICS"),
|
|
395
|
+
# because one select cannot post three fields. Split it back apart here.
|
|
396
|
+
#
|
|
397
|
+
# Returns {} unless all three parts are present: a half-parsed composite
|
|
398
|
+
# must contribute nothing rather than provision a VM against a partially
|
|
399
|
+
# specified GPU. {} is also the no-GPU-selected case, which is what keeps
|
|
400
|
+
# the provision request byte-identical to a pre-GPU one.
|
|
401
|
+
def gpu_provision_fields
|
|
402
|
+
# Nothing selected is the common case and means exactly that.
|
|
403
|
+
return {} if GpuProfile.blank?(@gpu)
|
|
404
|
+
|
|
405
|
+
# Selected but unparseable is different: the user asked for a GPU. Dropping
|
|
406
|
+
# it here would build a GPU-less VM and report the build as successful, so
|
|
407
|
+
# fail the way a missing network or storage container does.
|
|
408
|
+
parts = GpuProfile.parse(@gpu)
|
|
409
|
+
if parts.nil?
|
|
410
|
+
raise StandardError, "Malformed GPU selection #{@gpu.inspect}; expected " \
|
|
411
|
+
"'<device_id>:<vendor>:<gpu_type>'"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
device_id, vendor, mode = parts
|
|
415
|
+
|
|
416
|
+
# The count field is hidden until a GPU is picked, so it routinely arrives
|
|
417
|
+
# nil or empty; one GPU is what selecting a GPU means.
|
|
418
|
+
count = @gpu_count.to_i
|
|
419
|
+
count = 1 if count < 1
|
|
420
|
+
|
|
421
|
+
{
|
|
422
|
+
gpu_device_id: device_id.to_i,
|
|
423
|
+
gpu_vendor: vendor,
|
|
424
|
+
gpu_mode: mode,
|
|
425
|
+
gpu_count: count,
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# NutanixCompute holds no reference to the ComputeResource, so it builds its
|
|
430
|
+
# own client off the shim server address in the environment.
|
|
431
|
+
def shim
|
|
432
|
+
@shim ||= ShimClient.new
|
|
433
|
+
end
|
|
386
434
|
end
|
|
387
435
|
end
|
|
@@ -66,6 +66,32 @@ compute_resource.available_storage_containers,
|
|
|
66
66
|
max: 10_000,
|
|
67
67
|
} %>
|
|
68
68
|
|
|
69
|
+
<%# One select for three shim fields: the option value is the composite
|
|
70
|
+
# "<device_id>:<vendor_name>:<gpu_type>". NutanixCompute#save splits it.
|
|
71
|
+
# Passthrough only - available_gpus filters vGPU profiles out. %>
|
|
72
|
+
<%= select_f f,
|
|
73
|
+
:gpu,
|
|
74
|
+
compute_resource.available_gpus,
|
|
75
|
+
:id,
|
|
76
|
+
:name,
|
|
77
|
+
{ include_blank: _("No GPU"), selected: f.object.try(:gpu) },
|
|
78
|
+
{
|
|
79
|
+
label: _("GPU"),
|
|
80
|
+
help_inline: _("Passthrough GPU to attach when the VM is created"),
|
|
81
|
+
id: "gpu-select",
|
|
82
|
+
} %>
|
|
83
|
+
|
|
84
|
+
<div id="gpu-options" class="hidden">
|
|
85
|
+
<%= counter_f f,
|
|
86
|
+
:gpu_count,
|
|
87
|
+
{
|
|
88
|
+
label: _("GPU Count"),
|
|
89
|
+
help_inline: _("How many of the selected GPU to attach"),
|
|
90
|
+
min: 1,
|
|
91
|
+
max: 8,
|
|
92
|
+
} %>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
69
95
|
<%# `checked:` must reflect the saved value, else the box always renders
|
|
70
96
|
# checked and the compute profile's "off" never survives a page render.
|
|
71
97
|
# f.object is compute_resource.new_vm(vm_attrs); NutanixCompute has no :start
|
|
@@ -106,11 +132,20 @@ compute_resource.available_boot_methods,
|
|
|
106
132
|
|
|
107
133
|
<script>
|
|
108
134
|
var $bootMethod = $('#boot-method-select');
|
|
135
|
+
var $gpu = $('#gpu-select');
|
|
109
136
|
|
|
110
137
|
function toggleUefiOptions() {
|
|
111
138
|
$('#uefi-options').toggleClass('hidden', $bootMethod.val() !== 'uefi');
|
|
112
139
|
}
|
|
113
140
|
|
|
141
|
+
// Same pattern as above: the count only means anything once a GPU is picked,
|
|
142
|
+
// and the blank "No GPU" option posts an empty value.
|
|
143
|
+
function toggleGpuOptions() {
|
|
144
|
+
$('#gpu-options').toggleClass('hidden', !$gpu.val());
|
|
145
|
+
}
|
|
146
|
+
|
|
114
147
|
toggleUefiOptions();
|
|
148
|
+
toggleGpuOptions();
|
|
115
149
|
$bootMethod.on('change', toggleUefiOptions);
|
|
150
|
+
$gpu.on('change', toggleGpuOptions);
|
|
116
151
|
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ForemanNutanix
|
|
2
|
+
# The compute profile form is a single select, so a GPU choice is stored as one
|
|
3
|
+
# composite string: "<device_id>:<vendor_name>:<gpu_type>". This module owns that
|
|
4
|
+
# format at both ends - Nutanix#available_gpus builds it, NutanixCompute#save
|
|
5
|
+
# parses it - so the two can never disagree about it.
|
|
6
|
+
#
|
|
7
|
+
# Plain Ruby, no Rails: loaded directly by the standalone tests. See test/README.md.
|
|
8
|
+
module GpuProfile
|
|
9
|
+
SEPARATOR = ':'.freeze
|
|
10
|
+
PARTS = %w[device_id vendor_name gpu_type].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def blank?(value)
|
|
15
|
+
value.nil? || value.to_s.strip.empty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Only offer a profile that can survive the round trip: passthrough (the shim
|
|
19
|
+
# rejects VIRTUAL), and every composite part present. A profile missing one
|
|
20
|
+
# part builds a composite that #parse rejects, which would provision a VM with
|
|
21
|
+
# no GPU while reporting success.
|
|
22
|
+
def usable?(profile)
|
|
23
|
+
return false unless profile[PARTS.last].to_s.start_with?('PASSTHROUGH')
|
|
24
|
+
|
|
25
|
+
PARTS.none? { |part| blank?(profile[part]) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build(profile)
|
|
29
|
+
PARTS.map { |part| profile[part] }.join(SEPARATOR)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# [device_id, vendor, mode], or nil if this is not a usable composite.
|
|
33
|
+
# Caller distinguishes "nothing selected" from "malformed" by checking blank?
|
|
34
|
+
# first - the two mean very different things at provision time.
|
|
35
|
+
def parse(composite)
|
|
36
|
+
return nil if blank?(composite)
|
|
37
|
+
|
|
38
|
+
parts = composite.to_s.split(SEPARATOR, PARTS.length)
|
|
39
|
+
return nil unless parts.length == PARTS.length
|
|
40
|
+
return nil if parts.any? { |part| blank?(part) }
|
|
41
|
+
|
|
42
|
+
parts
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def label(profile)
|
|
46
|
+
name = [profile['vendor_name'], profile['device_name']]
|
|
47
|
+
.reject { |part| blank?(part) }
|
|
48
|
+
.join(' ')
|
|
49
|
+
name = "GPU #{profile['device_id']}" if name.empty?
|
|
50
|
+
assignable = profile['assignable']
|
|
51
|
+
assignable.nil? ? name : "#{name} (#{assignable} assignable)"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|