mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-27 02:56:01 +01:00
63c6b817d5
it's obvious what it is. The Session's uuid will be deprecated from the API bindings. Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
573 lines
23 KiB
C#
573 lines
23 KiB
C#
/* 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.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using DiscUtils.Iscsi;
|
|
using XenAPI;
|
|
using SuppressMessage = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
|
|
|
|
|
|
namespace XenOvfTransport
|
|
{
|
|
public class iSCSI
|
|
{
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
private const long KB = 1024;
|
|
private const long MB = (KB * 1024);
|
|
private const long GB = (MB * 1024);
|
|
private DiscUtils.Iscsi.Session _iscsisession;
|
|
private ulong _bytescopied;
|
|
private ulong _bytestotal;
|
|
private bool _newscsi;
|
|
private string _pluginrecord = "";
|
|
private Disk iDisk;
|
|
private string _hashAlgorithmName = "SHA1";
|
|
private byte[] _copyHash;
|
|
private byte[] _buffer = new byte[2 * MB];
|
|
|
|
private Dictionary<string, string> m_networkArgs = new Dictionary<string, string>();
|
|
|
|
#region PUBLIC
|
|
|
|
public bool Cancel { get; set; }
|
|
|
|
public Action<XenOvfTranportEventArgs> UpdateHandler { get; set; }
|
|
|
|
public Disk ScsiDisk
|
|
{
|
|
get
|
|
{
|
|
return iDisk;
|
|
}
|
|
}
|
|
|
|
public ulong Position
|
|
{
|
|
get
|
|
{
|
|
return _bytescopied;
|
|
}
|
|
}
|
|
|
|
public ulong Length
|
|
{
|
|
get
|
|
{
|
|
return _bytestotal;
|
|
}
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
public DiskStream Connect(XenAPI.Session xenSession, string vdiuuid, bool read_only)
|
|
{
|
|
StartiScsiTarget(xenSession, vdiuuid, read_only);
|
|
|
|
string ipaddress, port, targetGroupTag, username, password;
|
|
|
|
if (!TryParsePluginRecordFor(_pluginrecord, "ip", out ipaddress))
|
|
throw new Exception(Messages.ISCSI_ERROR_NO_IPADDRESS);
|
|
|
|
TryParsePluginRecordFor(_pluginrecord, "port", out port);
|
|
int ipport = Convert.ToInt32(port);
|
|
TryParsePluginRecordFor(_pluginrecord, "isci_lun", out targetGroupTag);
|
|
TryParsePluginRecordFor(_pluginrecord, "username", out username);
|
|
TryParsePluginRecordFor(_pluginrecord, "password", out password);
|
|
|
|
Initiator initiator = new Initiator();
|
|
if (username != null && password != null)
|
|
initiator.SetCredentials(username, password);
|
|
|
|
int iSCSIConnectRetry = Properties.Settings.Default.iSCSIConnectRetry;
|
|
bool iSCSIConnected = false;
|
|
|
|
while (!iSCSIConnected && iSCSIConnectRetry > 0)
|
|
{
|
|
if (Cancel)
|
|
throw new OperationCanceledException();
|
|
|
|
try
|
|
{
|
|
log.DebugFormat(Messages.FILES_TRANSPORT_SETUP, vdiuuid);
|
|
TargetAddress ta = new TargetAddress(ipaddress, ipport, targetGroupTag);
|
|
TargetInfo[] targets = initiator.GetTargets(ta);
|
|
log.InfoFormat("iSCSI.Connect found {0} targets, connecting to: {1}", targets.Length, targets[0].Name);
|
|
_iscsisession = initiator.ConnectTo(targets[0]);
|
|
iSCSIConnected = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.ErrorFormat("{0} {1}", Messages.ISCSI_ERROR, ex.Message);
|
|
Thread.Sleep(new TimeSpan(0, 0, 5));
|
|
iSCSIConnectRetry--;
|
|
}
|
|
}
|
|
|
|
if (!iSCSIConnected)
|
|
throw new Exception(Messages.ISCSI_ERROR);
|
|
|
|
long lun = 0;
|
|
try
|
|
{
|
|
LunInfo[] luns = _iscsisession.GetLuns();
|
|
if (_newscsi)
|
|
{
|
|
string idx;
|
|
TryParsePluginRecordFor(_pluginrecord, "iscsi_lun", out idx);
|
|
long lunIdx = Convert.ToInt32(idx);
|
|
lun = luns[lunIdx].Lun;
|
|
}
|
|
|
|
log.InfoFormat("iSCSI.Connect found {0} luns, looking for block storage.", luns.Length);
|
|
|
|
foreach (LunInfo iLun in luns)
|
|
{
|
|
if (iLun.DeviceType == LunClass.BlockStorage)
|
|
{
|
|
if (_newscsi && iLun.Lun == lun)
|
|
break;
|
|
|
|
lun = iLun.Lun;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
log.Error("Could not determin LUN");
|
|
throw;
|
|
}
|
|
|
|
log.InfoFormat("iSCSI.Connect, found on lun: {0}", lun);
|
|
|
|
try
|
|
{
|
|
iDisk = _iscsisession.OpenDisk(lun);
|
|
// Use our own DiskStream class to workaround a bug in DiscUtils.DiskStream.
|
|
return new DiskStream(_iscsisession, lun, (read_only ? FileAccess.Read : FileAccess.ReadWrite));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.ErrorFormat("{0} {1}", Messages.ISCSI_ERROR_CANNOT_OPEN_DISK, ex.Message);
|
|
throw new Exception(Messages.ISCSI_ERROR_CANNOT_OPEN_DISK, ex);
|
|
}
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
public void Disconnect(XenAPI.Session xenSession)
|
|
{
|
|
try
|
|
{
|
|
if (iDisk != null)
|
|
iDisk.Dispose();
|
|
iDisk = null;
|
|
}
|
|
catch (Exception exn)
|
|
{
|
|
log.DebugFormat("Failed to dispose iDisk: {0}. Continuing.", exn);
|
|
}
|
|
|
|
try
|
|
{
|
|
if (_iscsisession != null)
|
|
_iscsisession.Dispose();
|
|
_iscsisession = null;
|
|
}
|
|
catch (Exception exn)
|
|
{
|
|
log.DebugFormat("Failed to dispose iScsiSession: {0}. Continuing.", exn);
|
|
}
|
|
|
|
StopiScsiTarget(xenSession);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
public void Copy(Stream source, Stream destination, string filename, bool shouldHash)
|
|
{
|
|
log.InfoFormat("Started copying {0} bytes to {1} via iSCSI.", source.Length, filename);
|
|
|
|
int bytesRead = 0;
|
|
long offset = 0;
|
|
long limit = source.Length;
|
|
_bytestotal = (ulong)source.Length;
|
|
|
|
string updatemsg = string.Format(Messages.ISCSI_COPY_PROGRESS, filename);
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileStart, "SendData Start", updatemsg, 0, _bytestotal));
|
|
|
|
// Create a hash algorithm to compute the hash from separate blocks during the copy.
|
|
using (var hashAlgorithm = System.Security.Cryptography.HashAlgorithm.Create(_hashAlgorithmName))
|
|
{
|
|
while (offset < limit)
|
|
{
|
|
if (Cancel)
|
|
{
|
|
log.InfoFormat(Messages.ISCSI_COPY_CANCELLED, filename);
|
|
throw new OperationCanceledException(string.Format(Messages.ISCSI_COPY_CANCELLED, filename));
|
|
}
|
|
|
|
try
|
|
{
|
|
bytesRead = source.Read(_buffer, 0, _buffer.Length);
|
|
|
|
if (bytesRead <= 0)
|
|
break;
|
|
|
|
if (!IsZeros(_buffer))
|
|
{
|
|
// This block has content.
|
|
// Seek the same position in the destination.
|
|
destination.Seek(offset, SeekOrigin.Begin);
|
|
|
|
destination.Write(_buffer, 0, bytesRead);
|
|
|
|
if ((offset + bytesRead) < limit)
|
|
{
|
|
// This is not the last block.
|
|
// Compute the partial hash.
|
|
if (shouldHash)
|
|
hashAlgorithm.TransformBlock(_buffer, 0, bytesRead, _buffer, 0);
|
|
}
|
|
}
|
|
|
|
offset += bytesRead;
|
|
|
|
_bytescopied = (ulong)offset;
|
|
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileProgress, "SendData Start", updatemsg, _bytescopied, _bytestotal));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var message = string.Format(Messages.ISCSI_COPY_ERROR, filename);
|
|
log.Warn(message);
|
|
throw new Exception(message, ex);
|
|
}
|
|
}
|
|
|
|
if (shouldHash)
|
|
{
|
|
// It is necessary to call TransformBlock at least once and TransformFinalBlock only once before getting the hash.
|
|
// If only the last buffer had content, then TransformBlock would not have been called at least once.
|
|
// So, split the last buffer and hash it even if it is empty.
|
|
// Note: TransformBlock will accept an "inputCount" that is zero.
|
|
hashAlgorithm.TransformBlock(_buffer, 0, bytesRead / 2, _buffer, 0);
|
|
|
|
// Compute the final hash.
|
|
hashAlgorithm.TransformFinalBlock(_buffer, bytesRead / 2, bytesRead / 2);
|
|
|
|
_copyHash = hashAlgorithm.Hash;
|
|
}
|
|
}
|
|
|
|
destination.Flush();
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileComplete, "SendData Completed", updatemsg, _bytescopied, _bytestotal));
|
|
|
|
log.InfoFormat("Finished copying {0} bytes to {1} via iSCSI.", source.Length, filename);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
public void Verify(Stream target, string filename)
|
|
{
|
|
log.InfoFormat("Started verifying {0} bytes in {1} after copy via iSCSI.", _bytescopied, filename);
|
|
|
|
int bytesRead = 0;
|
|
long offset = 0;
|
|
long limit = (long)_bytescopied;
|
|
|
|
string updatemsg = string.Format(Messages.ISCSI_VERIFY_PROGRESS, filename);
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileStart, "SendData Start", updatemsg, 0, (ulong)limit));
|
|
|
|
// Create a hash algorithm to compute the hash from separate blocks in the same way as Copy().
|
|
using (var hashAlgorithm = System.Security.Cryptography.HashAlgorithm.Create(_hashAlgorithmName))
|
|
{
|
|
while (offset < limit)
|
|
{
|
|
if (Cancel)
|
|
{
|
|
log.Info(Messages.ISCSI_VERIFY_CANCELLED);
|
|
throw new OperationCanceledException(Messages.ISCSI_VERIFY_CANCELLED);
|
|
}
|
|
|
|
try
|
|
{
|
|
bytesRead = target.Read(_buffer, 0, _buffer.Length);
|
|
|
|
if (bytesRead <= 0)
|
|
break;
|
|
|
|
if (!IsZeros(_buffer))
|
|
{
|
|
if ((offset + bytesRead) < limit)
|
|
{
|
|
// This is not the last block.
|
|
// Compute the partial hash.
|
|
hashAlgorithm.TransformBlock(_buffer, 0, bytesRead, _buffer, 0);
|
|
}
|
|
}
|
|
|
|
offset += bytesRead;
|
|
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileProgress, "SendData Start", updatemsg, (ulong)offset, (ulong)limit));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var message = string.Format(Messages.ISCSI_VERIFY_ERROR, filename);
|
|
log.WarnFormat("{0} {1}", message, ex.Message);
|
|
throw new Exception(message, ex);
|
|
}
|
|
}
|
|
|
|
// It is necessary to call TransformBlock at least once and TransformFinalBlock only once before getting the hash.
|
|
// If only the last buffer had content, then TransformBlock would not have been called at least once.
|
|
// So, split the last buffer and hash it even if it is empty.
|
|
// Note: TransformBlock will accept an "inputCount" that is zero.
|
|
hashAlgorithm.TransformBlock(_buffer, 0, bytesRead / 2, _buffer, 0);
|
|
|
|
// Compute the final hash.
|
|
hashAlgorithm.TransformFinalBlock(_buffer, bytesRead / 2, bytesRead / 2);
|
|
|
|
// Compare targetHash with copyHash.
|
|
if (!System.Linq.Enumerable.SequenceEqual(_copyHash, hashAlgorithm.Hash))
|
|
{
|
|
log.Error(Messages.ISCSI_VERIFY_INVALID);
|
|
throw new Exception(Messages.ISCSI_VERIFY_INVALID);
|
|
}
|
|
}
|
|
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileComplete, "SendData Completed", updatemsg, (ulong)offset, (ulong)limit));
|
|
|
|
log.InfoFormat("Finished verifying {0} bytes in {1} after copy via iSCSI.", target.Length, filename);
|
|
}
|
|
|
|
public void WimCopy(Stream source, Stream destination, string filename, bool close, ulong fileindex, ulong filecount)
|
|
{
|
|
log.InfoFormat("iSCSI.Copy copying {0} bytes.", source.Length);
|
|
_bytestotal = (ulong)source.Length;
|
|
ulong zerosskipped = 0;
|
|
byte[] block = new byte[2 * MB];
|
|
ulong p = 0;
|
|
|
|
string updatemsg = string.Format(Messages.ISCSI_WIM_PROGRESS_FORMAT, fileindex, filecount, filename);
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileStart, "SendData Start", updatemsg, 0, _bytestotal));
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
int n = source.Read(block, 0, block.Length);
|
|
if (n <= 0)
|
|
break;
|
|
if (!IsZeros(block))
|
|
{
|
|
destination.Seek((long)p, SeekOrigin.Begin);
|
|
destination.Write(block, 0, n);
|
|
}
|
|
else
|
|
{
|
|
zerosskipped += (ulong)n;
|
|
}
|
|
if (Cancel)
|
|
{
|
|
log.WarnFormat(Messages.ISCSI_COPY_CANCELLED, filename);
|
|
throw new OperationCanceledException(string.Format(Messages.ISCSI_COPY_CANCELLED, filename));
|
|
}
|
|
p += (ulong)n;
|
|
_bytescopied = p;
|
|
if (p >= (ulong)source.Length)
|
|
break;
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileProgress, "SendData Start", updatemsg, _bytescopied, _bytestotal));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex is OperationCanceledException)
|
|
throw;
|
|
var message = string.Format(Messages.ISCSI_COPY_ERROR, filename);
|
|
log.Warn(message);
|
|
throw new Exception(message, ex);
|
|
}
|
|
}
|
|
destination.Flush();
|
|
if (close)
|
|
{
|
|
if (source != null) source.Close();
|
|
if (destination != null) destination.Close();
|
|
}
|
|
OnUpdate(new XenOvfTranportEventArgs(XenOvfTranportEventType.FileComplete, "SendData Completed", updatemsg, _bytescopied, _bytestotal));
|
|
log.Info("iSCSI.Copy done with copy.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a master boot record to the iSCSI device.
|
|
/// </summary>
|
|
/// <param name="mbrstream">a stream containing the MBR</param>
|
|
public void WriteMBR(Stream mbrstream)
|
|
{
|
|
mbrstream.Position = 0;
|
|
byte[] mbr = new byte[mbrstream.Length];
|
|
mbrstream.Read(mbr, 0, (int)mbrstream.Length);
|
|
iDisk.SetMasterBootRecord(mbr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure the network settings for the transfer VM
|
|
/// </summary>
|
|
/// <param name="isIpStatic">True if a static IP address is to be used, false if we get IP address through DHCP</param>
|
|
/// <param name="ip">The static IP address to use</param>
|
|
/// <param name="mask">The subnet mask</param>
|
|
/// <param name="gateway">The network gateway</param>
|
|
public void ConfigureTvmNetwork(string networkUuid, bool isIpStatic, string ip, string mask, string gateway)
|
|
{
|
|
m_networkArgs = new Dictionary<string, string>();
|
|
|
|
//network_config is "auto", therefore no related arguments need to be added
|
|
//if we set it to "manual", then we should also add:
|
|
//m_networkArgs.Add("network_port", <portValue>);
|
|
//m_networkArgs.Add("network_mac", <macValue>);
|
|
|
|
m_networkArgs.Add("network_uuid", networkUuid);
|
|
|
|
if (isIpStatic)
|
|
{
|
|
m_networkArgs.Add("network_mode", "manual");
|
|
m_networkArgs.Add("network_ip", ip);
|
|
m_networkArgs.Add("network_mask", mask);
|
|
m_networkArgs.Add("network_gateway", gateway);
|
|
}
|
|
else
|
|
{
|
|
m_networkArgs.Add("network_mode", "dhcp");
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PRIVATE
|
|
|
|
private void OnUpdate(XenOvfTranportEventArgs e)
|
|
{
|
|
if (UpdateHandler != null)
|
|
UpdateHandler.Invoke(e);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
private void StartiScsiTarget(XenAPI.Session xenSession, string vdiuuid, bool read_only)
|
|
{
|
|
try
|
|
{
|
|
string host = XenAPI.Session.get_this_host(xenSession, xenSession.opaque_ref);
|
|
|
|
// Transfer VM for VDI <uuid>
|
|
Dictionary<string, string> args = new Dictionary<string, string>();
|
|
args.Add("vdi_uuid", vdiuuid);
|
|
args.Add("transfer_mode", "ISCSI");
|
|
args.Add("read_only", read_only ? "true" : "false");
|
|
args.Add("timeout_minutes", "1");
|
|
|
|
//Transfer VM IP settings
|
|
foreach (var kvp in m_networkArgs)
|
|
args.Add(kvp.Key, kvp.Value);
|
|
|
|
string record_handle = Host.call_plugin(xenSession, host, "transfer", "expose", args);
|
|
Dictionary<string, string> get_record_args = new Dictionary<string, string>();
|
|
get_record_args["record_handle"] = record_handle;
|
|
_pluginrecord = Host.call_plugin(xenSession, host, "transfer", "get_record", get_record_args);
|
|
_newscsi = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.ErrorFormat("{0} {1}", Messages.ISCSI_START_ERROR, ex.Message);
|
|
throw new Exception(Messages.ISCSI_START_ERROR, ex);
|
|
}
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Logging mechanism")]
|
|
private void StopiScsiTarget(XenAPI.Session xenSession)
|
|
{
|
|
try
|
|
{
|
|
string host = XenAPI.Session.get_this_host(xenSession, xenSession.opaque_ref);
|
|
Dictionary<string, string> args = new Dictionary<string, string>();
|
|
|
|
string handle;
|
|
if (!TryParsePluginRecordFor(_pluginrecord, "record_handle", out handle))
|
|
{
|
|
log.Debug("Transfer VM was not started. Will not attempt to shut it down.");
|
|
return;
|
|
}
|
|
|
|
args["record_handle"] = handle;
|
|
Host.call_plugin(xenSession, host, "transfer", "unexpose", args);
|
|
log.Debug("iSCSI.StopScsiTarget: iSCSI Target Destroyed.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.WarnFormat("{0} {1}", Messages.ISCSI_SHUTDOWN_ERROR, ex.Message);
|
|
throw new Exception(Messages.ISCSI_SHUTDOWN_ERROR, ex);
|
|
}
|
|
}
|
|
|
|
private static bool IsZeros(byte[] buff)
|
|
{
|
|
foreach (byte b in buff)
|
|
{
|
|
if (b != 0x0)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static bool TryParsePluginRecordFor(string rec, string name, out string outValue)
|
|
{
|
|
outValue = null;
|
|
System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
|
|
|
|
try
|
|
{
|
|
doc.LoadXml(rec);
|
|
foreach (System.Xml.XmlElement n in doc.GetElementsByTagName("transfer_record"))
|
|
{
|
|
outValue = n.GetAttribute(name);
|
|
return true;
|
|
}
|
|
}
|
|
catch (System.Xml.XmlException)
|
|
{
|
|
log.DebugFormat("Failed to parse the plugin record: '{0}'", rec);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|