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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module ForemanNutanix
|
|
6
|
+
# Single HTTP seam for every call the plugin makes to the nutanix-shim-server.
|
|
7
|
+
#
|
|
8
|
+
# Deliberately plain Ruby: no Rails, no ActiveSupport, no Rails.logger. It must
|
|
9
|
+
# stay require-able from a bare minitest process with no Rails booted.
|
|
10
|
+
#
|
|
11
|
+
# Callers get back a Response value object rather than parsed data so that each
|
|
12
|
+
# one keeps its own (and today wildly inconsistent) success/failure branching.
|
|
13
|
+
class ShimClient
|
|
14
|
+
DEFAULT_BASE_URL = 'http://localhost:8000'.freeze
|
|
15
|
+
|
|
16
|
+
# Thin value object over a Net::HTTPResponse.
|
|
17
|
+
class Response
|
|
18
|
+
def initialize(raw)
|
|
19
|
+
@raw = raw
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def success?
|
|
23
|
+
@raw.is_a?(Net::HTTPSuccess)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Net::HTTPNoContent is the 204 response class. Exposed separately because
|
|
27
|
+
# destroy_vm historically tested for it explicitly alongside success?.
|
|
28
|
+
def no_content?
|
|
29
|
+
@raw.is_a?(Net::HTTPNoContent)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def code
|
|
33
|
+
@raw.code
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def body
|
|
37
|
+
@raw.body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Intentionally does NOT rescue: a non-JSON body (an HTML 500 page, say)
|
|
41
|
+
# must keep raising JSON::ParserError out to the caller, exactly as the
|
|
42
|
+
# inline JSON.parse(response.body) calls did before.
|
|
43
|
+
def json
|
|
44
|
+
JSON.parse(body)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.default_base_url
|
|
49
|
+
ENV['NUTANIX_SHIM_SERVER_ADDR'] || DEFAULT_BASE_URL
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Strips the "prefix:" from identities of the form "ZXJnb24=:<uuid>".
|
|
53
|
+
def self.normalize_uuid(uuid)
|
|
54
|
+
uuid.to_s.include?(':') ? uuid.to_s.split(':').last : uuid.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :base_url
|
|
58
|
+
|
|
59
|
+
# http: is an internal seam for tests only; production code never passes it.
|
|
60
|
+
def initialize(base_url = nil, http: Net::HTTP)
|
|
61
|
+
@base_url = (base_url || self.class.default_base_url).to_s.chomp('/')
|
|
62
|
+
@http = http
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get(path)
|
|
66
|
+
execute(Net::HTTP::Get, path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def post(path, body = nil)
|
|
70
|
+
execute(Net::HTTP::Post, path, body)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def delete(path)
|
|
74
|
+
execute(Net::HTTP::Delete, path)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def execute(verb_class, path, body = nil)
|
|
80
|
+
uri = URI("#{@base_url}#{path}")
|
|
81
|
+
connection = @http.new(uri.host, uri.port)
|
|
82
|
+
connection.use_ssl = uri.scheme == 'https'
|
|
83
|
+
|
|
84
|
+
request = verb_class.new(uri.request_uri)
|
|
85
|
+
unless body.nil?
|
|
86
|
+
request['Content-Type'] = 'application/json'
|
|
87
|
+
request.body = body.to_json
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
Response.new(connection.request(request))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/foreman_nutanix.rb
CHANGED
data/test/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Tests
|
|
2
|
+
|
|
3
|
+
Standalone minitest. No Rails, no Foreman, no database, no `test/dummy`.
|
|
4
|
+
|
|
5
|
+
Run with:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bundle exec rake test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`test_helper.rb` requires only `lib/foreman_nutanix/shim_client.rb`, which is
|
|
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.
|
|
15
|
+
|
|
16
|
+
## What this covers
|
|
17
|
+
|
|
18
|
+
`ForemanNutanix::ShimClient` — base URL resolution, trailing-slash chomping,
|
|
19
|
+
verb and path construction, JSON body encoding, `success?` / `no_content?` /
|
|
20
|
+
`code` / `body` / `json` on the response value object, and `normalize_uuid`.
|
|
21
|
+
HTTP is faked through the `http:` keyword seam; no network, no webmock.
|
|
22
|
+
|
|
23
|
+
## What cannot be tested here
|
|
24
|
+
|
|
25
|
+
Everything that needs a booted Foreman:
|
|
26
|
+
|
|
27
|
+
- `ForemanNutanix::Nutanix` — it is a `ComputeResource` subclass, and `cluster`
|
|
28
|
+
is stored in the inherited `url` column, so instantiating it at all needs
|
|
29
|
+
ActiveRecord, the Foreman schema, and the compute-resource STI machinery.
|
|
30
|
+
- `ForemanNutanix::HostManagedExtensions` — a concern mixed into `Host::Managed`.
|
|
31
|
+
- The `.erb` views under `app/views/`.
|
|
32
|
+
- The API controller extensions under `app/controllers/`.
|
|
33
|
+
- Anything touching the database, `Rails.logger`, or `Foreman::Plugin`.
|
|
34
|
+
|
|
35
|
+
`NutanixCompute` and `NutanixAdapter` are plain objects but still call
|
|
36
|
+
`Rails.logger` on nearly every method, so they are not reachable from here
|
|
37
|
+
either without stubbing Rails.
|
|
38
|
+
|
|
39
|
+
## What a full harness would need
|
|
40
|
+
|
|
41
|
+
1. A `test/dummy` Rails app generated against a Foreman core checkout.
|
|
42
|
+
2. That Foreman core pinned to a known version (the plugin requires >= 3.14.0),
|
|
43
|
+
since plugin tests load Foreman's own `test_helper`.
|
|
44
|
+
3. A database created and migrated in CI before the suite runs.
|
|
45
|
+
|
|
46
|
+
Not built. The standalone suite above is the part that pays for itself today.
|
|
@@ -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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require_relative '../app/models/foreman_nutanix/nutanix_compute'
|
|
5
|
+
|
|
6
|
+
class NutanixComputeTest < Minitest::Test
|
|
7
|
+
# Regression: ServersCollection#all builds VMs without a :gpus key, because
|
|
8
|
+
# the shim's /list-vms payload has no gpus field. The VM index template calls
|
|
9
|
+
# vm.gpus.empty? on every row, so a nil default raised NoMethodError there.
|
|
10
|
+
def test_gpus_defaults_to_an_empty_array_when_absent
|
|
11
|
+
vm = ForemanNutanix::NutanixCompute.new('cluster-1', name: 'vm1')
|
|
12
|
+
assert_equal [], vm.gpus
|
|
13
|
+
assert_empty vm.gpus
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_gpus_passes_through_when_supplied
|
|
17
|
+
vm = ForemanNutanix::NutanixCompute.new('cluster-1', name: 'vm1', gpus: %w[gpu-a gpu-b])
|
|
18
|
+
assert_equal %w[gpu-a gpu-b], vm.gpus
|
|
19
|
+
refute_empty vm.gpus
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_gpus_explicit_nil_still_yields_an_empty_array
|
|
23
|
+
vm = ForemanNutanix::NutanixCompute.new('cluster-1', name: 'vm1', gpus: nil)
|
|
24
|
+
assert_equal [], vm.gpus
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class NutanixComputeGpuLabelsTest < Minitest::Test
|
|
29
|
+
def build(gpus)
|
|
30
|
+
ForemanNutanix::NutanixCompute.new('cluster-1', name: 'vm1', gpus: gpus)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_no_gpus_gives_no_labels
|
|
34
|
+
assert_equal [], build(nil).gpu_labels
|
|
35
|
+
assert_equal [], build([]).gpu_labels
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_vendor_and_name_are_joined
|
|
39
|
+
labels = build([{ 'vendor' => 'NVIDIA', 'name' => 'Tesla T4', 'device_id' => 7864 }]).gpu_labels
|
|
40
|
+
assert_equal ['NVIDIA Tesla T4'], labels
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_falls_back_to_device_id_when_name_is_missing_or_blank
|
|
44
|
+
assert_equal ['NVIDIA 7864'], build([{ 'vendor' => 'NVIDIA', 'device_id' => 7864 }]).gpu_labels
|
|
45
|
+
assert_equal ['NVIDIA 7864'], build([{ 'vendor' => 'NVIDIA', 'name' => ' ', 'device_id' => 7864 }]).gpu_labels
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_name_alone_is_enough
|
|
49
|
+
assert_equal ['Tesla T4'], build([{ 'name' => 'Tesla T4' }]).gpu_labels
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_entirely_empty_gpu_still_gets_a_label
|
|
53
|
+
assert_equal ['GPU'], build([{}]).gpu_labels
|
|
54
|
+
assert_equal ['GPU'], build([{ 'vendor' => nil, 'name' => nil, 'device_id' => nil }]).gpu_labels
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_label_count_always_matches_gpu_count
|
|
58
|
+
labels = build([{ 'name' => 'A' }, {}, { 'vendor' => 'AMD' }]).gpu_labels
|
|
59
|
+
assert_equal 3, labels.size
|
|
60
|
+
refute_includes labels, ''
|
|
61
|
+
end
|
|
62
|
+
end
|