foreman_nutanix 0.1.5 → 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/app/models/foreman_nutanix/nutanix.rb +45 -0
- data/app/models/foreman_nutanix/nutanix_compute.rb +48 -1
- data/app/views/compute_resources_vms/form/nutanix/_base.html.erb +35 -0
- data/lib/foreman_nutanix/gpu_profile.rb +54 -0
- data/lib/foreman_nutanix/version.rb +1 -1
- data/lib/foreman_nutanix.rb +1 -0
- data/test/README.md +3 -2
- data/test/gpu_profile_test.rb +98 -0
- data/test/nutanix_compute_gpu_test.rb +207 -0
- data/test/nutanix_compute_test.rb +0 -9
- data/test/test_helper.rb +12 -2
- metadata +6 -1
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
|
|
@@ -135,6 +135,48 @@ module ForemanNutanix
|
|
|
135
135
|
[]
|
|
136
136
|
end
|
|
137
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
|
+
|
|
138
180
|
# Cluster resource statistics (CPU, memory, storage usage)
|
|
139
181
|
def cluster_resource_stats
|
|
140
182
|
Rails.logger.info '=== NUTANIX: Fetching cluster resource stats from shim server ==='
|
|
@@ -412,6 +454,9 @@ module ForemanNutanix
|
|
|
412
454
|
|
|
413
455
|
private
|
|
414
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
|
+
|
|
415
460
|
def shim
|
|
416
461
|
@shim ||= ShimClient.new(shim_server_url)
|
|
417
462
|
end
|
|
@@ -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} ==="
|
|
@@ -50,6 +50,13 @@ module ForemanNutanix
|
|
|
50
50
|
# carries no gpus field, so VMs built by ServersCollection#all never
|
|
51
51
|
# receive one, and the index view calls #gpus.empty? on every row.
|
|
52
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]
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
# Required by Foreman - indicates if VM exists
|
|
@@ -98,6 +105,10 @@ module ForemanNutanix
|
|
|
98
105
|
boot_method: @boot_method,
|
|
99
106
|
}
|
|
100
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
|
+
|
|
101
112
|
Rails.logger.info "=== NUTANIX: Provisioning VM with request: #{provision_request} ==="
|
|
102
113
|
|
|
103
114
|
# Call the shim server to provision the VM
|
|
@@ -379,6 +390,42 @@ module ForemanNutanix
|
|
|
379
390
|
|
|
380
391
|
private
|
|
381
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
|
+
|
|
382
429
|
# NutanixCompute holds no reference to the ComputeResource, so it builds its
|
|
383
430
|
# own client off the shim server address in the environment.
|
|
384
431
|
def shim
|
|
@@ -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
|
data/lib/foreman_nutanix.rb
CHANGED
data/test/README.md
CHANGED
|
@@ -9,8 +9,9 @@ bundle exec rake test
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
`test_helper.rb` requires only `lib/foreman_nutanix/shim_client.rb`, which is
|
|
12
|
-
deliberately plain Ruby (net/http, json, uri)
|
|
13
|
-
suite boots in milliseconds with nothing
|
|
12
|
+
deliberately plain Ruby (net/http, json, uri), plus a five-line `Rails.logger`
|
|
13
|
+
stub. That is the whole reason the suite boots in milliseconds with nothing
|
|
14
|
+
installed but minitest and rake.
|
|
14
15
|
|
|
15
16
|
## What this covers
|
|
16
17
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'foreman_nutanix/gpu_profile'
|
|
3
|
+
|
|
4
|
+
# A profile as it arrives from the shim's /gpu-profiles endpoint.
|
|
5
|
+
def profile(overrides = {})
|
|
6
|
+
{
|
|
7
|
+
'ext_id' => '0005a1b2-1111-2222-3333-444455556666',
|
|
8
|
+
'device_id' => 7864,
|
|
9
|
+
'device_name' => 'Tesla T4',
|
|
10
|
+
'vendor_name' => 'NVIDIA',
|
|
11
|
+
'gpu_type' => 'PASSTHROUGH_GRAPHICS',
|
|
12
|
+
'assignable' => 3,
|
|
13
|
+
'is_in_use' => false,
|
|
14
|
+
}.merge(overrides)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class GpuProfileUsableTest < Minitest::Test
|
|
18
|
+
def test_a_complete_passthrough_profile_is_usable
|
|
19
|
+
assert ForemanNutanix::GpuProfile.usable?(profile)
|
|
20
|
+
assert ForemanNutanix::GpuProfile.usable?(profile('gpu_type' => 'PASSTHROUGH_COMPUTE'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_virtual_profiles_are_not_usable
|
|
24
|
+
# vGPU is rejected by the shim, so offering one would only let a user pick a
|
|
25
|
+
# provision that cannot succeed.
|
|
26
|
+
refute ForemanNutanix::GpuProfile.usable?(profile('gpu_type' => 'VIRTUAL'))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Regression: a profile with a nil part still passed the PASSTHROUGH filter and
|
|
30
|
+
# was offered in the dropdown. Selecting it posted a composite that parse
|
|
31
|
+
# rejects, so the VM was created with NO GPU and the build reported success.
|
|
32
|
+
def test_a_profile_missing_any_composite_part_is_not_usable
|
|
33
|
+
refute ForemanNutanix::GpuProfile.usable?(profile('vendor_name' => nil))
|
|
34
|
+
refute ForemanNutanix::GpuProfile.usable?(profile('device_id' => nil))
|
|
35
|
+
refute ForemanNutanix::GpuProfile.usable?(profile('vendor_name' => ' '))
|
|
36
|
+
refute ForemanNutanix::GpuProfile.usable?(profile('gpu_type' => nil))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_an_entirely_empty_profile_is_not_usable
|
|
40
|
+
refute ForemanNutanix::GpuProfile.usable?({})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_every_usable_profile_round_trips_through_parse
|
|
44
|
+
built = ForemanNutanix::GpuProfile.build(profile)
|
|
45
|
+
assert_equal %w[7864 NVIDIA PASSTHROUGH_GRAPHICS], ForemanNutanix::GpuProfile.parse(built)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class GpuProfileParseTest < Minitest::Test
|
|
50
|
+
def parse(value)
|
|
51
|
+
ForemanNutanix::GpuProfile.parse(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_a_well_formed_composite_yields_three_parts
|
|
55
|
+
assert_equal %w[7864 NVIDIA PASSTHROUGH_GRAPHICS], parse('7864:NVIDIA:PASSTHROUGH_GRAPHICS')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_too_few_parts_are_rejected
|
|
59
|
+
assert_nil parse('7864')
|
|
60
|
+
assert_nil parse('7864:NVIDIA')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_blank_parts_are_rejected
|
|
64
|
+
assert_nil parse('::')
|
|
65
|
+
assert_nil parse('7864::PASSTHROUGH_GRAPHICS')
|
|
66
|
+
assert_nil parse(':NVIDIA:PASSTHROUGH_GRAPHICS')
|
|
67
|
+
assert_nil parse('7864:NVIDIA:')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_unselected_values_are_rejected
|
|
71
|
+
assert_nil parse(nil)
|
|
72
|
+
assert_nil parse('')
|
|
73
|
+
assert_nil parse(' ')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_split_caps_at_three_so_gpu_type_is_never_truncated
|
|
77
|
+
assert_equal %w[7864 NVIDIA A:B], parse('7864:NVIDIA:A:B')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class GpuProfileLabelTest < Minitest::Test
|
|
82
|
+
def label(overrides = {})
|
|
83
|
+
ForemanNutanix::GpuProfile.label(profile(overrides))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_vendor_name_and_assignable_count
|
|
87
|
+
assert_equal 'NVIDIA Tesla T4 (3 assignable)', label
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_missing_assignable_drops_the_suffix
|
|
91
|
+
assert_equal 'NVIDIA Tesla T4', label('assignable' => nil)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_missing_device_name_falls_back_to_the_device_id
|
|
95
|
+
assert_equal 'NVIDIA (3 assignable)', label('device_name' => nil)
|
|
96
|
+
assert_equal 'GPU 7864 (3 assignable)', label('device_name' => nil, 'vendor_name' => nil)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require_relative '../app/models/foreman_nutanix/nutanix_compute'
|
|
5
|
+
|
|
6
|
+
class NutanixComputeGpuAccessorsTest < Minitest::Test
|
|
7
|
+
def build(attrs = {})
|
|
8
|
+
ForemanNutanix::NutanixCompute.new('cluster-1', { name: 'vm1' }.merge(attrs))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_gpu_composite_comes_from_args
|
|
12
|
+
assert_equal '7864:NVIDIA:PASSTHROUGH_GRAPHICS',
|
|
13
|
+
build(gpu: '7864:NVIDIA:PASSTHROUGH_GRAPHICS').gpu
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_gpu_defaults_to_nil
|
|
17
|
+
assert_nil build.gpu
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_gpu_count_comes_from_args
|
|
21
|
+
assert_equal 4, build(gpu_count: 4).gpu_count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_gpu_count_defaults_to_nil
|
|
25
|
+
assert_nil build.gpu_count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_gpu_and_gpu_count_are_writable
|
|
29
|
+
vm = build
|
|
30
|
+
vm.gpu = '7864:NVIDIA:PASSTHROUGH_COMPUTE'
|
|
31
|
+
vm.gpu_count = 2
|
|
32
|
+
assert_equal '7864:NVIDIA:PASSTHROUGH_COMPUTE', vm.gpu
|
|
33
|
+
assert_equal 2, vm.gpu_count
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# #save posts the provision request through the private #shim memo. Seed that
|
|
38
|
+
# memo with a recorder instead of reaching for the network; ShimClient itself is
|
|
39
|
+
# covered separately in shim_client_test.rb.
|
|
40
|
+
class FakeShimResponse
|
|
41
|
+
def success?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def json
|
|
46
|
+
{ 'ext_id' => 'vm-ext-1' }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def code
|
|
50
|
+
'200'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def body
|
|
54
|
+
'{}'
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class RecordingShim
|
|
59
|
+
attr_reader :path, :payload
|
|
60
|
+
|
|
61
|
+
def post(path, payload = nil)
|
|
62
|
+
@path = path
|
|
63
|
+
@payload = payload
|
|
64
|
+
FakeShimResponse.new
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module GpuProvisionHelper
|
|
69
|
+
# A provision that supplies everything #save demands and nothing more.
|
|
70
|
+
BASE_ATTRS = {
|
|
71
|
+
name: 'vm1',
|
|
72
|
+
subnet_ext_id: 'subnet-1',
|
|
73
|
+
storage_container_ext_id: 'sc-1',
|
|
74
|
+
memory: 4,
|
|
75
|
+
disk_size_gb: 50,
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
# Exactly what the plugin posted before GPU support existed. Any drift here is
|
|
79
|
+
# a regression: a GPU-less provision must stay byte-identical.
|
|
80
|
+
PRE_GPU_PAYLOAD = {
|
|
81
|
+
name: 'vm1',
|
|
82
|
+
cluster_ext_id: 'cluster-1',
|
|
83
|
+
subnet_ext_id: 'subnet-1',
|
|
84
|
+
storage_container_ext_id: 'sc-1',
|
|
85
|
+
num_sockets: 1,
|
|
86
|
+
num_cores_per_socket: 2,
|
|
87
|
+
memory_size_bytes: 4_294_967_296,
|
|
88
|
+
disk_size_bytes: 53_687_091_200,
|
|
89
|
+
description: '',
|
|
90
|
+
secure_boot: nil,
|
|
91
|
+
boot_method: nil,
|
|
92
|
+
}.freeze
|
|
93
|
+
|
|
94
|
+
GPU_KEYS = %i[gpu_device_id gpu_vendor gpu_mode gpu_count].freeze
|
|
95
|
+
|
|
96
|
+
def provision(attrs = {})
|
|
97
|
+
vm = ForemanNutanix::NutanixCompute.new('cluster-1', BASE_ATTRS.merge(attrs))
|
|
98
|
+
shim = RecordingShim.new
|
|
99
|
+
vm.instance_variable_set(:@shim, shim)
|
|
100
|
+
vm.save
|
|
101
|
+
shim.payload
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class NutanixComputeGpuPayloadTest < Minitest::Test
|
|
107
|
+
include GpuProvisionHelper
|
|
108
|
+
|
|
109
|
+
def test_no_gpu_selected_leaves_the_payload_exactly_as_it_was
|
|
110
|
+
assert_equal PRE_GPU_PAYLOAD, provision
|
|
111
|
+
assert_equal PRE_GPU_PAYLOAD.keys, provision.keys
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_no_gpu_selected_adds_no_gpu_keys
|
|
115
|
+
payload = provision
|
|
116
|
+
GPU_KEYS.each { |key| refute_includes payload.keys, key }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_well_formed_composite_is_split_into_three_fields
|
|
120
|
+
payload = provision(gpu: '7864:NVIDIA:PASSTHROUGH_GRAPHICS')
|
|
121
|
+
assert_equal 7864, payload[:gpu_device_id]
|
|
122
|
+
assert_equal 'NVIDIA', payload[:gpu_vendor]
|
|
123
|
+
assert_equal 'PASSTHROUGH_GRAPHICS', payload[:gpu_mode]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_selecting_a_gpu_leaves_every_other_key_untouched
|
|
127
|
+
payload = provision(gpu: '7864:NVIDIA:PASSTHROUGH_GRAPHICS')
|
|
128
|
+
assert_equal PRE_GPU_PAYLOAD, payload.reject { |key, _| GPU_KEYS.include?(key) }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class NutanixComputeGpuCountTest < Minitest::Test
|
|
133
|
+
include GpuProvisionHelper
|
|
134
|
+
|
|
135
|
+
GPU = '7864:NVIDIA:PASSTHROUGH_GRAPHICS'.freeze
|
|
136
|
+
|
|
137
|
+
def test_count_defaults_to_one_when_a_gpu_is_selected_without_one
|
|
138
|
+
assert_equal 1, provision(gpu: GPU)[:gpu_count]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_count_defaults_to_one_when_the_hidden_field_posts_empty
|
|
142
|
+
assert_equal 1, provision(gpu: GPU, gpu_count: '')[:gpu_count]
|
|
143
|
+
assert_equal 1, provision(gpu: GPU, gpu_count: nil)[:gpu_count]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_an_explicit_count_is_carried_through
|
|
147
|
+
assert_equal 3, provision(gpu: GPU, gpu_count: 3)[:gpu_count]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_a_string_count_from_the_form_is_coerced
|
|
151
|
+
assert_equal 3, provision(gpu: GPU, gpu_count: '3')[:gpu_count]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_a_nonsensical_count_still_provisions_the_selected_gpu
|
|
155
|
+
assert_equal 1, provision(gpu: GPU, gpu_count: 0)[:gpu_count]
|
|
156
|
+
assert_equal 1, provision(gpu: GPU, gpu_count: -2)[:gpu_count]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class NutanixComputeMalformedGpuCompositeTest < Minitest::Test
|
|
161
|
+
include GpuProvisionHelper
|
|
162
|
+
|
|
163
|
+
# A composite that did not survive the round trip must omit the GPU entirely.
|
|
164
|
+
# Provisioning against half a GPU spec is worse than provisioning without one.
|
|
165
|
+
def assert_no_gpu_fields(gpu)
|
|
166
|
+
payload = provision(gpu: gpu)
|
|
167
|
+
GPU_KEYS.each do |key|
|
|
168
|
+
refute_includes payload.keys, key, "expected #{gpu.inspect} to contribute no #{key}"
|
|
169
|
+
end
|
|
170
|
+
assert_equal PRE_GPU_PAYLOAD, payload
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Unselected: no GPU fields, no error. This is what an untouched dropdown posts.
|
|
174
|
+
def test_unselected_values_contribute_no_gpu_fields
|
|
175
|
+
assert_no_gpu_fields ''
|
|
176
|
+
assert_no_gpu_fields ' '
|
|
177
|
+
assert_no_gpu_fields nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Selected but malformed: raise. Silently building a GPU-less VM and reporting
|
|
181
|
+
# success is the worst available outcome, and matches how a missing network or
|
|
182
|
+
# storage container already behaves in #save.
|
|
183
|
+
def assert_raises_on(gpu)
|
|
184
|
+
error = assert_raises(StandardError) { provision(gpu: gpu) }
|
|
185
|
+
assert_match(/Malformed GPU selection/, error.message)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_too_few_parts_raise
|
|
189
|
+
assert_raises_on '7864'
|
|
190
|
+
assert_raises_on '7864:NVIDIA'
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_blank_parts_raise
|
|
194
|
+
assert_raises_on '::'
|
|
195
|
+
assert_raises_on '7864::PASSTHROUGH_GRAPHICS'
|
|
196
|
+
assert_raises_on ':NVIDIA:PASSTHROUGH_GRAPHICS'
|
|
197
|
+
assert_raises_on '7864:NVIDIA:'
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# split(':', 3) caps at three, so a gpu_type is never truncated by a stray
|
|
201
|
+
# colon further right.
|
|
202
|
+
def test_extra_colons_stay_with_the_gpu_type
|
|
203
|
+
payload = provision(gpu: '7864:NVIDIA:PASSTHROUGH_GRAPHICS:EXTRA')
|
|
204
|
+
assert_equal 'PASSTHROUGH_GRAPHICS:EXTRA', payload[:gpu_mode]
|
|
205
|
+
assert_equal 'NVIDIA', payload[:gpu_vendor]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
require 'test_helper'
|
|
2
2
|
|
|
3
|
-
# NutanixCompute is a plain class, but it logs in #initialize. Stub just enough
|
|
4
|
-
# Rails to load it standalone; see test/README.md.
|
|
5
|
-
require 'logger'
|
|
6
|
-
module Rails
|
|
7
|
-
def self.logger
|
|
8
|
-
@logger ||= Logger.new(IO::NULL)
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
3
|
require 'ostruct'
|
|
13
4
|
require_relative '../app/models/foreman_nutanix/nutanix_compute'
|
|
14
5
|
|
data/test/test_helper.rb
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
require 'minitest/autorun'
|
|
2
2
|
|
|
3
|
-
# Standalone: no Rails, no Foreman, no database.
|
|
4
|
-
#
|
|
3
|
+
# Standalone: no Rails, no Foreman, no database. See test/README.md for what
|
|
4
|
+
# that rules out.
|
|
5
5
|
require 'foreman_nutanix/shim_client'
|
|
6
|
+
|
|
7
|
+
# NutanixCompute is a plain object, but it logs in nearly every method. This is
|
|
8
|
+
# the whole of the Rails surface the suite needs; it lives here rather than in
|
|
9
|
+
# one test file so a second file defining it cannot redefine the first's.
|
|
10
|
+
require 'logger'
|
|
11
|
+
module Rails
|
|
12
|
+
def self.logger
|
|
13
|
+
@logger ||= Logger.new(IO::NULL)
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_nutanix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miles Granger
|
|
@@ -54,12 +54,15 @@ files:
|
|
|
54
54
|
- config/routes.rb
|
|
55
55
|
- lib/foreman_nutanix.rb
|
|
56
56
|
- lib/foreman_nutanix/engine.rb
|
|
57
|
+
- lib/foreman_nutanix/gpu_profile.rb
|
|
57
58
|
- lib/foreman_nutanix/shim_client.rb
|
|
58
59
|
- lib/foreman_nutanix/version.rb
|
|
59
60
|
- lib/tasks/foreman_nutanix_tasks.rake
|
|
60
61
|
- locale/foreman_nutanix.pot
|
|
61
62
|
- package.json
|
|
62
63
|
- test/README.md
|
|
64
|
+
- test/gpu_profile_test.rb
|
|
65
|
+
- test/nutanix_compute_gpu_test.rb
|
|
63
66
|
- test/nutanix_compute_test.rb
|
|
64
67
|
- test/shim_client_test.rb
|
|
65
68
|
- test/test_helper.rb
|
|
@@ -101,6 +104,8 @@ specification_version: 4
|
|
|
101
104
|
summary: Nutanix plugin for Foreman
|
|
102
105
|
test_files:
|
|
103
106
|
- test/README.md
|
|
107
|
+
- test/gpu_profile_test.rb
|
|
108
|
+
- test/nutanix_compute_gpu_test.rb
|
|
104
109
|
- test/nutanix_compute_test.rb
|
|
105
110
|
- test/shim_client_test.rb
|
|
106
111
|
- test/test_helper.rb
|