/* Copyright (c) Citrix Systems, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.ComponentModel;
using XenAdmin.Core;
using System.IO;
using XenAdmin.Network;
using System.Threading;
using XenAPI;
using XenCenterLib.Archive;
namespace XenAdmin.Actions
{
public class ExportVmAction : AsyncAction
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly string _filename;
private Exception _exception = null;
private Export export;
private readonly bool verify;
///
/// RBAC dependencies needed to import appliance/export an appliance/import disk image.
///
public static RbacMethodList StaticRBACDependencies
{
get
{
var list = new RbacMethodList("task.create", "http/get_export");
list.AddRange(Role.CommonTaskApiList);
list.AddRange(Role.CommonSessionApiList);
return list;
}
}
///
///
///
///
/// Used for filtering purposes. May be null.
///
///
///
public ExportVmAction(IXenConnection connection, Host host,
VM vm, string filename, bool verify)
: base(connection, string.Format(Messages.EXPORT_VM_TITLE, vm.Name, Helpers.GetName(connection)),
Messages.ACTION_EXPORT_DESCRIPTION_PREPARING)
{
#region RBAC Dependencies
ApiMethodsToRoleCheck.Add("task.create");
ApiMethodsToRoleCheck.AddRange(Role.CommonTaskApiList);
ApiMethodsToRoleCheck.AddRange(Role.CommonSessionApiList);
ApiMethodsToRoleCheck.Add("http/get_export");
#endregion
this.Pool = Helpers.GetPool(vm.Connection);
this.Host = host;
this.VM = vm;
_filename = filename;
this.verify = verify;
}
protected override void Run()
{
SafeToExit = false;
Description = Messages.ACTION_EXPORT_DESCRIPTION_IN_PROGRESS;
RelatedTask = XenAPI.Task.create(Session,
string.Format(Messages.ACTION_EXPORT_TASK_NAME, VM.Name),
string.Format(Messages.ACTION_EXPORT_TASK_DESCRIPTION, VM.Name));
UriBuilder uriBuilder = new UriBuilder(this.Session.Url);
uriBuilder.Path = "export";
uriBuilder.Query = string.Format("session_id={0}&uuid={1}&task_id={2}",
Uri.EscapeDataString(this.Session.uuid),
Uri.EscapeDataString(this.VM.uuid),
Uri.EscapeDataString(this.RelatedTask.opaque_ref));
log.DebugFormat("Exporting {0} from {1} to {2}", VM.Name, uriBuilder.ToString(), _filename);
// The DownloadFile call will block, so we need a separate thread to poll for task status.
Thread taskThread = new Thread((ThreadStart)progressPoll);
taskThread.Name = "Progress polling thread for ExportVmAction for " + VM.Name.Ellipsise(20);
taskThread.IsBackground = true;
taskThread.Start();
// Create the file with a temporary name till it is fully downloaded
String tmpFile = _filename + ".tmp";
try
{
HttpGet(tmpFile, uriBuilder.Uri);
}
catch (Exception e)
{
if (XenAPI.Task.get_status(this.Session, this.RelatedTask.opaque_ref) == XenAPI.task_status_type.pending
&& XenAPI.Task.get_progress(this.Session, this.RelatedTask.opaque_ref) == 0)
{
// If task is pending and has zero progress, it probably hasn't been started,
// which probably means there was an exception in the GUI code before the
// action got going. Kill the task so that we don't block forever on
// taskThread.Join(). Brought to light by CA-11100.
DestroyTask();
}
// Test for null: don't overwrite a previous exception
if (_exception == null)
_exception = e;
}
taskThread.Join();
using (FileStream fs = new FileStream(tmpFile, FileMode.Append))
{
// Flush written data to disk
if (!Win32.FlushFileBuffers(fs.SafeFileHandle))
{
Win32Exception exn = new Win32Exception(System.Runtime.InteropServices.Marshal.GetLastWin32Error());
log.ErrorFormat("FlushFileBuffers failed in ExportVmAction.\nNativeErrorCode={0}\nMessage={1}\nToString={2}",
exn.NativeErrorCode, exn.Message, exn.ToString());
}
}
if (verify && _exception == null)
{
long read = 0;
int i = 0;
long filesize = new FileInfo(tmpFile).Length / 50; //Div by 50 to save doing the * 50 in the callback
Export.verifyCallback callback = new Export.verifyCallback(delegate(uint size)
{
read += size;
i++;
//divide number of updates by 10, so as not to spend all out time redrawing the control
//but try and send an update every second to keep the timer ticking
if (i > 10)
{
PercentComplete = 50 + (int)(read / filesize);
i = 0;
}
});
try
{
using (FileStream fs = new FileStream(tmpFile, FileMode.Open, FileAccess.Read))
{
log.DebugFormat("Verifying export of {0} in {1}", VM.Name, _filename);
this.Description = Messages.ACTION_EXPORT_VERIFY;
export = new Export();
export.verify(fs, null, (Export.cancellingCallback)delegate() { return Cancelling; }, callback);
}
}
catch (Exception e)
{
if (_exception == null)
_exception = e;
}
}
if (Cancelling || _exception is CancelledException)
{
log.InfoFormat("Export of VM {0} cancelled", VM.Name);
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_CANCELLED;
log.DebugFormat("Deleting {0}", tmpFile);
File.Delete(tmpFile);
throw new CancelledException();
}
else if (_exception != null)
{
log.Warn(string.Format("Export of VM {0} failed", VM.Name), _exception);
if (_exception is HeaderChecksumFailed || _exception is FormatException)
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_HEADER_CHECKSUM_FAILED;
else if (_exception is BlockChecksumFailed)
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_BLOCK_CHECKSUM_FAILED;
else if (_exception is IOException && Win32.GetHResult(_exception) == Win32.ERROR_DISK_FULL)
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_DISK_FULL;
else if (_exception is Failure && ((Failure)_exception).ErrorDescription[0] == Failure.VDI_IN_USE)
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_VDI_IN_USE;
else
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_FAILED;
var fi = new FileInfo(tmpFile);
log.DebugFormat("Progress of the action until exception: {0}", PercentComplete);
log.DebugFormat("Size file exported until exception: {0}", fi.Length);
try
{
using (Stream stream = new FileStream(tmpFile, FileMode.Open, FileAccess.Read))
{
ArchiveIterator iterator = ArchiveFactory.Reader(ArchiveFactory.Type.Tar,
stream);
while (iterator.HasNext())
{
log.DebugFormat("Tar entry: {0} {1}", iterator.CurrentFileName(), iterator.CurrentFileSize());
}
}
}
catch (Exception)
{}
log.DebugFormat("Deleting {0}", tmpFile);
File.Delete(tmpFile);
throw new Exception(Description);
}
else
{
log.InfoFormat("Export of VM {0} successful", VM.Name);
this.Description = Messages.ACTION_EXPORT_DESCRIPTION_SUCCESSFUL;
log.DebugFormat("Renaming {0} to {1}", tmpFile, _filename);
if (File.Exists(_filename))
File.Delete(_filename);
File.Move(tmpFile, _filename);
}
}
private void HttpGet(string filename, Uri uri)
{
using (FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
using (Stream http = HTTPHelper.GET(uri, Connection, true, true))
{
new Export().verify(http, fs, (Export.cancellingCallback)delegate() { return Cancelling; });
}
}
}
private void progressPoll()
{
try
{
PollToCompletion(0, verify ? 50 : 95);
}
catch (Failure e)
{
// Don't overwrite a previous exception unless we're sure that the one that
// we have here is going to be more useful than the client one. Sometimes,
// the server exception will be "failed to write", which is just in
// response to us closing the stream when we run out of disk space or whatever
// on the client side. Other times, it's the server that's got the useful
// error message.
if (_exception == null || e.ErrorDescription[0] == Failure.VDI_IN_USE)
_exception = e;
}
catch (Exception e)
{
// Test for null: don't overwrite a previous exception
if (_exception == null)
_exception = e;
}
}
}
}