module OpenNebula::VirtualMachineExt
Module to decorate VirtualMachine class with additional helpers not directly exposed through the OpenNebula XMLRPC API. The extensions include
- mp_import helper that imports a template into a marketplace
rubocop:disable Style/ClassAndModuleChildren
Public Class Methods
extend_object(obj)
click to toggle source
# File lib/opennebula/virtual_machine_ext.rb, line 26 def self.extend_object(obj) if !obj.is_a?(OpenNebula::VirtualMachine) raise StandardError, "Cannot extended #{obj.class} "\ ' with MarketPlaceAppExt' end class << obj #################################################################### # Public extended interface #################################################################### #------------------------------------------------------------------- # Clones the VM's source Template, replacing the disks with live # snapshots of the current disks. The VM capacity and NICs are also # preserved # # @param name [String] Name for the new Template # @param name [true,false,nil] Optional, true to make the saved # images persistent, false make them non-persistent # # @return [Integer, OpenNebula::Error] the new Template ID in case # of success, error otherwise #------------------------------------------------------------------- REMOVE_VNET_ATTRS = %w[AR_ID BRIDGE CLUSTER_ID IP MAC TARGET NIC_ID NETWORK_ID VN_MAD SECURITY_GROUPS VLAN_ID BRIDGE_TYPE] REMOVE_IMAGE_ATTRS = %w[DEV_PREFIX SOURCE ORIGINAL_SIZE SIZE DISK_SNAPSHOT_TOTAL_SIZE DRIVER IMAGE_STATE SAVE CLONE READONLY PERSISTENT TARGET ALLOW_ORPHANS CLONE_TARGET CLUSTER_ID DATASTORE DATASTORE_ID DISK_ID DISK_TYPE IMAGE_ID IMAGE IMAGE_UNAME IMAGE_UID LN_TARGET TM_MAD TYPE OPENNEBULA_MANAGED] def save_as_template(name, desc, opts = {}) opts = { :persistent => false, :poweroff => false, :logger => nil }.merge(opts) img_ids = [] ntid = nil logger = opts[:logger] poweron = false rc = info raise rc.message if OpenNebula.is_error?(rc) tid = self['TEMPLATE/TEMPLATE_ID'] raise 'VM has no associated template' unless valid?(tid) # -------------------------------------------------------------- # Check VM state and poweroff it if needed # -------------------------------------------------------------- if state_str != 'POWEROFF' raise 'VM must be POWEROFF' unless opts[:poweroff] logger.info 'Powering off VM' if logger poweron = true rc = poweroff raise rc.message if OpenNebula.is_error?(rc) end # -------------------------------------------------------------- # Ask if source VM is linked clone # -------------------------------------------------------------- use_linked_clones = self['USER_TEMPLATE/VCENTER_LINKED_CLONES'] if use_linked_clones && use_linked_clones.downcase == 'yes' # Delay the require until it is strictly needed # This way we can avoid the vcenter driver dependency # in no vCenter deployments require 'vcenter_driver' deploy_id = self['DEPLOY_ID'] vm_id = self['ID'] host_id = self['HISTORY_RECORDS/HISTORY[last()]/HID'] vi_client = VCenterDriver::VIClient.new_from_host(host_id) vm = VCenterDriver::VirtualMachine.new( vi_client, deploy_id, vm_id ) error, vm_template_ref = vm.save_as_linked_clones(name) raise error unless error.nil? return vm_template_ref end # -------------------------------------------------------------- # Clone the source template # -------------------------------------------------------------- vm_template = OpenNebula::Template.new_with_id(tid, @client) ntid = vm_template.clone(name) raise ntid.message if OpenNebula.is_error?(ntid) # -------------------------------------------------------------- # Replace the original template's capacity with VM values # -------------------------------------------------------------- cpu = self['TEMPLATE/CPU'] vcpu = self['TEMPLATE/VCPU'] mem = self['TEMPLATE/MEMORY'] replace = '' replace << "DESCRIPTION = \"#{desc}\"\n" if valid?(desc) replace << "CPU = #{cpu}\n" if valid?(cpu) replace << "VCPU = #{vcpu}\n" if valid?(vcpu) replace << "MEMORY = #{mem}\n" if valid?(mem) # -------------------------------------------------------------- # Process VM DISKs # -------------------------------------------------------------- logger.info 'Processing VM disks' if logger each('TEMPLATE/DISK') do |disk| # Wait for any pending operation rc = wait_state2('POWEROFF', 'LCM_INIT') raise rc.message if OpenNebula.is_error?(rc) disk_id = disk['DISK_ID'] image_id = disk['IMAGE_ID'] omng = disk['OPENNEBULA_MANAGED'] type = disk['TYPE'] raise 'Missing DISK_ID' unless valid?(disk_id) REMOVE_IMAGE_ATTRS.each do |attr| disk.delete_element(attr) end # Volatile disks cannot be saved, copy definition if !valid?(image_id) logger.info 'Adding volatile disk' if logger disk_str = template_like_str( 'TEMPLATE', true, "DISK [ DISK_ID = #{disk_id} ]" ) replace << "#{disk_str}\n" next end # CDROM disk, copy definition if type == 'CDROM' logger.info 'Adding CDROM disk' if logger replace << "DISK = [ IMAGE_ID = #{image_id}" replace << ", OPENNEBULA_MANAGED=#{omng}" if omng replace << " ]\n" next end # Regular disk, saveas it logger.info 'Adding and saving regular disk' if logger ndisk_name = "#{name}-disk-#{disk_id}" rc = disk_saveas(disk_id.to_i, ndisk_name, '', -1) raise rc.message if OpenNebula.is_error?(rc) if opts[:persistent] logger.info 'Making disk persistent' if logger nimg = OpenNebula::Image.new_with_id(rc.to_i, @client) nimg.persistent end img_ids << rc.to_i disk_tmpl = disk.template_like_str('.').tr("\n", ",\n") replace << "DISK = [ IMAGE_ID = #{rc} " replace << ", #{disk_tmpl}" unless disk_tmpl.empty? replace << " ]\n" end # -------------------------------------------------------------- # Process VM NICs # -------------------------------------------------------------- logger.info 'Processing VM NICs' if logger each('TEMPLATE/NIC') do |nic| nic_id = nic['NIC_ID'] raise 'Missing NIC_ID' unless valid?(nic_id) REMOVE_VNET_ATTRS.each do |attr| nic.delete_element(attr) end replace << 'NIC = [ ' replace << nic.template_like_str('.').tr("\n", ",\n") replace << " ] \n" end # -------------------------------------------------------------- # Extra. Required by the Sunstone Cloud View # -------------------------------------------------------------- replace << "SAVED_TEMPLATE_ID = #{tid}\n" new_tmpl = OpenNebula::Template.new_with_id(ntid, @client) logger.info 'Updating VM Template' if logger rc = new_tmpl.update(replace, true) raise rc.message if OpenNebula.is_error?(rc) # -------------------------------------------------------------- # Resume VM if needed # -------------------------------------------------------------- if poweron logger.info 'Powering on VM' if logger rc = wait_state2('POWEROFF', 'LCM_INIT') raise rc.message if OpenNebula.is_error?(rc) resume rc = wait_state2('ACTIVE', 'RUNNING') raise rc.message if OpenNebula.is_error?(rc) end ntid rescue StandardError # -------------------------------------------------------------- # Rollback. Delete the template and the images created # -------------------------------------------------------------- if ntid ntmpl = OpenNebula::Template.new_with_id(ntid, @client) ntmpl.delete end img_ids.each do |id| img = OpenNebula::Image.new_with_id(id, @client) img.delete end raise end #------------------------------------------------------------------- # Backups a VM. TODO Add final description # @param keep [Bool] # @param logger[Logger] # @param binfo[Hash] Oneshot #------------------------------------------------------------------- def backup(keep = false, logger = nil, binfo = nil) # -------------------------------------------------------------- # Check backup consistency # -------------------------------------------------------------- rc = info raise rc.message if OpenNebula.is_error?(rc) binfo.merge!(backup_info) do |_key, old_val, new_val| new_val.nil? ? old_val : new_val end raise 'No backup information' if binfo.nil? raise 'No frequency defined' unless valid?(binfo[:freq]) raise 'No marketplace defined' unless valid?(binfo[:market]) return if Time.now.to_i - binfo[:last].to_i < binfo[:freq].to_i # -------------------------------------------------------------- # Save VM as new template # -------------------------------------------------------------- logger.info 'Saving VM as template' if logger tid = save_as_template( binfo[:name], '', :poweroff => true, :logger => logger ) tmp = OpenNebula::Template.new_with_id(tid, @client) rc = tmp.info raise rc.message if OpenNebula.is_error?(rc) # -------------------------------------------------------------- # Import template into Marketplace & update VM info # -------------------------------------------------------------- logger.info "Importing template #{tmp.id} to marketplace "\ "#{binfo[:market]}" if logger tmp.extend(OpenNebula::TemplateExt) rc, ids = tmp.mp_import(binfo[:market], true, binfo[:name], :wait => true, :logger => logger) raise rc.message if OpenNebula.is_error?(rc) logger.info "Imported app ids: #{ids.join(',')}" if logger rc = update(backup_attr(binfo, ids), true) if OpenNebula.is_error?(rc) raise 'Could not update the backup reference: ' \ " #{rc.message}. New backup ids are #{ids.join(',')}." end # -------------------------------------------------------------- # Cleanup # -------------------------------------------------------------- backup_cleanup(keep, logger, binfo, tmp) rescue Error, StandardError => e backup_cleanup(keep, logger, binfo, tmp) logger.fatal(e.inspect) if logger raise end #------------------------------------------------------------------- # Restores VM information from previous backup # # @param datastore [Integer] Datastore ID to import app backup # @param logger [Logger] Logger instance to print debug info # # @return [Integer] VM ID #------------------------------------------------------------------- def restore(datastore, logger = nil) rc = info if OpenNebula.is_error?(rc) raise "Error getting VM: #{rc.message}" end logger.info 'Reading backup information' if logger backup_ids = backup_info[:apps] # highest (=last) of the app ids is the template id app_id = backup_ids.last app = OpenNebula::MarketPlaceApp.new_with_id(app_id, @client) rc = app.info if OpenNebula.is_error?(rc) raise "Can not find appliance #{app_id}: #{rc.message}." end if logger logger.info "Restoring VM #{self['ID']} from " \ "saved appliance #{app_id}" end app.extend(OpenNebula::MarketPlaceAppExt) exp = app.export(:dsid => Integer(datastore), :name => "#{self['NAME']} - RESTORED") if OpenNebula.is_error?(exp) raise "Can not restore app: #{exp.message}." end # Check possible errors when exporting apps exp[:image].each do |image| next unless OpenNebula.is_error?(image) raise "Error restoring image: #{image.message}." end template = exp[:vmtemplate].first if OpenNebula.is_error?(template) raise "Error restoring template: #{template.message}." end if logger logger.info( "Backup restored, VM template: #{exp[:vmtemplate]}, " \ "images: #{exp[:image]}" ) logger.info( "Instantiating the template #{exp[:vmtemplate]}" ) end tmpl = OpenNebula::Template.new_with_id(template, @client) rc = tmpl.instantiate if OpenNebula.is_error?(rc) raise "Can not instantiate the template: #{rc.message}." end rc rescue Error, StandardError => e logger.fatal(e.inspect) if logger raise end #################################################################### # Private extended interface #################################################################### private #------------------------------------------------------------------- # Check an attribute is defined and valid #------------------------------------------------------------------- def valid?(att) return false if att.nil? return !att.nil? && !att.empty? if att.is_a? String !att.nil? end #------------------------------------------------------------------- # Get backup information from the VM #------------------------------------------------------------------- def backup_info base = '//USER_TEMPLATE/BACKUP' binfo = {} app_ids = self["#{base}/MARKETPLACE_APP_IDS"] || '' binfo[:name] = "#{self['NAME']} - BACKUP " \ " - #{Time.now.strftime('%Y%m%d_%k%M')}" binfo[:freq] = self["#{base}/FREQUENCY_SECONDS"] binfo[:last] = self["#{base}/LAST_BACKUP_TIME"] binfo[:market] = Integer(self["#{base}/MARKETPLACE_ID"]) binfo[:apps] = app_ids.split(',') binfo rescue StandardError binfo end #------------------------------------------------------------------- # Generate backup information string #------------------------------------------------------------------- def backup_attr(binfo, ids) 'BACKUP=[' \ " MARKETPLACE_APP_IDS = \"#{ids.join(',')}\"," \ " FREQUENCY_SECONDS = \"#{binfo[:freq]}\"," \ " LAST_BACKUP_TIME = \"#{Time.now.to_i}\"," \ " MARKETPLACE_ID = \"#{binfo[:market]}\" ]" end #------------------------------------------------------------------- # Cleanup backup leftovers in case of failure #------------------------------------------------------------------- def backup_cleanup(keep, logger, binfo, template) if template logger.info "Deleting template #{template.id}" if logger template.delete(true) end binfo[:apps].each do |id| logger.info "Deleting appliance #{id}" if logger papp = OpenNebula::MarketPlaceApp.new_with_id(id, @client) papp.delete end if !keep && binfo[:apps] end end end