module Ironfan class Provider class Vsphere class Machine < Ironfan::IaasProvider::Machine delegate :config, :connection, :connection=, :disks, :Destroy_Task, :guest, :PowerOffVM_Task, :PowerOnVM_Task, :powerState, :ReconfigVM_Task, :runtime, :to => :adaptee def self.shared?() false; end def self.multiple?() false; end def self.resource_type() :machine; end def self.expected_ids(computer) [computer.server.full_name]; end def name return adaptee.config.name end def keypair end def vpc_id return true end def dns_name host = adaptee.guest.hostName domain = adaptee.guest.domainName return host unless domain return "%s.%s" %[host, domain] end def public_ip_address # FIXME return adaptee.guest.ipAddress end def private_ip_address return adaptee.guest.ipAddress end def public_hostname # FIXME return nil end def destroy adaptee.PowerOffVM_Task.wait_for_completion unless adaptee.runtime.powerState == "poweredOff" adaptee.Destroy_Task.wait_for_completion state = "destroyed" end def created? ["poweredOn", "poweredOff"].include? adaptee.runtime.powerState end def wait_for return true end def running? adaptee.runtime.powerState == "poweredOn" state = "poweredOn" end def stopped? adaptee.runtime.powerState == "poweredOff" state = "poweredOff" end def start adaptee.PowerOnVM_Task.wait_for_completion end def stop # TODO: Gracefully shutdown guest using adatee.ShutdownGuest ? # There is no "wait_for_completion" with this however... adaptee.PowerOffVM_Task.wait_for_completion end def perform_after_launch_tasks? true end def self.ip_settings(settings, hostname) # FIXME: A lot of this is required if using a static IP # Heavily referenced Serengeti's FOG here ip_settings = RbVmomi::VIM::CustomizationIPSettings.new(:ip => RbVmomi::VIM::CustomizationFixedIp(:ipAddress => settings[:ip]), :gateway => settings[:gateway], :subnetMask => settings[:subnet]) if settings[:ip] ip_settings ||= RbVmomi::VIM::CustomizationIPSettings.new("ip" => RbVmomi::VIM::CustomizationDhcpIpGenerator.new()) ip_settings.dnsDomain = settings[:domain] global_ip_settings = RbVmomi::VIM.CustomizationGlobalIPSettings global_ip_settings.dnsServerList = settings[:dnsServers] global_ip_settings.dnsSuffixList = [settings[:domain]] hostname = RbVmomi::VIM::CustomizationFixedName.new(:name => hostname) linux_prep = RbVmomi::VIM::CustomizationLinuxPrep.new( :domain => settings[:domain], :hostName => hostname ) adapter_mapping = [RbVmomi::VIM::CustomizationAdapterMapping.new("adapter" => ip_settings)] spec = RbVmomi::VIM::CustomizationSpec.new( :identity => linux_prep, :globalIPSettings => global_ip_settings, :nicSettingMap => adapter_mapping ) return spec end def self.generate_clone_spec(src_config, dc, cpus, memory, datastore, virtual_disks, network, cluster) # TODO : A lot of this should be moved to utilities in providers/vsphere.rb rspec = Vsphere.get_rspec(dc, cluster) rspec.datastore = datastore clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec, :template => false, :powerOn => true) clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new, :extraConfig => nil) network = Vsphere.find_network(network, dc) card = src_config.hardware.device.find { |d| d.deviceInfo.label == "Network adapter 1" } begin switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection( :switchUuid => network.config.distributedVirtualSwitch.uuid, :portgroupKey => network.key ) card.backing.port = switch_port rescue card.backing.deviceName = network.name end network_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit") clone_spec.config.deviceChange.push network_spec virtual_disks.each_with_index do |vd, idx| size = vd[:size].to_i label = vd[:label].to_i key = 2001 + idx # key 2000 -> SCSI0, 2001 -> SCSI1... filename = vd[:datastore] || datastore.name disk = RbVmomi::VIM.VirtualDisk( :key => key, :capacityInKB => size * 1024 * 1024, :controllerKey => 1000, # SCSI controller :unitNumber => idx + 1, :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo( :fileName => "[#{filename}]", :diskMode => :persistent, :thinProvisioned => true, :datastore => Vsphere.find_ds(dc, vd[:datastore]) || datastore ), :deviceInfo => RbVmomi::VIM.Description( :label => label, :summary => "%sGB" %[size] ), ) disk_spec = {:operation => :add, :fileOperation => :create, :device => disk } clone_spec.config.deviceChange.push disk_spec end clone_spec.config.numCPUs = Integer(cpus) clone_spec.config.memoryMB = Integer(memory) * 1024 clone_spec end def self.create!(computer) return if computer.machine? and computer.machine.created? Ironfan.step(computer.name,"creating cloud machine", :green) errors = lint(computer) if errors.present? then raise ArgumentError, "Failed validation: #{errors.inspect}" ; end # TODO: Pass launch_desc to a single function and let it do the rest... like fog does launch_desc = launch_description(computer) cpus = launch_desc[:cpus] datacenter = launch_desc[:datacenter] memory = launch_desc[:memory] template = launch_desc[:template] user_data = launch_desc[:user_data] virtual_disks = launch_desc[:virtual_disks] network = launch_desc[:network] ip_settings = launch_desc[:ip_settings] datacenter = Vsphere.find_dc(launch_desc[:datacenter]) raise "Couldn't find #{launch_desc[:datacenter]} datacenter" unless datacenter cluster = Vsphere.find_in_folder(datacenter.hostFolder, RbVmomi::VIM::ClusterComputeResource, launch_desc[:cluster]) raise "Couldn't find #{launch_desc[:cluster]} cluster in #{datacenter}" unless cluster datastore = Vsphere.find_ds(datacenter, launch_desc[:datastore]) # FIXME: add in round robin choosing? raise "Couldn't find #{launch_desc[:datastore]} datastore in #{datacenter}" unless datastore src_folder = datacenter.vmFolder src_vm = Vsphere.find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, template) raise "Couldn't find template #{template} in #{datacenter}:#{cluster}" unless src_vm Ironfan.safely do clone_spec = generate_clone_spec(src_vm.config, datacenter, cpus, memory, datastore, virtual_disks, network, cluster) clone_spec.customization = ip_settings(ip_settings, computer.name) if ip_settings[:ip] vsphere_server = src_vm.CloneVM_Task(:folder => src_vm.parent, :name => computer.name, :spec => clone_spec) state = vsphere_server.info.state # RbVmomi wait_for_completion stops world... while (state != 'error') and (state != 'success') sleep 2 state = vsphere_server.info.state end # Will break if the VM isn't finished building. new_vm = Vsphere.find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, computer.name) machine = Machine.new(:adaptee => new_vm) computer.machine = machine remember machine, :id => machine.name Ironfan.step(computer.name,"pushing keypair", :green) public_key = Vsphere::Keypair.public_key(computer) # TODO - This probably belongs somewhere else. This is how we'll enject the pubkey as well as any user information we may want # This data is not persistent across reboots... extraConfig = [{:key => 'guestinfo.pubkey', :value => public_key}, {:key => "guestinfo.user_data", :value => user_data}] machine.ReconfigVM_Task(:spec => RbVmomi::VIM::VirtualMachineConfigSpec(:extraConfig => extraConfig)).wait_for_completion # FIXME: This is extremelty fragile right now if ip_settings[:ip] Ironfan.step(computer.name,"waiting for ip customizations to complete", :green) while machine.guest.ipAddress != ip_settings[:ip] sleep 2 end end end end # # Discovery # def self.load!(cluster=nil) # FIXME: Fix this one day when we have multiple "datacenters". cloud = cluster.servers.values[0].cloud(:vsphere) vm_folder = Vsphere.find_dc(cloud.datacenter).vmFolder Vsphere.find_all_in_folder(vm_folder, RbVmomi::VIM::VirtualMachine).each do |fs| machine = new(:adaptee => fs) if recall? machine.name machine.bogus << :duplicate_machines recall(machine.name).bogus << :duplicate_machines remember machine, :append_id => "duplicate:#{machine.config.uuid}" else # never seen it remember machine end Chef::Log.debug("Loaded #{machine}") end end def self.destroy!(computer) return unless computer.machine? forget computer.machine.name computer.machine.destroy end def self.launch_description(computer) cloud = computer.server.cloud(:vsphere) user_data_hsh = { :chef_server => Chef::Config[:chef_server_url], :client_key => computer.private_key, :cluster_name => computer.server.cluster_name, :facet_index => computer.server.index, :facet_name => computer.server.facet_name, :node_name => computer.name, :organization => Chef::Config[:organization], } ip_settings = { :dnsServers => cloud.dns_servers, :domain => cloud.domain, :gateway => cloud.gateway, :ip => cloud.ip, :subnet => cloud.subnet, } description = { :cpus => cloud.cpus, :cluster => cloud.cluster, :datacenter => cloud.datacenter, :datastore => cloud.datastore, :memory => cloud.memory, :network => cloud.network, :template => cloud.template, :user_data => JSON.pretty_generate(user_data_hsh), :virtual_disks => cloud.virtual_disks, :ip_settings => ip_settings } description end # @returns [Hash{String, Array}] of 'what you did wrong' => [relevant, info] def self.lint(computer) cloud = computer.server.cloud(:vsphere) info = [computer.name, cloud.inspect] errors = {} server_errors = computer.server.lint errors["Unhappy Server"] = server_errors if server_errors.present? errors["Datacenter"] = info if cloud.datacenter.blank? errors["Template"] = info if cloud.template.blank? errors["Datastore"] = info if cloud.datastore.blank? errors['Missing client'] = info unless computer.client? errors['Missing private_key'] = computer.client unless computer.private_key # errors end def to_display(style,values={}) # style == :minimal values["State"] = runtime.powerState rescue "Terminated" values["MachineID"] = config.uuid rescue "" # values["Public IP"] = public_ip_address values["Private IP"] = guest.ipAddress rescue "" # values["Created On"] = created_at.to_date return values if style == :minimal # style == :default # values["Flavor"] = flavor_id # values["AZ"] = availability_zone return values if style == :default # style == :expanded # values["Image"] = image_id values["Virtual Disks"] = disks.map { |d| d.backing.fileName }.join(', ') # values["SSH Key"] = key_name values end def ssh_key keypair = cloud.keypair || computer.server.cluster_name end def self.validate_resources!(computers) recall.each_value do |machine| next unless machine.users.empty? and machine.name if machine.name.match("^#{computers.cluster.name}-") machine.bogus << :unexpected_machine end next unless machine.bogus? fake = Ironfan::Broker::Computer.new fake[:machine] = machine fake.name = machine.name machine.users << fake computers << fake end end end end end end