From 3dfd70fa0e0a560769c9f77fad444d1ce68663ca Mon Sep 17 00:00:00 2001 From: Belen Bustamante Date: Tue, 9 Jun 2020 16:23:43 -0700 Subject: [PATCH] Allow for network configuration at vm clone time --- lib/vmpooler/providers/vsphere.rb | 110 ++++++++++++++++++++------ spec/rbvmomi_helper.rb | 115 ++++++++++++++++++++++++++++ spec/unit/providers/vsphere_spec.rb | 66 ++++++++++++++-- 3 files changed, 262 insertions(+), 29 deletions(-) diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb index 386d07e..79cee25 100644 --- a/lib/vmpooler/providers/vsphere.rb +++ b/lib/vmpooler/providers/vsphere.rb @@ -298,7 +298,6 @@ module Vmpooler template_path = pool['template'] target_folder_path = pool['folder'] target_datastore = pool['datastore'] - target_cluster_name = get_target_cluster_from_config(pool_name) target_datacenter_name = get_target_datacenter_from_config(pool_name) # Get the template VM object @@ -320,31 +319,19 @@ module Vmpooler ] ) - # Put the VM in the specified folder and resource pool - relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec( - datastore: find_datastore(target_datastore, connection, target_datacenter_name), - diskMoveType: get_disk_backing(pool) - ) - - manage_host_selection = @config[:config]['manage_host_selection'] if @config[:config].key?('manage_host_selection') - if manage_host_selection - run_select_hosts(pool_name, @provider_hosts) - target_host = select_next_host(pool_name, @provider_hosts) - host_object = find_host_by_dnsname(connection, target_host) - relocate_spec.host = host_object - else - # Choose a cluster/host to place the new VM on - target_cluster_object = find_cluster(target_cluster_name, connection, target_datacenter_name) - relocate_spec.pool = target_cluster_object.resourcePool + # Check if alternate network configuration is specified and add configuration + if pool.key?('network') + template_vm_network_device = template_vm_object.config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard).first + network_name = pool['network'] + network_device = set_network_device(target_datacenter_name, template_vm_network_device, network_name, connection) + config_spec.deviceChange = [{ operation: 'edit', device: network_device }] end + # Put the VM in the specified folder and resource pool + relocate_spec = create_relocate_spec(target_datastore, target_datacenter_name, pool_name, connection) + # Create a clone spec - clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec( - location: relocate_spec, - config: config_spec, - powerOn: true, - template: false - ) + clone_spec = create_clone_spec(relocate_spec, config_spec) begin vm_target_folder = find_vm_folder(pool_name, connection) @@ -356,7 +343,7 @@ module Vmpooler raise end end - raise ArgumentError, "Can not find the configured folder for #{pool_name} #{target_folder_path}" unless vm_target_folder + raise ArgumentError, "Cannot find the configured folder for #{pool_name} #{target_folder_path}" unless vm_target_folder # Create the new VM new_vm_object = template_vm_object.CloneVM_Task( @@ -370,6 +357,81 @@ module Vmpooler vm_hash end + def create_relocate_spec(target_datastore, target_datacenter_name, pool_name, connection) + pool = pool_config(pool_name) + target_cluster_name = get_target_cluster_from_config(pool_name) + + relocate_spec = RbVmomi::VIM.VirtualMachineRelocateSpec( + datastore: find_datastore(target_datastore, connection, target_datacenter_name), + diskMoveType: get_disk_backing(pool) + ) + manage_host_selection = @config[:config]['manage_host_selection'] if @config[:config].key?('manage_host_selection') + if manage_host_selection + run_select_hosts(pool_name, @provider_hosts) + target_host = select_next_host(pool_name, @provider_hosts) + host_object = find_host_by_dnsname(connection, target_host) + relocate_spec.host = host_object + else + # Choose a cluster/host to place the new VM on + target_cluster_object = find_cluster(target_cluster_name, connection, target_datacenter_name) + relocate_spec.pool = target_cluster_object.resourcePool + end + relocate_spec + end + + def create_clone_spec(relocate_spec, config_spec) + RbVmomi::VIM.VirtualMachineCloneSpec( + location: relocate_spec, + config: config_spec, + powerOn: true, + template: false + ) + end + + def set_network_device(datacenter_name, template_vm_network_device, network_name, connection) + # Retrieve network object + datacenter = connection.serviceInstance.find_datacenter(datacenter_name) + new_network = datacenter.network.find { |n| n.name == network_name } + + raise("Cannot find network #{network_name} in datacenter #{datacenter_name}") unless new_network + + # Determine network device type + # All possible device type options here: https://vdc-download.vmware.com/vmwb-repository/dcr-public/98d63b35-d822-47fe-a87a-ddefd469df06/2e3c7b58-f2bd-486e-8bb1-a75eb0640bee/doc/vim.vm.device.VirtualEthernetCard.html + network_device = + if template_vm_network_device.is_a? RbVmomi::VIM::VirtualVmxnet2 + RbVmomi::VIM.VirtualVmxnet2 + elsif template_vm_network_device.is_a? RbVmomi::VIM::VirtualVmxnet3 + RbVmomi::VIM.VirtualVmxnet3 + elsif template_vm_network_device.is_a? RbVmomi::VIM::VirtualE1000 + RbVmomi::VIM.VirtualE1000 + elsif template_vm_network_device.is_a? RbVmomi::VIM::VirtualE1000e + RbVmomi::VIM.VirtualE1000e + elsif template_vm_network_device.is_a? RbVmomi::VIM::VirtualSriovEthernetCard + RbVmomi::VIM.VirtualSriovEthernetCard + else + RbVmomi::VIM.VirtualPCNet32 + end + + # Set up new network device attributes + network_device.key = template_vm_network_device.key + network_device.deviceInfo = RbVmomi::VIM.Description( + label: template_vm_network_device.deviceInfo.label, + summary: network_name + ) + network_device.backing = RbVmomi::VIM.VirtualEthernetCardNetworkBackingInfo( + deviceName: network_name, + network: new_network, + useAutoDetect: false + ) + network_device.addressType = 'assigned' + network_device.connectable = RbVmomi::VIM.VirtualDeviceConnectInfo( + allowGuestControl: true, + startConnected: true, + connected: true + ) + network_device + end + def create_disk(pool_name, vm_name, disk_size) pool = pool_config(pool_name) raise("Pool #{pool_name} does not exist for the provider #{name}") if pool.nil? diff --git a/spec/rbvmomi_helper.rb b/spec/rbvmomi_helper.rb index 05754d9..f8c8a1a 100644 --- a/spec/rbvmomi_helper.rb +++ b/spec/rbvmomi_helper.rb @@ -63,6 +63,20 @@ MockDatacenter = Struct.new( end end +MockNetwork = Struct.new( + # https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.Network.html + # From Network + :host, :name, :summary, :vm +) + +MockVirtualVmxnet3 = Struct.new( + # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.device.VirtualVmxnet.html + # From VirtualEthenetCard + :addressType, + # From VirtualDevice + :key, :deviceInfo, :backing, :connectable +) + MockDatastore = Struct.new( # https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.Datastore.html # From Datastore @@ -208,6 +222,33 @@ MockDescription = Struct.new( :label, :summary ) +MockVirtualEthernetCardNetworkBackingInfo = Struct.new( + # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.device.VirtualEthernetCard.NetworkBackingInfo.html + # From VirtualEthernetCardNetworkBackingInfo + :network, + + # From VirtualDeviceBackingInfo + :deviceName, :useAutoDetect +) + +MockVirtualDeviceConnectInfo = Struct.new( + # https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.device.VirtualDevice.ConnectInfo.html + # From VirtualDeviceConnectInfo + :allowGuestControl, :connected, :startConnected +) + +MockVirtualMachineConfigSpec = Struct.new( + # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.ConfigSpec.html + # From VirtualMachineConfigSpec + :deviceChange, :annotation, :extraConfig +) + +MockVirtualMachineRelocateSpec = Struct.new( + # https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.RelocateSpec.html + # From VirtualMachineRelocateSpec + :datastore, :diskMoveType, :pool +) + MockDynamicProperty = Struct.new( # https://pubs.vmware.com/vsphere-55/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvmodl.DynamicProperty.html # From DynamicProperty @@ -433,6 +474,7 @@ def mock_RbVmomi_VIM_Datacenter(options = {}) # Currently don't support mocking datastore tree options[:datastores] = [] if options[:datastores].nil? options[:name] = 'Datacenter' + rand(65536).to_s if options[:name].nil? + options[:networks] = [] if options[:networks].nil? mock = MockDatacenter.new() @@ -440,6 +482,7 @@ def mock_RbVmomi_VIM_Datacenter(options = {}) mock.hostFolder = mock_RbVmomi_VIM_Folder({ :name => 'hostFolderRoot'}) mock.vmFolder = mock_RbVmomi_VIM_Folder({ :name => 'vmFolderRoot'}) mock.datastore = [] + mock.network = [] # Create vmFolder hierarchy recurse_folder_tree(options[:vmfolder_tree],mock.vmFolder.childEntity) @@ -453,6 +496,12 @@ def mock_RbVmomi_VIM_Datacenter(options = {}) mock.datastore << mock_ds end + # Create mock Networks + options[:networks].each do |networkname| + mock_nw = mock_RbVmomi_VIM_Network({ :name => networkname }) + mock.network << mock_nw + end + allow(mock).to receive(:is_a?) do |expected_type| expected_type == RbVmomi::VIM::Datacenter end @@ -508,6 +557,72 @@ def mock_RbVmomi_VIM_Datastore(options = {}) mock end +# https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.Network.html +def mock_RbVmomi_VIM_Network(options = {}) + options[:name] = 'Network' + rand(65536).to_s if options[:name].nil? + + mock = MockNetwork.new() + + mock.name = options[:name] + + allow(mock).to receive(:is_a?) do |expected_type| + expected_type == RbVmomi::VIM::Network + end + + mock +end + +# https://www.vmware.com/support/developer/vc-sdk/visdk400pubs/ReferenceGuide/vim.vm.device.VirtualVmxnet3.html +def mock_RbVmomi_VIM_VirtualVmxnet3(options = {}) + options[:key] = rand(65536) if options[:key].nil? + options[:deviceInfo] = MockDescription.new() + options[:backing] = MockVirtualEthernetCardNetworkBackingInfo.new() + options[:addressType] = 'assigned' + options[:connectable] = MockVirtualDeviceConnectInfo.new() + + mock = MockVirtualVmxnet3.new() + + mock.key = options[:key] + mock.deviceInfo = options[:deviceInfo] + mock.backing = options[:backing] + mock.addressType = options[:addressType] + mock.connectable = options[:connectable] + + allow(mock).to receive(:is_a?) do |expected_type| + expected_type == RbVmomi::VIM::VirtualVmxnet3 + end + + mock +end + +# https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.RelocateSpec.html +def mock_RbVmomi_VIM_VirtualMachineRelocateSpec(options = {}) + options[:datastore] = 'Datastore' + rand(65536).to_s if options[:datastore].nil? + options[:diskMoveType] = :moveChildMostDiskBacking + options[:pool] = 'Pool' + rand(65536).to_s if options[:pool].nil? + + mock = MockVirtualMachineRelocateSpec.new + + mock.datastore = mock_RbVmomi_VIM_Datastore({ :name => options[:datastore]}) + mock.diskMoveType = options[:diskMoveType] + mock.pool = mock_RbVmomi_VIM_ResourcePool({:name => options[:pool]}) + allow(mock).to receive(:is_a?).and_return(RbVmomi::VIM::VirtualMachineRelocateSpec) + mock +end + +# https://pubs.vmware.com/vi3/sdk/ReferenceGuide/vim.vm.ConfigSpec.html +def mock_RbVmomi_VIM_VirtualMachineConfigSpec(options = {}) + options[:device] = mock_RbVmomi_VIM_VirtualVmxnet3() + + + mock = MockVirtualMachineConfigSpec.new + + mock.deviceChange = [] + mock.deviceChange << { operation: :edit, device: options[:device]} + + mock +end + # https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.Datastore.html def mock_RbVmomi_VIM_Folder(options = {}) options[:name] = 'Folder' + rand(65536).to_s if options[:name].nil? diff --git a/spec/unit/providers/vsphere_spec.rb b/spec/unit/providers/vsphere_spec.rb index a613051..f8514b5 100644 --- a/spec/unit/providers/vsphere_spec.rb +++ b/spec/unit/providers/vsphere_spec.rb @@ -648,7 +648,6 @@ EOT context 'when create_vm_folder returns nil' do before(:each) do - template_vm = new_template_object allow(subject).to receive(:find_template_vm).and_return(new_template_object) expect(subject).to receive(:find_vm_folder).and_return(nil) end @@ -691,6 +690,43 @@ EOT end end + describe '#set_network_device' do + let(:datacenter_object) { mock_RbVmomi_VIM_Datacenter({ + :datastores => ['datastore0'], + :networks => ['network0'], + }) + } + let(:template_vm_network_device) { mock_RbVmomi_VIM_VirtualVmxnet3() } + + before(:each) do + allow(subject).to receive(:connect_to_vsphere).and_return(connection) + allow(connection.serviceInstance).to receive(:find_datacenter).and_return(datacenter_object) + end + + context 'Given an invalid network name' do + network_name = "invalid_network" + + it 'should raise an error' do + expect { subject.set_network_device("datacenter_name", template_vm_network_device, network_name, connection)}.to raise_error(/Cannot find network/) + end + end + + context 'Given a valid network name' do + network_name = "network0" + it 'should return the network device' do + result = subject.set_network_device("datacenter_name", template_vm_network_device, network_name, connection) + expect(result).to be_instance_of(RbVmomi::VIM::VirtualVmxnet3) + expect(result.deviceInfo).to be_instance_of(RbVmomi::VIM::Description) + expect(result.deviceInfo.summary).to eq('network0') + expect(result.backing).to be_instance_of(RbVmomi::VIM::VirtualEthernetCardNetworkBackingInfo) + expect(result.backing.network.is_a?(RbVmomi::VIM::Network)).to be true + expect(result.backing.network.name).to eq('network0') + expect(result.connectable).to be_instance_of(RbVmomi::VIM::VirtualDeviceConnectInfo) + expect(result.addressType).to eq('assigned') + end + end + end + describe '#create_disk' do let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) } let(:datastorename) { 'datastore0' } @@ -1144,7 +1180,7 @@ EOT let (:credentials) { config[:providers][:vsphere] } - context 'succesful connection' do + context 'successful connection' do it 'should use the supplied credentials' do expect(RbVmomi::VIM).to receive(:connect).with({ :host => credentials['server'], @@ -1436,7 +1472,7 @@ EOT allow(reconfig_vm_task).to receive(:wait_for_completion).and_return(true) end - context 'Succesfully addding disk' do + context 'Successfully adding disk' do it 'should return true' do expect(subject.add_disk(vm_object,disk_size,datastorename,connection,datacenter_name)).to be true end @@ -1501,6 +1537,26 @@ EOT end end + describe '#create_clone_spec' do + let(:relocate_spec) { mock_RbVmomi_VIM_VirtualMachineRelocateSpec({ + :datastore => 'datastore0', + :diskMoveType => :moveChildMostDiskBacking, + :pool => 'pool0' + }) + } + + let(:config_spec) { mock_RbVmomi_VIM_VirtualMachineConfigSpec()} + + it 'should return the configured clone spec' do + result = subject.create_clone_spec(relocate_spec, config_spec) + expect(result.location.pool.name).to eq('pool0') + expect(result.location.datastore.name).to eq('datastore0') + expect(result.location.diskMoveType).to eq(:moveChildMostDiskBacking) + expect(result.config.deviceChange.first[:operation]).to eq(:edit) + expect(result.config.deviceChange.first[:device].is_a?(RbVmomi::VIM::VirtualVmxnet3)).to be true + end + end + describe '#find_datastore' do let(:datastorename) { 'datastore' } let(:datastore_list) { [] } @@ -2378,7 +2434,7 @@ EOT end it 'should return the second host if called twice' do - result = subject.select_next_host(poolname, hosts_hash) + subject.select_next_host(poolname, hosts_hash) result2 = subject.select_next_host(poolname, hosts_hash) expect(result2).to eq('host2') end @@ -2391,7 +2447,7 @@ EOT end it 'should return the second host if called twice' do - result = subject.select_next_host(poolname, hosts_hash, architecture) + subject.select_next_host(poolname, hosts_hash, architecture) result2 = subject.select_next_host(poolname, hosts_hash, architecture) expect(result2).to eq('host4') end