Class: Beaker::KubevirtHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/beaker/hypervisor/kubevirt_helper.rb

Overview

Helper class for KubeVirt operations

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ KubevirtHelper

Returns a new instance of KubevirtHelper.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 13

def initialize(options)
  @options = options
  @namespace = options[:namespace] || 'default'
  raw_kubeconfig = options[:kubeconfig] || ENV['KUBECONFIG'] || File.join(Dir.home, '.kube', 'config')
  @kubeconfig_path = File.expand_path(raw_kubeconfig)
  @kubecontext = options[:kubecontext] || ENV.fetch('KUBECONTEXT', nil)
  @logger = options[:logger]

  # Store original SSL options for port forwarding (before kubeclient processes them)
  @original_ssl_options = nil

  # Keep temp files alive to prevent garbage collection
  @temp_files = []

  # Allow injection of clients for testing
  @k8s_client = options[:k8s_client]
  @kubevirt_client = options[:kubevirt_client]
  @cdi_client = options[:cdi_client]

  # Only setup clients if not provided (for testing)
  return if @k8s_client && @kubevirt_client

  # Manually extract SSL options with CA file by parsing kubeconfig directly
  # (Kubeclient context doesn't expose the raw CA file path/data)
  @original_ssl_options = extract_ssl_from_kubeconfig
  setup_kubernetes_client
  setup_kubevirt_client
  setup_cdi_client unless @cdi_client
end

Instance Attribute Details

#cdi_clientObject (readonly)

Returns the value of attribute cdi_client.



11
12
13
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 11

def cdi_client
  @cdi_client
end

#k8s_clientObject (readonly)

Returns the value of attribute k8s_client.



11
12
13
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 11

def k8s_client
  @k8s_client
end

#kubevirt_clientObject (readonly)

Returns the value of attribute kubevirt_client.



11
12
13
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 11

def kubevirt_client
  @kubevirt_client
end

#namespaceObject (readonly)

Returns the value of attribute namespace.



11
12
13
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 11

def namespace
  @namespace
end

#optionsObject (readonly)

Returns the value of attribute options.



11
12
13
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 11

def options
  @options
end

Instance Method Details

#cleanup_data_volumes(test_group_identifier) ⇒ Object

Cleanup DataVolumes associated with a test group. No-op if CDI isn't available on the cluster (the @cdi_client stays nil in that case).

Parameters:

  • test_group_identifier (String)

    The identifier for the test group



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 210

def cleanup_data_volumes(test_group_identifier)
  return unless @cdi_client

  @logger.info("Cleaning up DataVolumes for test group: #{test_group_identifier}")
  label_selector = "beaker/test-group=#{test_group_identifier}"

  begin
    dvs = @cdi_client.get_data_volumes(namespace: @namespace, label_selector: label_selector)
  rescue StandardError => e
    @logger.warn("Failed to list DataVolumes for cleanup: #{e.class}: #{e.message}")
    return
  end

  @logger.info("Found #{dvs.length} DataVolume(s) with label #{label_selector}")

  dvs.each do |dv|
    md = dv[:metadata] || dv['metadata']
    dv_name = md[:name] || md['name'] if md
    unless dv_name && !dv_name.empty?
      @logger.error("Skipping DataVolume with missing name: #{dv.inspect[0, 200]}")
      next
    end

    @cdi_client.delete_data_volume(dv_name, @namespace)
    @logger.info("Deleted DataVolume #{dv_name}")
  rescue Kubeclient::ResourceNotFoundError
    @logger.debug("DataVolume #{dv_name} not found during cleanup")
  rescue StandardError => e
    @logger.error("Error deleting DataVolume #{dv_name}: #{e.message}")
  end
end

#cleanup_secrets(test_group_identifier) ⇒ Object

Cleanup secrets associated with a test group

Parameters:

  • test_group_identifier (String)

    The identifier for the test group



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 141

def cleanup_secrets(test_group_identifier)
  @logger.info("Cleaning up secrets for test group: #{test_group_identifier}")
  label_selector = "beaker/test-group=#{test_group_identifier}"
  @logger.debug("Using label selector: #{label_selector}")

  secrets = @k8s_client.get_secrets(namespace: @namespace,
                                    label_selector: label_selector)
  @logger.info("Found #{secrets.length} secret(s) with label beaker/test-group=#{test_group_identifier}")

  secrets.each do |secret|
    # Extract secret name - access hash-style to avoid RSpec metadata collision
     = secret[:metadata] || secret['metadata']
    secret_name = [:name] || ['name'] if 

    unless secret_name && !secret_name.empty?
      @logger.error('Cannot delete secret with empty or nil name.')
      @logger.error("Secret structure: #{secret.inspect[0..1000]}")
      next
    end

    secret_labels = [:labels] || ['labels'] if 
    @logger.debug("Deleting secret #{secret_name} with labels: #{secret_labels.inspect}")
    @k8s_client.delete_secret(secret_name, @namespace)
    @logger.info("Deleted secret #{secret_name}")
  rescue Kubeclient::ResourceNotFoundError
    @logger.debug("Secret #{secret_name} not found during cleanup")
  rescue StandardError => e
    @logger.error("Error deleting secret #{secret_name}: #{e.message}")
  end
end

#cleanup_services(test_group_identifier) ⇒ Object

Cleanup services associated with a test group

Parameters:

  • test_group_identifier (String)

    The identifier for the test group



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 175

def cleanup_services(test_group_identifier)
  @logger.info("Cleaning up services for test group: #{test_group_identifier}")
  label_selector = "beaker/test-group=#{test_group_identifier}"
  @logger.debug("Using label selector: #{label_selector}")

  services = @k8s_client.get_services(namespace: @namespace,
                                      label_selector: label_selector)
  @logger.info("Found #{services.length} service(s) with label beaker/test-group=#{test_group_identifier}")

  services.each do |service|
    # Extract service name - access hash-style to avoid RSpec metadata collision
     = service[:metadata] || service['metadata']
    service_name = [:name] || ['name'] if 

    unless service_name && !service_name.empty?
      @logger.error('Cannot delete service with empty or nil name.')
      @logger.error("Service structure: #{service.inspect[0..1000]}")
      next
    end

    service_labels = [:labels] || ['labels'] if 
    @logger.debug("Deleting service #{service_name} with labels: #{service_labels.inspect}")
    @k8s_client.delete_service(service_name, @namespace)
    @logger.info("Deleted service #{service_name}")
  rescue Kubeclient::ResourceNotFoundError
    @logger.debug("Service #{service_name} not found during cleanup")
  rescue StandardError => e
    @logger.error("Error deleting service #{service_name}: #{e.message}")
  end
end

#cleanup_temp_filesObject

Unlink the kubeconfig-derived cert/key tempfiles kept alive for the process lifetime. Safe to call after all API operations that need the files have completed.



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 320

def cleanup_temp_files
  return unless @temp_files

  @temp_files.each do |file|
    path = file.path
    file.close unless file.closed?
    file.unlink
  rescue StandardError => e
    @logger&.warn("Failed to unlink temp file #{path}: #{e.message}")
  end
  @temp_files.clear
end

#cleanup_vms(test_group_identifier) ⇒ Object

Cleanup VMs created by this test group

Parameters:

  • test_group_identifier (String)

    The identifier for the test group



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 106

def cleanup_vms(test_group_identifier)
  @logger.info("Cleaning up VMs for test group: #{test_group_identifier}")
  label_selector = "beaker/test-group=#{test_group_identifier}"
  @logger.debug("Using label selector: #{label_selector}")

  vms = @kubevirt_client.get_virtual_machines(namespace: @namespace,
                                              label_selector: label_selector)
  @logger.info("Found #{vms.length} VM(s) with label beaker/test-group=#{test_group_identifier}")

  vms.each do |vm|
    # Extract VM name - access hash-style to avoid RSpec metadata collision
     = vm[:metadata] || vm['metadata']
    vm_name = [:name] || ['name'] if 

    unless vm_name && !vm_name.empty?
      @logger.error('Cannot delete VM with empty or nil name.')
      @logger.error("VM structure: #{vm.inspect[0..1000]}")
      next
    end

    vm_labels = [:labels] || ['labels'] if 
    @logger.debug("Deleting VM #{vm_name} with labels: #{vm_labels.inspect}")
    @kubevirt_client.delete_virtual_machine(vm_name, @namespace)
    @logger.info("Deleted VM #{vm_name}")
  rescue Kubeclient::ResourceNotFoundError
    @logger.debug("VM #{vm_name} not found during cleanup")
  rescue StandardError => e
    @logger.error("Error deleting VM #{vm_name}: #{e.message}")
    @logger.error("Backtrace: #{e.backtrace[0..5].join("\n")}")
  end
end

#create_nodeport_service(vm_name, service_name, test_group_identifier = nil) ⇒ Hash

Create a NodePort service for SSH access

Parameters:

  • vm_name (String)

    The VM name

  • service_name (String)

    The service name

  • test_group_identifier (String, nil) (defaults to: nil)

    Test-group identifier applied as the beaker/test-group label so cleanup_services can match the service. When nil, only the beaker/vm label is set (legacy behavior).

Returns:

  • (Hash)

    Service object



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 284

def create_nodeport_service(vm_name, service_name, test_group_identifier = nil)
  labels = { 'beaker/vm' => vm_name }
  labels['beaker/test-group'] = test_group_identifier if test_group_identifier

  service_spec = {
    'apiVersion' => 'v1',
    'kind' => 'Service',
    'metadata' => {
      'name' => service_name,
      'namespace' => @namespace,
      'labels' => labels,
    },
    'spec' => {
      'type' => 'NodePort',
      'selector' => {
        'kubevirt.io/vm' => vm_name,
      },
      'ports' => [
        {
          'name' => 'ssh',
          'port' => 22,
          'targetPort' => 22,
          'protocol' => 'TCP',
        },
      ],
    },
  }

  service_spec_sym = symbolize_keys(service_spec)
  @k8s_client.create_service(service_spec_sym)
end

#create_secret(secret_spec) ⇒ Object

Create a secret

Parameters:

  • secret_spec (Hash)

    The secret specification



55
56
57
58
59
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 55

def create_secret(secret_spec)
  # Convert all keys to symbols recursively
  secret_spec_sym = symbolize_keys(secret_spec)
  @k8s_client.create_secret(secret_spec_sym)
end

#create_vm(vm_spec) ⇒ Object

Create a virtual machine

Parameters:

  • vm_spec (Hash)

    The VM specification



46
47
48
49
50
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 46

def create_vm(vm_spec)
  # Convert all keys to symbols recursively
  vm_spec_sym = symbolize_keys(vm_spec)
  @kubevirt_client.create_virtual_machine(vm_spec_sym)
end

#delete_vm(vm_name) ⇒ Object

Delete a virtual machine

Parameters:

  • vm_name (String)

    The VM name



96
97
98
99
100
101
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 96

def delete_vm(vm_name)
  @kubevirt_client.delete_virtual_machine(vm_name, @namespace)
  @logger.debug("Deleted VM #{vm_name}")
rescue Kubeclient::ResourceNotFoundError
  @logger.debug("VM #{vm_name} not found during deletion")
end

#forwarder_error_handler(error) ⇒ Object



271
272
273
274
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 271

def forwarder_error_handler(error)
  @logger.error("Port forwarder error: #{error.message}")
  # Optionally, you can implement retry logic or cleanup here
end

#get_virt_launcher_pod(vmi_name) ⇒ Hash?

Get the virt-launcher pod backing a VMI.

Parameters:

  • vmi_name (String)

    The VMI name (== VM name)

Returns:

  • (Hash, nil)

    pod object, or nil if not yet scheduled



85
86
87
88
89
90
91
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 85

def get_virt_launcher_pod(vmi_name)
  pods = @k8s_client.get_pods(namespace: @namespace,
                              label_selector: "kubevirt.io/vm=#{vmi_name}")
  pods.first
rescue Kubeclient::ResourceNotFoundError
  nil
end

#get_vm(vm_name) ⇒ Hash

Get a virtual machine

Parameters:

  • vm_name (String)

    The VM name

Returns:

  • (Hash)

    VM object



65
66
67
68
69
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 65

def get_vm(vm_name)
  @kubevirt_client.get_virtual_machine(vm_name, @namespace)
rescue Kubeclient::ResourceNotFoundError
  nil
end

#get_vmi(vmi_name) ⇒ Hash

Get a virtual machine instance

Parameters:

  • vmi_name (String)

    The VMI name

Returns:

  • (Hash)

    VMI object



75
76
77
78
79
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 75

def get_vmi(vmi_name)
  @kubevirt_client.get_virtual_machine_instance(vmi_name, @namespace)
rescue Kubeclient::ResourceNotFoundError
  nil
end

#node_ipString

Get a cluster node IP address

Returns:

  • (String)

    Node IP address



336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 336

def node_ip
  nodes = @k8s_client.get_nodes
  node = nodes.first

  # Try to get external IP first, fallback to internal IP
  addresses = node.dig('status', 'addresses') || []
  external_ip = addresses.find { |addr| addr['type'] == 'ExternalIP' }
  internal_ip = addresses.find { |addr| addr['type'] == 'InternalIP' }

  if external_ip
    external_ip['address']
  elsif internal_ip
    internal_ip['address']
  else
    raise 'Could not determine node IP address'
  end
end

#setup_port_forward(vm_name, vm_port, local_port) ⇒ Process

Setup port forwarding for a VM

Parameters:

  • vm_name (String)

    The VM name

  • vm_port (Integer)

    The VM port to forward

  • local_port (Integer)

    The local port to forward to

Returns:

  • (Process)

    The port-forward process



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/beaker/hypervisor/kubevirt_helper.rb', line 248

def setup_port_forward(vm_name, vm_port, local_port)
  require 'beaker/hypervisor/port_forward'
  forwarder = KubeVirtPortForwarder.new(
    kube_client: @kubevirt_client,
    namespace: @namespace,
    vmi_name: vm_name,
    target_port: vm_port,
    local_port: local_port,
    logger: @logger,
    on_error: method(:forwarder_error_handler),
    ssl_options: @original_ssl_options,
  )

  # Start the port forwarder in a background thread
  forwarder.start

  # Check if the forwarder started correctly.
  return forwarder if forwarder.state == :running

  @logger.error("Port forwarder failed to start for VM #{vm_name} on port #{vm_port}")
  raise "Port forwarder failed to start for VM #{vm_name} on port #{vm_port}"
end