lib/esx.rb in esx-0.1.1 vs lib/esx.rb in esx-0.2

- old
+ new

@@ -1,22 +1,33 @@ require 'rubygems' require 'rbvmomi' require 'alchemist' +require 'net/scp' +require 'net/ssh' module ESX - VERSION = '0.1.1' + VERSION = '0.2' + class Host + attr_reader :address, :user, :password + + def initialize(address, user, password) + @address = address + @password = password + @user = user + end + # Connect to a ESX host # # Requires hostname/ip, username and password # # Host connection is insecure by default def self.connect(host, user, password, insecure=true) vim = RbVmomi::VIM.connect :host => host, :user => user, :password => password, :insecure => insecure - host = Host.new + host = Host.new(host, user,password) host.vim = vim host end def vim=(vim) @@ -75,10 +86,12 @@ # :cpus => 1, #(int, optional) # :guest_id => 'otherGuest', #(string, optional) # :disk_size => 4096, #(in MB, optional) # :memory => 128, #(in MB, optional) # :datastore => datastore1 #(string, optional) + # :disk_file => path to vmdk inside datastore (optional) + # :disk_type => flat, sparse (default flat) # } # # Default values above. def create_vm(specification) spec = specification @@ -96,11 +109,11 @@ spec[:datastore] = '[datastore1]' end vm_cfg = { :name => spec[:vm_name], :guestId => spec[:guest_id], - :files => { :vmPathName => '[datastore1]' }, + :files => { :vmPathName => spec[:datastore] }, :numCPUs => spec[:cpus], :memoryMB => spec[:memory], :deviceChange => [ { :operation => :add, @@ -109,23 +122,10 @@ :busNumber => 0, :sharedBus => :noSharing) }, { :operation => :add, - :fileOperation => :create, - :device => RbVmomi::VIM.VirtualDisk( - :key => 0, - :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo( - :fileName => spec[:datastore], - :diskMode => :persistent, - :thinProvisioned => true), - :controllerKey => 1000, - :unitNumber => 0, - :capacityInKB => spec[:disk_size]) - }, - { - :operation => :add, :device => RbVmomi::VIM.VirtualE1000( :key => 0, :deviceInfo => { :label => 'Network Adapter 1', :summary => 'VM Network' @@ -141,10 +141,14 @@ :key => 'bios.bootOrder', :value => 'ethernet0' } ] } + vm_cfg[:deviceChange].push(create_disk_spec(:disk_file => spec[:disk_file], + :disk_type => spec[:disk_type], + :disk_size => spec[:disk_size], + :datastore => spec[:datastore])) VM.wrap(@_datacenter.vmFolder.CreateVM_Task(:config => vm_cfg, :pool => @_datacenter.hostFolder.children.first.resourcePool).wait_for_completion) end # Return product info as an array of strings containing # @@ -170,10 +174,101 @@ vms << VM.wrap(x) end vms end + # + # Run a command in the ESX host via SSH + # + def remote_command(cmd) + Net::SSH.start(@address, @user, :password => @password) do |ssh| + ssh.exec! cmd + end + end + + # + # Upload file + # + def upload_file(source, dest, print_progress = true) + Net::SSH.start(@address, @user, :password => @password) do |ssh| + puts "Uploading file... (#{File.basename(source)})" + ssh.scp.upload!(source, dest) do |ch, name, sent, total| + if print_progress + print "\rProgress: #{(sent.to_f * 100 / total.to_f).to_i}% completed" + end + end + end + puts if print_progress + end + + def import_disk(source, destination, print_progress = true) + tmp_dest = destination + ".tmp" + Net::SSH.start(@address, @user, :password => @password) do |ssh| + if not (ssh.exec! "ls #{destination} 2>/dev/null").nil? + raise Exception.new("Destination file #{destination} already exists") + end + puts "Uploading file... (#{File.basename(source)})" + ssh.scp.upload!(source, tmp_dest) do |ch, name, sent, total| + if print_progress + print "\rProgress: #{(sent.to_f * 100 / total.to_f).to_i}%" + end + end + if print_progress + puts "\nConverting disk..." + ssh.exec "vmkfstools -i #{tmp_dest} --diskformat thin #{destination}; rm -f #{tmp_dest}" + else + ssh.exec "vmkfstools -i #{tmp_dest} --diskformat thin #{destination} >/dev/null 2>&1; rm -f #{tmp_dest}" + end + end + puts + end + + private + # + # disk_file + # datastore + # disk_size + # disk_type + # + def create_disk_spec(params) + disk_type = params[:disk_type] || :flat + disk_file = params[:disk_file] + if disk_type == :sparse and disk_file.nil? + raise Exception.new("Creating sparse disks in ESX is not supported. Use an existing image.") + end + disk_size = params[:disk_size] + datastore = params[:datastore] + datastore = datastore + " #{disk_file}" if not disk_file.nil? + spec = {} + if disk_type == :sparse + spec = { + :operation => :add, + :device => RbVmomi::VIM.VirtualDisk( + :key => 0, + :backing => RbVmomi::VIM.VirtualDiskSparseVer2BackingInfo( + :fileName => datastore, + :diskMode => :persistent), + :controllerKey => 1000, + :unitNumber => 0, + :capacityInKB => disk_size) + } + else + spec = { + :operation => :add, + :device => RbVmomi::VIM.VirtualDisk( + :key => 0, + :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo( + :fileName => datastore, + :diskMode => :persistent), + :controllerKey => 1000, + :unitNumber => 0, + :capacityInKB => disk_size) + } + end + spec[:fileOperation] = :create if disk_file.nil? + spec + end end class VM attr_accessor :memory_size, :cpus, :ethernet_cards_number @@ -229,28 +324,98 @@ # vm_object.ReconfigVM_Task(:spec => spec).wait_for_completion #end vm_object.Destroy_Task.wait_for_completion end + def guest_info + GuestInfo.wrap(vm_object.guest) + end + + # + # Shortcut to GuestInfo.ip_address + # + def ip_address + guest_info.ip_address + end + end + class NetworkInterface + + attr_accessor :_wrapped_object + + def self.wrap(obj) + ni = NetworkInterface.new + ni._wrapped_object = obj + ni + end + + def ip_address + _wrapped_object.ipAddress.first + end + + def mac + _wrapped_object.ipAddress.last + end + + end + + class GuestInfo + + attr_accessor :_wrapped_object + + def self.wrap(obj) + gi = GuestInfo.new + gi._wrapped_object = obj + gi + end + + def ip_address + _wrapped_object.ipAddress + end + + def nics + n = [] + _wrapped_object.net.each do |nic| + n << NetworkInterface.wrap(nic) + end + n + end + + def tools_running_status + _wrapped_object.toolsRunningStatus + end + + def vmware_tools_installed? + _wrapped_object.toolsRunningStatus != 'guestToolsNotRunning' + end + + end + class Datastore attr_accessor :name, :capacity, :datastore_type, :free_space, :accessible attr_accessor :url + # Internal use only + attr_accessor :_wrapped_object # # Internal method. Do not use # def self.wrap(ds) - @_datastore = ds _ds = Datastore.new + _ds._wrapped_object = ds _ds.name = ds.summary.name _ds.capacity = ds.summary.capacity _ds.free_space = ds.summary.freeSpace _ds.datastore_type = ds.summary.type _ds.accessible = ds.summary.accessible _ds.url = ds.summary.url _ds end + + def method_missing(name, *args) + @_wrapped_object.send name, *args + end + end end