SHARE
TWEET

Untitled

a guest Jan 25th, 2016 101 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. require 'puppet_x/puppetlabs/prefetch_error'
  2. require 'puppet_x/puppetlabs/vsphere'
  3. require 'retries'
  4.  
  5. class UnableToLoadConfigurationError < StandardError
  6. end
  7.  
  8. Puppet::Type.type(:vsphere_vm).provide(:rbvmomi, :parent => PuppetX::Puppetlabs::Vsphere) do
  9.   confine feature: :rbvmomi
  10.   confine feature: :hocon
  11.  
  12.   mk_resource_methods
  13.  
  14.   [ :cpus, :memory, :extra_config, :annotation, :disks ].each do |property|
  15.     define_method("#{property}=") do |v|
  16.       # if @property_hash[property] != v
  17.         @property_hash[property] = v
  18.         @property_hash[:flush_reboot] = true
  19.       # end
  20.     end
  21.   end
  22.  
  23.   def self.instances
  24.     begin
  25.       result = nil
  26.       benchmark(:debug, 'loaded list of VMs') do
  27.         data = load_machine_info(datacenter_instance)
  28.         result = data[RbVmomi::VIM::VirtualMachine].collect do |obj, machine|
  29.           hash = nil
  30.           benchmark(:debug, "loaded machine information for #{machine['name']}") do
  31.             hash = hash_from_machine_data(obj, machine, data)
  32.           end
  33.           new(hash)
  34.         end
  35.       end
  36.       result
  37.     rescue Timeout::Error, StandardError => e
  38.       # put error in the debug log, as re-raising it below swallows the correct stack trace
  39.       Puppet.debug(e.inspect)
  40.       Puppet.debug(e.backtrace)
  41.  
  42.       raise PuppetX::Puppetlabs::PrefetchError.new(self.resource_type.name.to_s, e.message)
  43.     end
  44.   end
  45.  
  46.   def self.prefetch(resources)
  47.     instances.each do |prov|
  48.       if resource = resources[prov.name]
  49.         resource.provider = prov
  50.       end
  51.     end
  52.   end
  53.  
  54.   def self.resource_pool_from_machine_data(machine, data)
  55.     path_components = []
  56.     i = data[RbVmomi::VIM::ResourcePool][machine['resourcePool']]
  57.     while i
  58.       path_components << i['name']
  59.       if i.has_key? 'parent'
  60.         if data[RbVmomi::VIM::ResourcePool].has_key? i['parent']
  61.           i = data[RbVmomi::VIM::ResourcePool][i['parent']]
  62.         elsif data[RbVmomi::VIM::ClusterComputeResource].has_key? i['parent']
  63.           # There's always a top-level "Resources" pool that we do not want to show
  64.           path_components.pop
  65.           i = data[RbVmomi::VIM::ClusterComputeResource][i['parent']]
  66.         else
  67.           i = nil
  68.         end
  69.       else
  70.         i = nil
  71.       end
  72.     end
  73.     '/' + path_components.reverse.join('/')
  74.   end
  75.  
  76.   def self.hash_from_machine_data(obj, machine, data)
  77.     path_components = []
  78.     i = machine
  79.     while i
  80.       path_components << i['name']
  81.       if i.has_key? 'parent'
  82.         if data[RbVmomi::VIM::Folder].has_key? i['parent']
  83.           i = data[RbVmomi::VIM::Folder][i['parent']]
  84.         elsif data[RbVmomi::VIM::Datacenter].has_key? i['parent']
  85.           i = data[RbVmomi::VIM::Datacenter][i['parent']]
  86.         else
  87.           i = nil
  88.         end
  89.       else
  90.         i = nil
  91.       end
  92.     end
  93.     name = '/' + path_components.reverse.join('/')
  94.  
  95.     property_mappings = {
  96.       cpus: 'summary.config.numCpu',
  97.       snapshot_disabled: 'config.flags.snapshotDisabled',
  98.       snapshot_locked: 'config.flags.snapshotLocked',
  99.       annotation: 'config.annotation',
  100.       guest_os: 'config.guestFullName',
  101.       snapshot_power_off_behavior: 'config.flags.snapshotPowerOffBehavior',
  102.       memory: 'summary.config.memorySizeMB',
  103.       template: 'summary.config.template',
  104.       memory_reservation: 'summary.config.memoryReservation',
  105.       cpu_reservation: 'summary.config.cpuReservation',
  106.       number_ethernet_cards: 'summary.config.numEthernetCards',
  107.       power_state: 'runtime.powerState',
  108.       tools_installer_mounted: 'summary.runtime.toolsInstallerMounted',
  109.       uuid: 'summary.config.uuid',
  110.       instance_uuid: 'summary.config.instanceUuid',
  111.       hostname: 'summary.guest.hostName',
  112.       guest_ip: 'guest.ipAddress',
  113.     }
  114.  
  115.     api_properties = Hash[property_mappings
  116.       .select { |_, v| machine.has_key? v }
  117.       .collect { |key, property_name|
  118.         [key, machine[property_name]]
  119.       }
  120.     ]
  121.  
  122.     cpu_affinity = machine['config.cpuAffinity']
  123.     memory_affinity = machine['config.memoryAffinity']
  124.     disks = Hash.new
  125.     obj.disks.each do |disk|
  126.       disks[disk.deviceInfo.label] = {
  127.         'capacityInKB' => disk.capacityInKB,
  128.       }
  129.     end
  130.  
  131.  
  132.     curated_properties = {
  133.       name: name,
  134.       resource_pool: resource_pool_from_machine_data(machine, data),
  135.       ensure: machine_state(machine['runtime.powerState']),
  136.       hostname: api_properties['hostname'] == '(none)' ? nil : api_properties['hostname'],
  137.       datacenter: data[RbVmomi::VIM::Datacenter].first.last['name'],
  138.       drs_behavior: drs_behavior_from_machine_data(machine, data),
  139.       memory_affinity: memory_affinity.respond_to?('affinitySet') ? memory_affinity.affinitySet : [],
  140.       cpu_affinity: cpu_affinity.respond_to?('affinitySet') ? cpu_affinity.affinitySet : [],
  141.       object: obj,
  142.       datastore_obj: obj.datastore.first,
  143.       datastore: obj.datastore.first.respond_to?('name') ? obj.datastore.first.name : nil,
  144.       disks_obj: obj.disks,
  145.       disks: disks,
  146.     }
  147.  
  148.     # While the machine is booting, no extra config is available.
  149.     curated_properties[:extra_config] = Hash[machine['config.extraConfig'].collect { |setting| [setting.key, setting.value] }] if machine.has_key? 'config.extraConfig'
  150.  
  151.     api_properties.merge(about_info).merge(curated_properties)
  152.   end
  153.  
  154.   def self.cluster_compute_from_machine_data(machine, data)
  155.     focus = data[RbVmomi::VIM::ResourcePool][machine['resourcePool']]
  156.     while focus and focus.class != RbVmomi::VIM::ClusterComputeResource
  157.       if (focus.class == Hash && focus.has_key?('parent'))
  158.         focus = focus['parent']
  159.       elsif focus.respond_to? 'parent'
  160.         focus = focus.parent
  161.       else
  162.         focus = nil
  163.       end
  164.     end
  165.     focus
  166.   end
  167.  
  168.   def self.drs_behavior_from_machine_data(machine, data)
  169.     cluster_compute = cluster_compute_from_machine_data(machine, data)
  170.     if cluster_compute
  171.       config = data[RbVmomi::VIM::ClusterComputeResource][cluster_compute_from_machine_data(machine, data)]['configurationEx']
  172.       override = config.drsVmConfig.find {|c| c.key.name == machine['name'] }
  173.       if override
  174.         override.behavior
  175.       else
  176.         config.drsConfig.defaultVmBehavior
  177.       end
  178.     end
  179.   end
  180.  
  181.   def self.machine_state(power_state)
  182.     case power_state
  183.     when 'poweredOn'
  184.       :running
  185.     when 'poweredOff'
  186.       :stopped
  187.     when 'suspended'
  188.       :suspended
  189.     else
  190.       :unknown
  191.     end
  192.   end
  193.  
  194.   def exists?
  195.     Puppet.info("Checking if #{type_name} #{name} exists")
  196.     @property_hash[:ensure] and @property_hash[:ensure] != :absent
  197.   end
  198.  
  199.   def create(args={})
  200.     raise Puppet::Error, "Must provide a source machine, template or datastore folder to base the machine on" unless resource[:source]
  201.     if resource[:source_type] == :folder
  202.       create_from_folder
  203.     else
  204.       create_from_path(args)
  205.     end
  206.   end
  207.  
  208.   def create_disk
  209.   end
  210.  
  211.   def lookup_disks(vm)
  212.     disks = Hash.new
  213.     vm.disks.each do |disk|
  214.       disks[disk.deviceInfo.label] = {
  215.         'capacityInKB' => disk.capacityInKB.to_s,
  216.       }
  217.     end
  218.     disks
  219.   end
  220.  
  221.   def disks_match?(vm)
  222.     disks = lookup_disks(vm)
  223.     resource[:disks] == disks
  224.   end
  225.  
  226.   def create_from_path(args)
  227.     Puppet.info("Creating #{type_name} #{name}")
  228.     base_machine = PuppetX::Puppetlabs::Vsphere::Machine.new(resource[:source])
  229.     vm = datacenter_instance.find_vm(base_machine.local_path)
  230.     raise Puppet::Error, "No machine found at #{base_machine.local_path}" unless vm
  231.  
  232.     if resource[:resource_pool]
  233.       path_components = resource[:resource_pool].split('/').select { |s| !s.empty? }
  234.       compute_resource_name = path_components.shift
  235.       compute_resource = datacenter_instance.find_compute_resource(compute_resource_name)
  236.       raise Puppet::Error, "No compute resource found named #{compute_resource_name}" unless compute_resource
  237.       if path_components.empty?
  238.         pool = compute_resource.resourcePool
  239.       else
  240.         pool = compute_resource.resourcePool.traverse path_components.join('/')
  241.       end
  242.       raise Puppet::Error, "No resource pool found named #{resource[:resource_pool]}" unless pool
  243.     else
  244.       hosts = datacenter_instance.hostFolder.children
  245.       raise Puppet::Error, "No resource pool found for default datacenter" if hosts.empty?
  246.       pool = hosts.first.resourcePool
  247.     end
  248.  
  249.     relocate_spec = if is_linked_clone?
  250.       vm.add_delta_disk_layer_on_all_disks
  251.       # although we wait for the previous task to complete I was able
  252.       # to sometimes trigger a race condition. I didn't find a suitable
  253.       # assertion to make but a small sleep appears to aleviate the issue
  254.       sleep 5
  255.       RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => pool, :diskMoveType => :moveChildMostDiskBacking)
  256.     else
  257.       RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => pool)
  258.     end
  259.  
  260.     power_on = args[:stopped] == true ? false : true
  261.     power_on = false if is_template?
  262.     clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(
  263.       :location => relocate_spec,
  264.       :template => is_template?,
  265.       :powerOn => power_on)
  266.  
  267.     if resource[:customization_spec]
  268.       begin
  269.         clone_spec.customization = vim.serviceContent.customizationSpecManager.GetCustomizationSpec({name: resource[:customization_spec]}).spec
  270.       rescue RbVmomi::Fault => exception
  271.         if exception.message.split(':').first == 'NotFound'
  272.           raise Puppet::Error, "Customization specification #{resource[:customization_spec]} not found"
  273.         else
  274.           raise
  275.         end
  276.       end
  277.     end
  278.  
  279.     if resource[:cpus] || resource[:memory] || resource[:annotation]
  280.       Puppet.debug("adding VirtualMachineConfigSpec for #{type_name} #{name}")
  281.       clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(deviceChange: [])
  282.       clone_spec.config.numCPUs = resource[:cpus] if resource[:cpus] && resource[:cpus] != vm.summary.config.numCpu
  283.       # Store the set values in @property_hash, so that flush can skip those values
  284.       @property_hash[:cpus] = resource[:cpus]
  285.       clone_spec.config.memoryMB = resource[:memory] if resource[:memory] && resource[:memory] != vm.summary.config.memorySizeMB
  286.       @property_hash[:memory] = resource[:memory]
  287.       clone_spec.config.annotation = resource[:annotation] if resource[:annotation] && resource[:annotation] != vm.config.annotation
  288.       @property_hash[:annotation] = resource[:annotation]
  289.       if resource[:extra_config]
  290.         clone_spec.config.extraConfig = resource[:extra_config].map do |k,v|
  291.           {:key => k, :value => v}
  292.         end
  293.         @property_hash[:extra_config] = resource[:extra_config].dup
  294.       end
  295.     end
  296.  
  297.  
  298.     if resource[:disks]
  299.       if disks_match?(vm)
  300.         require 'pry'
  301.         binding.pry
  302.         @property_hash[:disks] = resource[:disks]
  303.       elsif @property_hash[:disks] != nil
  304.         @property_hash[:disks] = lookup_disks(vm)
  305.         @missing_disks = resource[:disks].reject do |disk, capacityhash|
  306.           @property_hash[:disks].has_key?(disk) && @property_hash[disks][disk] == capacityhash
  307.         end
  308.       else
  309.         @missing_disks = resource[:disks]
  310.       end
  311.     end
  312.  
  313.     vm.CloneVM_Task(
  314.       :folder => find_or_create_folder(datacenter_instance.vmFolder, instance.folder),
  315.       :name => instance.name,
  316.       :spec => clone_spec).wait_for_completion
  317.  
  318.     execute_command_on_machine if resource[:create_command]
  319.  
  320.     @property_hash[:ensure] = :present
  321.   end
  322.  
  323.   def create_from_folder
  324.     Puppet.info("Registering #{type_name} #{name}")
  325.  
  326.     base_machine = PuppetX::Puppetlabs::Vsphere::Machine.new(resource[:name])
  327.     template = resource[:template].to_s == 'true' || false
  328.     vm_folder = resource[:source]
  329.     vm_ext = template ? "vmtx" : "vmx"
  330.  
  331.     datastore = datacenter_instance.datastore.first
  332.     raise Puppet::Error, "No datastore found for default datacenter" unless datastore
  333.  
  334.     if resource[:resource_pool]
  335.       compute = datacenter_instance.find_compute_resource(resource[:resource_pool])
  336.       raise Puppet::Error, "No resource pool found with name #{resource[:resource_pool]}" unless compute
  337.       host = template ? compute.host.first : nil
  338.       raise Puppet::Error, "No host system found for resource pool #{resource[:resource_pool]}" unless host
  339.       pool = template ? nil : compute.resourcePool
  340.       raise Puppet::Error, "No resource pool found for #{resource[:resource_pool]}" unless pool
  341.     else
  342.       hosts = datacenter_instance.hostFolder.children
  343.       raise Puppet::Error, "No resource pool found for default datacenter" if hosts.empty?
  344.       host = template ? hosts.first.host.first : nil
  345.       pool = template ? nil : hosts.first.resourcePool
  346.     end
  347.  
  348.     folder = find_or_create_folder(datacenter_instance.vmFolder, base_machine.folder)
  349.     folder.RegisterVM_Task(
  350.       :path       => "[#{datastore.name}] #{vm_folder}/#{vm_folder}.#{vm_ext}",
  351.       :asTemplate => template,
  352.       :pool       => pool,
  353.       :host       => host
  354.     ).wait_for_completion
  355.   end
  356.  
  357.   def execute_command_on_machine
  358.     new_machine = PuppetX::Puppetlabs::Vsphere::Machine.new(name)
  359.     machine = datacenter_instance.find_vm(new_machine.local_path)
  360.     machine_credentials = {
  361.       interactiveSession: false,
  362.       username: resource[:create_command]['user'],
  363.       password: resource[:create_command]['password'],
  364.     }
  365.     manager = vim.serviceContent.guestOperationsManager
  366.     auth = RbVmomi::VIM::NamePasswordAuthentication(machine_credentials)
  367.     handler = Proc.new do |exception, attempt_number, total_delay|
  368.       Puppet.debug("#{exception.message}; retry attempt #{attempt_number}; #{total_delay} seconds have passed")
  369.       # All exceptions in RbVmomi are RbVmomi::Fault, rather than the actual API exception
  370.       # The actual exceptions come out in the message, so we parse them out
  371.       case exception.message.split(':').first
  372.       when 'GuestComponentsOutOfDate'
  373.         raise Puppet::Error, 'VMware Tools is out of date on the guest machine'
  374.       when 'InvalidGuestLogin'
  375.         raise Puppet::Error, 'Incorrect credentials for the guest machine'
  376.       when 'OperationDisabledByGuest'
  377.         raise Puppet::Error, 'Remote access is disabled on the guest machine'
  378.       when 'OperationNotSupportedByGuest'
  379.         raise Puppet::Error, 'Remote access is not supported by the guest operating system'
  380.       end
  381.     end
  382.     arguments = resource[:create_command].has_key?('arguments') ? resource[:create_command]['arguments'] : ''
  383.     working_directory = resource[:create_command].has_key?('working_directory') ? resource[:create_command]['working_directory'] : '/'
  384.     spec = RbVmomi::VIM::GuestProgramSpec(
  385.       programPath: resource[:create_command]['command'],
  386.       arguments: arguments,
  387.       workingDirectory: working_directory,
  388.     )
  389.     with_retries(:max_tries => 10,
  390.                  :handler => handler,
  391.                  :base_sleep_seconds => 5,
  392.                  :max_sleep_seconds => 15,
  393.                  :rescue => RbVmomi::Fault) do
  394.       manager.authManager.ValidateCredentialsInGuest(vm: machine, auth: auth)
  395.       response = manager.processManager.StartProgramInGuest(vm: machine, auth: auth, spec: spec)
  396.       Puppet.info("Ran #{resource[:create_command]['command']}, started with PID #{response}")
  397.     end
  398.   end
  399.  
  400.   # ensure that 'is' has all key/value pairs present in 'should'
  401.   def extra_config_matches?(is, should)
  402.     Hash[should.keys.collect { |k| [k, is[k]] } ] == should
  403.   end
  404.  
  405.  
  406.   def flush
  407.     if ! @property_hash.empty? and @property_hash[:flush_reboot]
  408.       config_spec = RbVmomi::VIM.VirtualMachineConfigSpec
  409.       config_spec.numCPUs = resource[:cpus] if resource[:cpus]
  410.       config_spec.memoryMB = resource[:memory] if resource[:memory]
  411.       config_spec.annotation = resource[:annotation] if resource[:annotation]
  412.  
  413.       if resource[:extra_config]
  414.         config_spec.extraConfig = resource[:extra_config].map do |k,v|
  415.           {:key => k, :value => v}
  416.         end
  417.       end
  418.  
  419.       if config_spec.props.count > 0
  420.         do_reboot = running?
  421.         if do_reboot
  422.           Puppet.info("Stopping #{name} to apply configuration changes")
  423.           stop
  424.         end
  425.         machine.ReconfigVM_Task(:spec => config_spec).wait_for_completion
  426.         if do_reboot
  427.           Puppet.info("Starting #{name} after applying configuration changes")
  428.           start
  429.         end
  430.       end
  431.     end
  432.   end
  433.  
  434.   def find_or_create_folder(root, parts)
  435.     if parts.empty?
  436.       root
  437.     else
  438.       part = parts.shift
  439.       folder = root.find(part)
  440.       folder = root.CreateFolder(:name => part) if folder.nil?
  441.       find_or_create_folder(folder, parts)
  442.     end
  443.   end
  444.  
  445.   def unregister
  446.     Puppet.info("Unregistering #{type_name} #{name}")
  447.     machine.UnregisterVM
  448.     @property_hash[:ensure] = :unregistered
  449.   end
  450.  
  451.   def delete_from_disk
  452.     Puppet.info("Deleting #{type_name} #{name}")
  453.     machine.Destroy_Task.wait_for_completion
  454.     @property_hash[:ensure] = :absent
  455.   end
  456.  
  457.   def destroy
  458.     if resource[:delete_from_disk].to_s == 'false'
  459.       unregister
  460.     else
  461.       delete_from_disk
  462.     end
  463.   end
  464.  
  465.   def stop
  466.     machine.PowerOffVM_Task.wait_for_completion
  467.     @property_hash[:ensure] = :stopped
  468.   end
  469.  
  470.   def start
  471.     machine.PowerOnVM_Task.wait_for_completion
  472.     @property_hash[:ensure] = :running
  473.   end
  474.  
  475.   def suspend
  476.     machine.SuspendVM_Task.wait_for_completion
  477.     @property_hash[:ensure] = :suspended
  478.   end
  479.  
  480.   def reset
  481.     machine.ResetVM_Task.wait_for_completion
  482.     @property_hash[:ensure] = :running
  483.   end
  484.  
  485.   def running?
  486.     current_state == :running
  487.   end
  488.  
  489.   def stopped?
  490.     current_state == :stopped
  491.   end
  492.  
  493.   def suspended?
  494.     current_state == :suspended
  495.   end
  496.  
  497.   def unknown?
  498.     current_state == :unknown
  499.   end
  500.  
  501.   def current_state
  502.     self.class.machine_state(machine['runtime.powerState'])
  503.   end
  504.  
  505.   private
  506.     def machine
  507.       unless @property_hash[:object]
  508.         benchmark(:debug, "fetched #{instance.local_path} info from vSphere") do
  509.           vim_machine = datacenter_instance.find_vm(instance.local_path)
  510.           data = self.class.load_machine_info(vim_machine)
  511.           machine = data[RbVmomi::VIM::VirtualMachine][vim_machine]
  512.           hash = self.class.hash_from_machine_data(vim_machine, machine, data)
  513.           @property_hash[:object] = hash[:object]
  514.         end
  515.       end
  516.       raise Puppet::Error, "No virtual machine found at #{instance.local_path}" unless @property_hash[:object]
  517.       @property_hash[:object]
  518.     end
  519.  
  520.     def instance
  521.       @instance ||= PuppetX::Puppetlabs::Vsphere::Machine.new(name)
  522.     end
  523.  
  524.     def is_template?
  525.       resource[:template].to_s == 'true'
  526.     end
  527.  
  528.     def is_linked_clone?
  529.       resource[:linked_clone].to_s == 'true'
  530.     end
  531.  
  532.     def type_name
  533.       is_template? ? "template" : "machine"
  534.     end
  535.  
  536. end
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top