diff --git a/include/RequestManagerVirtualMachine.h b/include/RequestManagerVirtualMachine.h index 491a5cf97a..6cd874ca23 100644 --- a/include/RequestManagerVirtualMachine.h +++ b/include/RequestManagerVirtualMachine.h @@ -176,7 +176,7 @@ public: VirtualMachineSaveDisk(): RequestManagerVirtualMachine("VirtualMachineSaveDisk", "Saves a disk from virtual machine as a new image", - "A:siissbb"){}; + "A:siissb"){}; ~VirtualMachineSaveDisk(){}; diff --git a/src/cli/onevm b/src/cli/onevm index c619e77b1f..e70c934840 100755 --- a/src/cli/onevm +++ b/src/cli/onevm @@ -97,13 +97,6 @@ cmd=CommandParser::CmdParser.new(ARGV) do :description => "Recover a VM by retrying the last failed action" } - CLONETEMPLATE={ - :name => "clonetemplate", - :short => "-c", - :large => "--clonetemplate", - :description => "Clone original VM Template and replace disk with saved one" - } - ######################################################################## # Global Options ######################################################################## @@ -310,7 +303,7 @@ cmd=CommandParser::CmdParser.new(ARGV) do EOT command :"disk-snapshot", disk_snapshot_desc, :vmid, :diskid, :img_name, - :options=>[TYPE, OneVMHelper::LIVE, CLONETEMPLATE] do + :options=>[TYPE, OneVMHelper::LIVE] do disk_id = args[1].to_i image_name = args[2] image_type = options[:type] || "" @@ -320,7 +313,7 @@ cmd=CommandParser::CmdParser.new(ARGV) do helper.perform_action(args[0],options,verbose) do |vm| res = vm.disk_snapshot(disk_id, image_name, image_type, - options[:live]==true, options[:clonetemplate]==true) + options[:live]==true) if !OpenNebula.is_error?(res) puts "Image ID: #{res}" @@ -843,6 +836,23 @@ cmd=CommandParser::CmdParser.new(ARGV) do end end + save_desc = <<-EOT.unindent + Clones the VM's source Template, replacing the disks with live snapshots + of the current disks. The VM capacity and NICs are also preserved + EOT + + command :save, save_desc, :vmid, :name, :options=>[OneVMHelper::LIVE] do + helper.perform_action(args[0],options,"Saving VM") do |vm| + res = vm.save_as_template(args[1]) + + if !OpenNebula.is_error?(res) + puts "Template ID: #{res}" + end + + res + end + end + # Deprecated commands deprecated_command(:attachdisk, 'disk-attach') diff --git a/src/oca/java/src/org/opennebula/client/vm/VirtualMachine.java b/src/oca/java/src/org/opennebula/client/vm/VirtualMachine.java index 3e43555f7e..42da7acd42 100644 --- a/src/oca/java/src/org/opennebula/client/vm/VirtualMachine.java +++ b/src/oca/java/src/org/opennebula/client/vm/VirtualMachine.java @@ -399,16 +399,12 @@ public class VirtualMachine extends PoolElement{ * the default type * @param hot True to save the disk immediately, false will perform * the operation when the VM shuts down - * @param doTemplate True to clone also the VM originating template - * and replace the disk with the saved image * @return If an error occurs the error message contains the reason. */ public static OneResponse diskSnapshot(Client client, int id, - int diskId, String imageName, String imageType, - boolean hot, boolean doTemplate) + int diskId, String imageName, String imageType, boolean hot) { - return client.call(SAVEDISK, id ,diskId, imageName, imageType, - hot, doTemplate); + return client.call(SAVEDISK, id ,diskId, imageName, imageType, hot); } /** @@ -747,15 +743,12 @@ public class VirtualMachine extends PoolElement{ * the default type * @param hot True to save the disk immediately, false will perform * the operation when the VM shuts down - * @param doTemplate True to clone also the VM originating template - * and replace the disk with the saved image * @return If an error occurs the error message contains the reason. */ public OneResponse diskSnapshot(int diskId, String imageName, - String imageType, boolean hot, boolean doTemplate) + String imageType, boolean hot) { - return diskSnapshot(client, id, diskId, imageName, imageType, - hot, doTemplate); + return diskSnapshot(client, id, diskId, imageName, imageType, hot); } /** @@ -768,7 +761,7 @@ public class VirtualMachine extends PoolElement{ */ public OneResponse diskSnapshot(int diskId, String imageName) { - return diskSnapshot(diskId, imageName, "", false, false); + return diskSnapshot(diskId, imageName, "", false); } /** @@ -782,7 +775,7 @@ public class VirtualMachine extends PoolElement{ */ public OneResponse diskSnapshot(int diskId, String imageName, boolean hot) { - return diskSnapshot(diskId, imageName, "", hot, false); + return diskSnapshot(diskId, imageName, "", hot); } /** @@ -1170,11 +1163,11 @@ public class VirtualMachine extends PoolElement{ } /** - * @deprecated Replaced by {@link #diskSnapshot(int,String,String,boolean,boolean)} + * @deprecated Replaced by {@link #diskSnapshot(int,String,String,boolean)} */ public OneResponse savedisk(int diskId, String imageName, String imageType) { - return diskSnapshot(diskId, imageName, imageType, false, false); + return diskSnapshot(diskId, imageName, imageType, false); } /** diff --git a/src/oca/ruby/opennebula/virtual_machine.rb b/src/oca/ruby/opennebula/virtual_machine.rb index 3436b88513..23c053d6e3 100644 --- a/src/oca/ruby/opennebula/virtual_machine.rb +++ b/src/oca/ruby/opennebula/virtual_machine.rb @@ -457,13 +457,10 @@ module OpenNebula # to use the default type # @param hot [true|false] True to save the disk immediately, false will # perform the operation when the VM shuts down - # @param do_template [true|false] True to clone also the VM originating - # template and replace the disk with the saved image # # @return [Integer, OpenNebula::Error] the new Image ID in case of # success, error otherwise - def disk_snapshot(disk_id, image_name, image_type="", hot=false, - do_template=false) + def disk_snapshot(disk_id, image_name, image_type="", hot=false) return Error.new('ID not defined') if !@pe_id rc = @client.call(VM_METHODS[:savedisk], @@ -471,8 +468,7 @@ module OpenNebula disk_id, image_name, image_type, - hot, - do_template) + hot) return rc end @@ -667,6 +663,125 @@ module OpenNebula self['DEPLOY_ID'] end + # 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 + # + # @return [Integer, OpenNebula::Error] the new Template ID in case of + # success, error otherwise + def save_as_template(name) + rc = info() + return rc if OpenNebula.is_error?(rc) + + tid = self['TEMPLATE/TEMPLATE_ID'] + if tid.nil? || tid.empty? + return Error.new('VM has no template to be saved') + end + + if state_str() != "POWEROFF" + return Error.new("VM state must be POWEROFF, "<< + "current state is #{state_str()}, #{lcm_state_str()}") + end + + # Clone the source template + new_tid = OpenNebula::Template.new_with_id(tid, @client).clone(name) + return new_tid if OpenNebula.is_error?(new_tid) + + # Replace the original template's capacity with the actual VM values + replace = "" + + cpu = self['TEMPLATE/CPU'] + if !cpu.nil? && !cpu.empty? + replace << "CPU = #{cpu}\n" + end + + vcpu = self['TEMPLATE/VCPU'] + if !vcpu.nil? && !vcpu.empty? + replace << "VCPU = #{vcpu}\n" + end + + mem = self['TEMPLATE/MEMORY'] + if !mem.nil? && !mem.empty? + replace << "MEMORY = #{mem}\n" + end + + self.each('TEMPLATE/DISK') do |disk| + # While the previous snapshot is still in progress, we wait + # indefinitely + rc = info() + return rc if OpenNebula.is_error?(rc) + + steps = 0 + while lcm_state_str() == "HOTPLUG_SAVEAS_POWEROFF" + if steps < 30 + sleep 1 + else + sleep 15 + end + + rc = info() + return rc if OpenNebula.is_error?(rc) + + steps += 1 + end + + # If the VM is not busy with a previous disk snapshot, we wait + # but this time with a timeout + rc = wait_state("POWEROFF") + return rc if OpenNebula.is_error?(rc) + + disk_id = disk["DISK_ID"] + if disk_id.nil? || disk_id.empty? + return Error.new('The DISK_ID is missing from the VM template') + end + + image_id = disk["IMAGE_ID"] + + if !image_id.nil? && !image_id.empty? + rc = disk_snapshot(disk_id.to_i, "#{name}-disk-#{disk_id}", + "", true) + + return rc if OpenNebula.is_error?(rc) + + replace << "DISK = [ IMAGE_ID = #{rc} ]\n" + else + # Volatile disks cannot be saved, so the definition is copied + replace << self.template_like_str( + "TEMPLATE", true, "DISK[DISK_ID=#{disk_id}]") << "\n" + end + end + + self.each('TEMPLATE/NIC') do |nic| + nic_id = nic["NIC_ID"] + if nic_id.nil? || nic_id.empty? + return Error.new('The NIC_ID is missing from the VM template') + end + + net_id = nic["NETWORK_ID"] + + if !net_id.nil? && !net_id.empty? + replace << "NIC = [ NETWORK_ID = #{net_id} ]\n" + else + # This NIC does not use a Virtual Network + replace << self.template_like_str( + "TEMPLATE", true, "NIC[NIC_ID=#{nic_id}]") << "\n" + end + end + + # Required by the Sunstone Cloud View + replace << "SAVED_TEMPLATE_ID = #{tid}\n" + + new_tmpl = OpenNebula::Template.new_with_id(new_tid, @client) + + rc = new_tmpl.update(replace, true) + return rc if OpenNebula.is_error?(rc) + + return new_tid + + # TODO: rollback in case of error + end + private def action(name) return Error.new('ID not defined') if !@pe_id @@ -676,5 +791,49 @@ module OpenNebula return rc end + + def wait_state(state, timeout=10) + vm_state = "" + lcm_state = "" + + timeout.times do + rc = info() + return rc if OpenNebula.is_error?(rc) + + vm_state = state_str() + lcm_state = lcm_state_str() + + if vm_state == state + return true + end + + sleep 1 + end + + return Error.new("Timeout expired for state #{state}. "<< + "VM is in state #{vm_state}, #{lcm_state}") + end + + def wait_lcm_state(state, timeout=10) + vm_state = "" + lcm_state = "" + + timeout.times do + rc = info() + return rc if OpenNebula.is_error?(rc) + + vm_state = state_str() + lcm_state = lcm_state_str() + + if lcm_state == state + return true + end + + sleep 1 + end + + return Error.new("Timeout expired for state #{state}. "<< + "VM is in state #{vm_state}, #{lcm_state}") + end end end diff --git a/src/rm/RequestManagerVirtualMachine.cc b/src/rm/RequestManagerVirtualMachine.cc index b1336c5a0f..a078013e15 100644 --- a/src/rm/RequestManagerVirtualMachine.cc +++ b/src/rm/RequestManagerVirtualMachine.cc @@ -1202,30 +1202,22 @@ void VirtualMachineSaveDisk::request_execute(xmlrpc_c::paramList const& paramLis ImagePool * ipool = nd.get_ipool(); DatastorePool * dspool = nd.get_dspool(); - VMTemplatePool* tpool = nd.get_tpool(); int id = xmlrpc_c::value_int(paramList.getInt(1)); int disk_id = xmlrpc_c::value_int(paramList.getInt(2)); string img_name = xmlrpc_c::value_string(paramList.getString(3)); string img_type = xmlrpc_c::value_string(paramList.getString(4)); bool is_hot = false; //Optional XML-RPC argument - bool do_template = false; //Optional XML-RPC argument if ( paramList.size() > 5 ) { is_hot = xmlrpc_c::value_boolean(paramList.getBoolean(5)); } - if ( paramList.size() > 6 ) - { - do_template = xmlrpc_c::value_boolean(paramList.getBoolean(6)); - } - VirtualMachinePool * vmpool = static_cast(pool); VirtualMachine * vm; Datastore * ds; int iid; - int tid; string error_str; @@ -1265,18 +1257,6 @@ void VirtualMachineSaveDisk::request_execute(xmlrpc_c::paramList const& paramLis return; } - if (do_template && !vm->get_template_attribute("TEMPLATE_ID",tid)) - { - vm->clear_saveas_state(disk_id, is_hot); - - vm->unlock(); - - failure_response(ACTION, - request_error("VM has no template to be saved",""), - att); - return; - } - vmpool->update(vm); vm->unlock(); @@ -1502,96 +1482,6 @@ void VirtualMachineSaveDisk::request_execute(xmlrpc_c::paramList const& paramLis ds->unlock(); } - // Return the new allocated Image ID - if (!do_template) - { - success_response(iid, att); - return; - } - - // ------------------------------------------------------------------------- - // Clone original template and replace disk with saved one - // ------------------------------------------------------------------------- - int ntid; - - PoolObjectAuth perms; - VMTemplate * vm_tmpl = tpool->get(tid,true); - - if ( vm_tmpl == 0 ) //Failed to get original template return saved image id - { - ostringstream error; - - error << get_error(object_name(PoolObjectSQL::TEMPLATE), tid) - << "Image successfully saved with id: " << iid; - - failure_response(NO_EXISTS, error.str(), att); - return; - } - - VirtualMachineTemplate * tmpl = vm_tmpl->clone_template(); - - vm_tmpl->get_permissions(perms); - - vm_tmpl->unlock(); - - //Setup the new template: name and replace disk - - ostringstream tmpl_name; - - tmpl_name << img_name << "-" << iid; - - tmpl->replace("NAME", tmpl_name.str()); - tmpl->replace("SAVED_TEMPLATE_ID", tid); - tmpl->replace("SAVED_TO_IMAGE_ID", iid); - - tmpl->replace_disk_image(iid_orig, iname_orig, iuname_orig, img_name, att.uname); - - //Authorize the template creation operation - - if ( att.uid != 0 ) - { - string tmpl_str = ""; - - AuthRequest ar(att.uid, att.group_ids); - - ar.add_auth(AuthRequest::USE, perms); - - tmpl->to_xml(tmpl_str); - - ar.add_create_auth(att.uid, att.gid, PoolObjectSQL::TEMPLATE, tmpl_str); - - if (UserPool::authorize(ar) == -1) - { - delete tmpl; - - ostringstream error; - - error << authorization_error(ar.message, att) - << "Image successfully saved with id: " << iid; - - failure_response(AUTHORIZATION, error.str(), att); - - return; - } - } - - //Allocate the template - - rc = tpool->allocate(att.uid, att.gid, att.uname, att.gname, att.umask, - tmpl, &ntid, error_str); - - if (rc < 0) - { - ostringstream error; - - error << allocate_error(PoolObjectSQL::TEMPLATE, error_str) - << "Image successfully saved with id: " << iid; - - failure_response(INTERNAL, error.str(), att); - - return; - } - success_response(iid, att); } diff --git a/src/sunstone/models/OpenNebulaJSON/VirtualMachineJSON.rb b/src/sunstone/models/OpenNebulaJSON/VirtualMachineJSON.rb index a249b059f1..987d780f78 100644 --- a/src/sunstone/models/OpenNebulaJSON/VirtualMachineJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/VirtualMachineJSON.rb @@ -76,6 +76,7 @@ module OpenNebulaJSON when "resched" then self.resched when "unresched" then self.unresched when "recover" then self.recover(action_hash['params']) + when "save_as_template" then self.save_as_template(action_hash['params']) else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -171,5 +172,9 @@ module OpenNebulaJSON def recover(params=Hash.new) super(params['result'].to_i) end + + def save_as_template(params=Hash.new) + super(params['name']) + end end end diff --git a/src/sunstone/public/js/opennebula.js b/src/sunstone/public/js/opennebula.js index e94f01b8fc..98a0051b48 100644 --- a/src/sunstone/public/js/opennebula.js +++ b/src/sunstone/public/js/opennebula.js @@ -1226,7 +1226,12 @@ var OpenNebula = { }, "showback": function(params){ OpenNebula.Action.showback(params,OpenNebula.VM.resource); - } + }, + "save_as_template": function(params){ + var action_obj = params.data.extra_param; + OpenNebula.Action.simple_action(params,OpenNebula.VM.resource, + "save_as_template",action_obj); + }, }, "Group": { diff --git a/src/sunstone/public/js/plugins/provision-tab.js b/src/sunstone/public/js/plugins/provision-tab.js index 2f7587f2f5..c4634f77df 100644 --- a/src/sunstone/public/js/plugins/provision-tab.js +++ b/src/sunstone/public/js/plugins/provision-tab.js @@ -3887,22 +3887,18 @@ function setup_info_vm(context) { var context = $(".provision_info_vm[vm_id]"); var vm_id = context.attr("vm_id"); - var image_name = $('.provision_snapshot_name', context).val(); + var template_name = $('.provision_snapshot_name', context).val(); - OpenNebula.VM.saveas({ + OpenNebula.VM.save_as_template({ data : { id: vm_id, extra_param: { - disk_id : "0", - image_name : image_name, - type: "", - clonetemplate: true, - hot: true + name : template_name } }, success: function(request, response){ OpenNebula.Helper.clear_cache("VMTEMPLATE"); - notifyMessage(tr("Image") + ' ' + request.request.data[0][1].image_name + ' ' + tr("saved successfully")) + notifyMessage(tr("Image") + ' ' + request.request.data[0][1].name + ' ' + tr("saved successfully")) update_provision_vm_info(vm_id, context); button.removeAttr("disabled"); },