mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-12-28 23:00:07 +01:00
9d2a204bb7
My commit fixes the error message so the dialog will show "Could not create SSL/TLS secure channel." instead of "An unknown error occurred". This text is localised. Unfortunately we see only a WebException with Status=SecureChannelFailure with no other details - couldn't provide an improved error message Signed-off-by: Gabor Apati-Nagy <gabor.apati-nagy@citrix.com>
2138 lines
83 KiB
C#
2138 lines
83 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.Globalization;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using CookComputing.XmlRpc;
|
|
using XenAdmin.Actions;
|
|
using XenAdmin.Core;
|
|
using XenAPI;
|
|
using System.Diagnostics;
|
|
|
|
using System.Xml.Serialization;
|
|
|
|
namespace XenAdmin.Network
|
|
{
|
|
|
|
[DebuggerDisplay("IXenConnection :{HostnameWithPort}")]
|
|
public class XenConnection : IXenConnection,IXmlSerializable
|
|
{
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
public string Hostname { get; set; }
|
|
public int Port { get; set; }
|
|
public string Username { get; set; }
|
|
public string Password { get; set; }
|
|
public string FriendlyName { get; set; }
|
|
|
|
/// <summary>
|
|
/// The last known name for the pool, in the form "'Pool friendly name' (hostname or IP)".
|
|
/// </summary>
|
|
public string LastConnectionFullName = "";
|
|
|
|
/// <summary>
|
|
/// Whether this connection is saved in a disconnected state (i.e. don't reconnect on session restore).
|
|
/// </summary>
|
|
public bool SaveDisconnected { get; set; }
|
|
|
|
/// <summary>
|
|
/// Indicates whether this connection was created in the Add Server dialog.
|
|
/// </summary>
|
|
public bool fromDialog = false;
|
|
|
|
/// <summary>
|
|
/// Whether we're expecting network disruption, say because we're reconfiguring the network at this time. In that case, we ignore
|
|
/// keepalive failures, and expect task polling to be disrupted.
|
|
/// </summary>
|
|
private volatile bool _expectedDisruption = false;
|
|
|
|
public bool ExpectDisruption { get { return _expectedDisruption; } set { _expectedDisruption = value; } }
|
|
|
|
/// <summary>
|
|
/// If we are 'expecting' this connection's Password property to contain the correct password
|
|
/// (i.e. false if the user has just entered the password, true if it was restored from the saved session).
|
|
/// </summary>
|
|
public bool ExpectPasswordIsCorrect { get; set; }
|
|
|
|
/// <summary>
|
|
/// Used by the patch wizard, supress any errors coming from reconnect attempts
|
|
/// </summary>
|
|
private volatile bool _supressErrors = false;
|
|
public bool SupressErrors { get { return _supressErrors; } set { _supressErrors = value; } }
|
|
|
|
/// <summary>
|
|
/// Indicates whether we are expecting the pool master to change soon (e.g. when explicitly designating a new master).
|
|
/// </summary>
|
|
private volatile bool _masterMayChange = false;
|
|
|
|
public bool MasterMayChange { get { return _masterMayChange; } set { _masterMayChange = value; } }
|
|
|
|
/// <summary>
|
|
/// Set when we detect that Event.next() has become blocked and we need to reset the connection. See CA-33145.
|
|
/// </summary>
|
|
private bool EventNextBlocked = false;
|
|
|
|
/// <summary>
|
|
/// A cache of the pool's opaque_ref, last time this connection was connected. This will be
|
|
/// Helper.NullOpaqueRef if it's never been connected.
|
|
/// </summary>
|
|
private string PoolOpaqueRef = Helper.NullOpaqueRef;
|
|
/// <summary>
|
|
/// Set after a successful connection attempt, before the cache is populated.
|
|
/// </summary>
|
|
private string MasterIPAddress = "";
|
|
|
|
/// <summary>
|
|
/// The lock that must be taken around connectTask and monitor.
|
|
/// </summary>
|
|
private readonly object connectTaskLock = new object();
|
|
private ConnectTask connectTask = null;
|
|
private Heartbeat heartbeat = null;
|
|
|
|
/// <summary>
|
|
/// Whether we are trying to automatically connect to the new master. Set in HandleConnectionLost.
|
|
/// Note: I think we are not using this correctly -- see CA-37864 for details -- but I'm not going
|
|
/// to fix it unless it gives rise to a reported bug, because I can't test the fix.
|
|
/// </summary>
|
|
private volatile bool FindingNewMaster = false;
|
|
|
|
/// <summary>
|
|
/// The time at which we started looking for the new master.
|
|
/// </summary>
|
|
private DateTime FindingNewMasterStartedAt = DateTime.MinValue;
|
|
|
|
/// <summary>
|
|
/// Timeout before we consider that Event.next() has got blocked: see CA-33145
|
|
/// </summary>
|
|
private const int EVENT_NEXT_TIMEOUT = 120 * 1000; // 2 minutes
|
|
|
|
private string LastMasterHostname = "";
|
|
public readonly object PoolMembersLock = new object();
|
|
private List<string> _poolMembers = new List<string>();
|
|
public List<string> PoolMembers { get { return _poolMembers; } set { _poolMembers = value; } }
|
|
private int PoolMemberIndex = 0;
|
|
private System.Threading.Timer ReconnectionTimer = null;
|
|
|
|
private ActionBase ConnectAction;
|
|
|
|
private DateTime m_startTime = DateTime.MinValue;
|
|
private int m_lastDebug;
|
|
private TimeSpan ServerTimeOffset_ = TimeSpan.Zero;
|
|
private object ServerTimeOffsetLock = new object();
|
|
/// <summary>
|
|
/// The offset between the clock at the client and the clock at the server. server time + ServerTimeOffset = client time.
|
|
/// This does not take the local timezone into account -- all calculations should be in UTC.
|
|
/// For Orlando and greater, this value is set by XenMetricsMonitor, calling Host.get_servertime on a heartbeat.
|
|
/// </summary>
|
|
public TimeSpan ServerTimeOffset
|
|
{
|
|
get
|
|
{
|
|
lock (ServerTimeOffsetLock)
|
|
{
|
|
return ServerTimeOffset_;
|
|
}
|
|
}
|
|
|
|
set
|
|
{
|
|
lock (ServerTimeOffsetLock)
|
|
{
|
|
TimeSpan diff = ServerTimeOffset_ - value;
|
|
|
|
if (diff.TotalSeconds < -1 || 1 < diff.TotalSeconds)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var debugMsg = string.Format("Time offset for {0} is now {1}. It's now {2} UTC here, and {3} UTC on the server.",
|
|
Hostname, value,
|
|
now.ToString("o", CultureInfo.InvariantCulture),
|
|
(now.Subtract(value)).ToString("o", CultureInfo.InvariantCulture));
|
|
|
|
if (m_startTime.Ticks == 0)//log it the first time it is detected
|
|
{
|
|
m_startTime = now;
|
|
log.Info(debugMsg);
|
|
}
|
|
|
|
//then log every 5mins
|
|
int currDebug = (int)((now - m_startTime).TotalSeconds) / 300;
|
|
if (currDebug > m_lastDebug)
|
|
{
|
|
m_lastDebug = currDebug;
|
|
log.InfoFormat(debugMsg);
|
|
}
|
|
}
|
|
|
|
ServerTimeOffset_ = value;
|
|
}
|
|
|
|
if (TimeSkewUpdated != null)
|
|
TimeSkewUpdated(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public string HostnameWithPort
|
|
{
|
|
get
|
|
{
|
|
return Port == ConnectionsManager.DEFAULT_XEN_PORT ? Hostname : Hostname + ':' + Port.ToString();
|
|
}
|
|
}
|
|
|
|
public string UriScheme
|
|
{
|
|
get
|
|
{
|
|
return Port == 8080 || Port == 80 ? Uri.UriSchemeHttp : Uri.UriSchemeHttps;
|
|
}
|
|
}
|
|
|
|
public string Version { get; set; }
|
|
|
|
public string Name
|
|
{
|
|
get
|
|
{
|
|
string result = Helpers.GetName(Helpers.GetPoolOfOne(this));
|
|
return !string.IsNullOrEmpty(result)
|
|
? result
|
|
: !string.IsNullOrEmpty(FriendlyName) ? FriendlyName : Hostname;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cache of XenAPI objects for this connection.
|
|
/// </summary>
|
|
private readonly ICache _cache = new Cache();
|
|
public ICache Cache { get { return _cache; } }
|
|
|
|
private readonly LockFreeQueue<ObjectChange> eventQueue = new LockFreeQueue<ObjectChange>();
|
|
private readonly System.Threading.Timer cacheUpdateTimer;
|
|
|
|
/// <summary>
|
|
/// Whether the cache for this connection has been populated.
|
|
/// </summary>
|
|
private bool cacheIsPopulated = false;
|
|
public bool CacheIsPopulated
|
|
{
|
|
get { return cacheIsPopulated; }
|
|
}
|
|
|
|
private bool cacheUpdaterRunning = false;
|
|
private bool updatesWaiting = false;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="XenConnection"/> class.
|
|
/// </summary>
|
|
public XenConnection()
|
|
{
|
|
Port = ConnectionsManager.DEFAULT_XEN_PORT;
|
|
Username = "root";
|
|
SaveDisconnected = false;
|
|
ExpectPasswordIsCorrect = true;
|
|
cacheUpdateTimer = new System.Threading.Timer(cacheUpdater);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used by the automated tests. Initializes a new instance of the <see cref="XenConnection"/> class.
|
|
/// <param name="hostname"></param>
|
|
/// <param name="friendlyName"></param>
|
|
/// </summary>
|
|
public XenConnection(string hostname, string friendlyName)
|
|
: this()
|
|
{
|
|
this.Hostname = hostname;
|
|
this.FriendlyName = friendlyName;
|
|
this.Port = 443;
|
|
this.SaveDisconnected = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For use by unit tests only.
|
|
/// </summary>
|
|
/// <param name="user"></param>
|
|
/// <param name="password"></param>
|
|
/// <returns></returns>
|
|
public Session Connect(string user, string password)
|
|
{
|
|
heartbeat = new Heartbeat(this, XenAdminConfigManager.Provider.ConnectionTimeout);
|
|
Session session = SessionFactory.CreateSession(this, Hostname, Port);
|
|
|
|
try
|
|
{
|
|
session.login_with_password(user, password, Helper.APIVersionString(API_Version.LATEST), Session.UserAgent);
|
|
|
|
// this is required so connection.IsConnected returns true in the unit tests.
|
|
connectTask = new ConnectTask("test", 0);
|
|
connectTask.Connected = true;
|
|
connectTask.Session = session;
|
|
|
|
return session;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used by unit tests only.
|
|
/// </summary>
|
|
/// <param name="session">The session.</param>
|
|
public void LoadCache(Session session)
|
|
{
|
|
this.Cache.Clear();
|
|
if (XenObjectDownloader.LegacyEventSystem(session))
|
|
XenObjectDownloader.RegisterForEvents(session);
|
|
|
|
cacheIsPopulated = false;
|
|
|
|
string token = "";
|
|
bool legacyEventSystem = XenObjectDownloader.LegacyEventSystem(session);
|
|
XenObjectDownloader.GetAllObjects(session, eventQueue, () => false, legacyEventSystem, ref token);
|
|
List<ObjectChange> events = new List<ObjectChange>();
|
|
|
|
while (eventQueue.NotEmpty)
|
|
events.Add(eventQueue.Dequeue());
|
|
|
|
this.Cache.UpdateFrom(this, events);
|
|
cacheIsPopulated = true;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fired just before the cache is cleared (i.e. the cache is still populated).
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> ClearingCache;
|
|
public event EventHandler<EventArgs> CachePopulated;
|
|
public event EventHandler<ConnectionResultEventArgs> ConnectionResult;
|
|
public event EventHandler<EventArgs> ConnectionStateChanged;
|
|
public event EventHandler<EventArgs> ConnectionLost;
|
|
public event EventHandler<EventArgs> ConnectionClosed;
|
|
public event EventHandler<EventArgs> ConnectionReconnecting;
|
|
public event EventHandler<EventArgs> BeforeConnectionEnd;
|
|
public event EventHandler<ConnectionMessageChangedEventArgs> ConnectionMessageChanged;
|
|
public event EventHandler<ConnectionMajorChangeEventArgs> BeforeMajorChange;
|
|
public event EventHandler<ConnectionMajorChangeEventArgs> AfterMajorChange;
|
|
|
|
/// <summary>
|
|
/// Fired on the UI thread, once per batch of events in CacheUpdater.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> XenObjectsUpdated;
|
|
public event EventHandler<EventArgs> TimeSkewUpdated;
|
|
|
|
public bool IsConnected
|
|
{
|
|
get
|
|
{
|
|
// sneaky, but avoids a lock
|
|
ConnectTask t = connectTask;
|
|
return t != null && t.Connected;
|
|
}
|
|
}
|
|
|
|
public bool InProgress
|
|
{
|
|
get { return connectTask != null; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the Session passed to ConnectWorkerThread and used to update the cache in XenObjectDownloader.
|
|
/// May return null.
|
|
/// </summary>
|
|
public Session Session
|
|
{
|
|
get
|
|
{
|
|
ConnectTask t = connectTask;
|
|
return t == null ? null : t.Session;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a duplicate of the Session that this connection is currently using.
|
|
/// This will use a separate TCP stream, but the same authentication credentials.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public Session DuplicateSession()
|
|
{
|
|
return DuplicateSession(Session.STANDARD_TIMEOUT);
|
|
}
|
|
|
|
public Session DuplicateSession(int timeout)
|
|
{
|
|
Session s = Session;
|
|
if (s == null)
|
|
throw new DisconnectionException();
|
|
return SessionFactory.CreateSession(s, this, timeout);
|
|
}
|
|
|
|
/// <summary>
|
|
/// For retrieving an extra session using different credentials to those stored in the connection. Used for the sudo
|
|
/// function in actions. Does not prompt for new credentials if the authentication fails.
|
|
/// Does not change connection's Username and Password.
|
|
/// </summary>
|
|
/// <param name="username"></param>
|
|
/// <param name="password"></param>
|
|
/// <returns></returns>
|
|
public Session ElevatedSession(string username, string password)
|
|
{
|
|
return GetNewSession(Hostname, Port, username, password, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// For retrieving a new session. Changes connection's Username and Password.
|
|
/// </summary>
|
|
/// <param name="hostname"></param>
|
|
/// <param name="port"></param>
|
|
/// <param name="username"></param>
|
|
/// <param name="password"></param>
|
|
/// <param name="isElevated"></param>
|
|
/// <returns></returns>
|
|
private Session NewSession(string hostname, int port, string username, string password, bool isElevated)
|
|
{
|
|
Password = password;
|
|
Username = username;
|
|
|
|
return GetNewSession(hostname, port, username, password, isElevated);
|
|
}
|
|
|
|
/// <summary>
|
|
/// For retrieving a new session.
|
|
/// </summary>
|
|
/// <param name="hostname"></param>
|
|
/// <param name="port"></param>
|
|
/// <param name="username"></param>
|
|
/// <param name="password"></param>
|
|
/// <param name="isElevated"></param>
|
|
/// <returns>null if the server password has changed, the user has been prompted for the new password,
|
|
/// but the user has clicked cancel on the dialog.</returns>
|
|
private Session GetNewSession(string hostname, int port, string username, string password, bool isElevated)
|
|
{
|
|
const int DELAY = 250; // unit = ms
|
|
int attempt = 0;
|
|
while (true)
|
|
{
|
|
attempt++;
|
|
string uname = isElevated ? username : Username;
|
|
string pwd = isElevated ? password : Password; // Keep the password that we're using for this iteration, as it may
|
|
// be changed by another thread handling an authentication failure.
|
|
// For elevated session we use the elevated username and password passed into this function,
|
|
// as the connection's Username and Password are not updated.
|
|
|
|
Session session = SessionFactory.CreateSession(this, hostname, port);
|
|
if (isElevated)
|
|
session.IsElevatedSession = true;
|
|
try
|
|
{
|
|
session.login_with_password(uname, pwd, !string.IsNullOrEmpty(Version) ? Version : Helper.APIVersionString(API_Version.LATEST), Session.UserAgent);
|
|
return session;
|
|
}
|
|
catch (Failure f)
|
|
{
|
|
if (connectTask == null || connectTask.Cancelled) // the user has clicked cancel on the connection to server dialog
|
|
{
|
|
attempt = DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS; // make sure we throw rather than try again
|
|
throw new CancelledException(); // make the dialog pop up again
|
|
}
|
|
else if (f.ErrorDescription.Count > 0)
|
|
{
|
|
switch (f.ErrorDescription[0])
|
|
{
|
|
case Failure.SESSION_AUTHENTICATION_FAILED:
|
|
if (isElevated)
|
|
throw;
|
|
if (PromptForNewPassword(pwd))
|
|
attempt = 0;
|
|
else
|
|
{
|
|
//user cannot provide correct credentials, we d/c now to save the confusion of having the server available
|
|
//but unusable.
|
|
EndConnect();
|
|
OnConnectionClosed();
|
|
throw new CancelledException();
|
|
}
|
|
break;
|
|
case Failure.HOST_IS_SLAVE:
|
|
// we know it is a slave so there there is no need to try and connect again, we need to connect to the master
|
|
case Failure.RBAC_PERMISSION_DENIED:
|
|
// No point retrying this, the user needs the read only role at least to log in
|
|
case Failure.HOST_UNKNOWN_TO_MASTER:
|
|
// Will never succeed, CA-74718
|
|
throw;
|
|
default:
|
|
if (isElevated)
|
|
{
|
|
if (attempt >= DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS)
|
|
throw;
|
|
else
|
|
break;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (attempt >= DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS)
|
|
throw;
|
|
}
|
|
}
|
|
catch (WebException e)
|
|
{
|
|
if (e.Status == WebExceptionStatus.TrustFailure)
|
|
throw new CancelledException();
|
|
if (e.Status == WebExceptionStatus.NameResolutionFailure || e.Status == WebExceptionStatus.ProtocolError || attempt >= DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS)
|
|
throw;
|
|
}
|
|
catch (UriFormatException)
|
|
{
|
|
// No point trying to connect more than once to a duff URI
|
|
throw;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
if (attempt >= DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS)
|
|
throw;
|
|
}
|
|
Thread.Sleep(DELAY);
|
|
}
|
|
}
|
|
|
|
// This CompareTo() mimics the sort order we get from XenSearch, with
|
|
// pools first, then hosts, then disconnected connections. It is used
|
|
// in some dialogs where we don't do a proper search: see CA-57131 &
|
|
// CA-60517 for examples.
|
|
public int CompareTo(IXenConnection other)
|
|
{
|
|
if (this == other)
|
|
return 0;
|
|
|
|
if (other == null)
|
|
return -1;
|
|
|
|
Pool thisPool = Helpers.GetPool(this);
|
|
Pool otherPool = Helpers.GetPool(other);
|
|
|
|
int thisClass = (this.IsConnected ?
|
|
(thisPool == null ? 2 : 1) : 3);
|
|
int otherClass = (other.IsConnected ?
|
|
(otherPool == null ? 2 : 1) : 3);
|
|
|
|
if (thisClass != otherClass)
|
|
return thisClass - otherClass;
|
|
|
|
int result = StringUtility.NaturalCompare(Name, other.Name);
|
|
if (result != 0)
|
|
return result;
|
|
|
|
Pool p1 = Helpers.GetPoolOfOne(this);
|
|
Pool p2 = Helpers.GetPoolOfOne(other);
|
|
if (p1 == null || p2 == null)
|
|
return 0; // shouldn't happen once connected, but let's be safe
|
|
|
|
return p1.opaque_ref.CompareTo(p2.opaque_ref);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the pool and master details in the Action to allow proper filtering in HistoryPanel.
|
|
/// </summary>
|
|
private void SetPoolAndHostInAction(ActionBase action)
|
|
{
|
|
Pool pool = Helpers.GetPoolOfOne(this);
|
|
if (pool != null)
|
|
SetPoolAndHostInAction(action, pool, PoolOpaqueRef);
|
|
}
|
|
|
|
private void SetPoolAndHostInAction(ActionBase action, Pool pool, string poolopaqueref)
|
|
{
|
|
if (pool != null && !string.IsNullOrEmpty(poolopaqueref))
|
|
{
|
|
Pool p = new Pool();
|
|
p.Connection = this;
|
|
p.opaque_ref = poolopaqueref;
|
|
p.UpdateFrom(pool);
|
|
|
|
Host h = new Host();
|
|
h.Connection = this;
|
|
h.opaque_ref = pool.master.opaque_ref;
|
|
h.name_label = Hostname;
|
|
|
|
action.Pool = p;
|
|
action.Host = h;
|
|
}
|
|
else
|
|
{
|
|
// Match the hack in XenSearch/GroupAlg.cs. We are creating fake Host objects to
|
|
// represent the disconnected server, with the opaque_ref set to the connection's HostnameWithPort.
|
|
// We need to do the same here, so that Actions for disconnected hosts (like failed connections)
|
|
// are attached to the disconnected server correctly.
|
|
Host host = new Host();
|
|
host.Connection = this;
|
|
host.opaque_ref = HostnameWithPort;
|
|
action.Host = host;
|
|
}
|
|
}
|
|
|
|
private Func<IXenConnection, string, bool> _promptForNewPassword;
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="initiateMasterSearch">If true, if connection to the master fails we will start trying to connect to
|
|
/// each remembered slave in turn.</param>
|
|
/// <param name="promptForNewPassword">A function that prompts the user for the changed password for a server.</param>
|
|
public void BeginConnect(bool initiateMasterSearch, Func<IXenConnection, string, bool> promptForNewPassword)
|
|
{
|
|
_promptForNewPassword = promptForNewPassword;
|
|
|
|
//InvokeHelper.Synchronizer is used for synchronizing the cache update. Must not be null at this point. It can be initialized through InvokeHelper.Initialize()
|
|
Trace.Assert(InvokeHelper.Synchronizer != null);
|
|
|
|
InvokeHelper.AssertOnEventThread();
|
|
|
|
if (initiateMasterSearch)
|
|
{
|
|
FindingNewMaster = true;
|
|
FindingNewMasterStartedAt = DateTime.Now;
|
|
}
|
|
MasterMayChange = false;
|
|
|
|
if (!HandlePromptForNewPassword())
|
|
return;
|
|
|
|
lock (connectTaskLock)
|
|
{
|
|
if (connectTask == null)
|
|
{
|
|
ClearEventQueue();
|
|
OnBeforeMajorChange(false);
|
|
Cache.Clear();
|
|
OnAfterMajorChange(false);
|
|
connectTask = new ConnectTask(Hostname, Port);
|
|
StopMonitor();
|
|
heartbeat = new Heartbeat(this, XenAdminConfigManager.Provider.ConnectionTimeout);
|
|
Thread t = new Thread(ConnectWorkerThread);
|
|
t.Name = "Connection to " + Hostname;
|
|
t.IsBackground = true;
|
|
t.Start(connectTask);
|
|
}
|
|
else
|
|
{
|
|
// a connection is already in progress
|
|
}
|
|
}
|
|
}
|
|
|
|
private static object WaitForMonitor = new object();
|
|
private static int WaitForEventRegistered = 0;
|
|
private static object WaitForEventRegisteredLock = new object();
|
|
public void WaitFor(Func<bool> predicate, Func<bool> cancelling)
|
|
{
|
|
lock (WaitForEventRegisteredLock)
|
|
{
|
|
if (WaitForEventRegistered == 0)
|
|
XenObjectsUpdated += WakeWaitFor;
|
|
WaitForEventRegistered++;
|
|
}
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
lock (WaitForMonitor)
|
|
{
|
|
if (predicate() || (cancelling != null && cancelling()))
|
|
return;
|
|
System.Threading.Monitor.Wait(WaitForMonitor, 500);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (WaitForEventRegisteredLock)
|
|
{
|
|
WaitForEventRegistered--;
|
|
if (WaitForEventRegistered == 0)
|
|
XenObjectsUpdated -= WakeWaitFor;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WakeWaitFor(object sender, EventArgs e)
|
|
{
|
|
lock (WaitForMonitor)
|
|
{
|
|
System.Threading.Monitor.PulseAll(WaitForMonitor);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Equivalent to WaitForCache(xenref, null).
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="xenref"></param>
|
|
/// <returns></returns>
|
|
public T WaitForCache<T>(XenRef<T> xenref) where T : XenObject<T>
|
|
{
|
|
return WaitForCache(xenref, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wait for the given object to arrive in our cache. Returns the XenObject, or null if it does
|
|
/// not appear within the timeout (1 minute).
|
|
///
|
|
/// This blocks, so must only be used on a background thread.
|
|
/// </summary>
|
|
/// <param name="xenRef"></param>
|
|
/// <param name="Cancelling">A delegate to check whether to cancel. May be null, in which case it's ignored</param>
|
|
public T WaitForCache<T>(XenRef<T> xenref, Func<bool> cancelling) where T : XenObject<T>
|
|
{
|
|
lock (WaitForEventRegisteredLock)
|
|
{
|
|
if (WaitForEventRegistered == 0)
|
|
XenObjectsUpdated += WakeWaitFor;
|
|
WaitForEventRegistered++;
|
|
}
|
|
try
|
|
{
|
|
for (int i = 0; i < 120; i++)
|
|
{
|
|
lock (WaitForMonitor)
|
|
{
|
|
T result = Resolve<T>(xenref);
|
|
if (result != null || (cancelling != null && cancelling()))
|
|
return result;
|
|
System.Threading.Monitor.Wait(WaitForMonitor, 500);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
lock (WaitForEventRegisteredLock)
|
|
{
|
|
WaitForEventRegistered--;
|
|
if (WaitForEventRegistered == 0)
|
|
XenObjectsUpdated -= WakeWaitFor;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Equivalent to EndConnect(true).
|
|
/// i.e. Clears the cache.
|
|
/// </summary>
|
|
public void EndConnect()
|
|
{
|
|
EndConnect(true);
|
|
}
|
|
|
|
/// <param name="resetState">Whether the cache should be cleared (requires invoking onto the GUI thread)</param>
|
|
public void EndConnect(bool resetState)
|
|
{
|
|
ConnectTask t = connectTask;
|
|
connectTask = null;
|
|
EndConnect(resetState, t);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the connecting dialog, stops the XenMetricsMonitor thread, marks this.task as Cancelled and
|
|
/// logs out of the task's Session on a background thread.
|
|
/// </summary>
|
|
/// <param name="clearCache">Whether the cache should be cleared (requires invoking onto the GUI thread)</param>
|
|
/// <param name="task"></param>
|
|
private void EndConnect(bool clearCache, ConnectTask task)
|
|
{
|
|
OnBeforeConnectionEnd();
|
|
|
|
lock (connectTaskLock)
|
|
{
|
|
StopMonitor();
|
|
if (task != null)
|
|
{
|
|
task.Cancelled = true;
|
|
Session session = task.Session;
|
|
task.Session = null;
|
|
if (session != null)
|
|
{
|
|
Logout(session);
|
|
}
|
|
}
|
|
}
|
|
|
|
MarkConnectActionComplete();
|
|
|
|
// Save list of addresses of current hosts in pool
|
|
List<string> members = new List<string>();
|
|
foreach (Host host in Cache.Hosts)
|
|
{
|
|
members.Add(host.address);
|
|
}
|
|
lock (PoolMembersLock)
|
|
{
|
|
PoolMembers = members;
|
|
}
|
|
|
|
// Clear the XenAPI object cache
|
|
if (clearCache)
|
|
{
|
|
ClearCache();
|
|
}
|
|
|
|
_promptForNewPassword = null;
|
|
OnConnectionStateChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to logout the given session. This will cause any threads blocking on Event.next() to get
|
|
/// a XenAPI.Failure (which is better than them hanging around forever).
|
|
/// Do on a background thread - otherwise, if the master has died, then this will block
|
|
/// until the timeout is reached (default 20s).
|
|
/// </summary>
|
|
/// <param name="session">May be null, in which case nothing happens.</param>
|
|
public void Logout(Session session)
|
|
{
|
|
if (session == null || session.uuid == null)
|
|
return;
|
|
|
|
Thread t = new Thread(Logout_);
|
|
t.Name = string.Format("Logging out session {0}", session.uuid);
|
|
t.IsBackground = true;
|
|
t.Priority = ThreadPriority.Lowest;
|
|
t.Start(session);
|
|
}
|
|
|
|
public void Logout()
|
|
{
|
|
Logout(Session);
|
|
}
|
|
|
|
private static void Logout_(object o)
|
|
{
|
|
Session session = (Session)o;
|
|
try
|
|
{
|
|
log.Debug("Trying Session.logout() on background thread");
|
|
session.logout();
|
|
log.Debug("Session.logout() succeeded");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
log.Debug("Session.logout() failed", e);
|
|
}
|
|
}
|
|
|
|
public void Interrupt()
|
|
{
|
|
ConnectTask t = connectTask;
|
|
ICache coll = Cache;
|
|
connectTask = null;
|
|
if (t != null && t.Connected)
|
|
{
|
|
string poolopaqueref = null;
|
|
Pool pool = coll == null ? null : getAPool(coll, out poolopaqueref);
|
|
t.Cancelled = true;
|
|
HandleConnectionLost(t, pool, poolopaqueref);
|
|
}
|
|
}
|
|
|
|
private void ClearCache()
|
|
{
|
|
OnClearingCache();
|
|
ClearEventQueue();
|
|
// This call to Clear needs to occur on the background thread, otherwise the event firing in response to all the changes
|
|
// block in one big lump rather than the smaller pieces that you get when invoking onto the event thread on a finer
|
|
// granularity. If you do all this on the event thread, then the app tends to go (Not Responding) when you lose a connection.
|
|
// It doesn't actually occur on the background thread all the time. There's a path from AddServerDialog.ConnectToServer.
|
|
Cache.Clear();
|
|
}
|
|
|
|
private void OnCachePopulated()
|
|
{
|
|
lock (connectTaskLock)
|
|
{
|
|
if (heartbeat != null)
|
|
heartbeat.Start();
|
|
}
|
|
|
|
if (CachePopulated != null)
|
|
CachePopulated(this, EventArgs.Empty);
|
|
|
|
MarkConnectActionComplete();
|
|
}
|
|
|
|
private string GetReason(Exception error)
|
|
{
|
|
if (error is CookComputing.XmlRpc.XmlRpcServerException)
|
|
{
|
|
return string.Format(Messages.SERVER_FAILURE, error.Message);
|
|
}
|
|
else if (error is ArgumentException)
|
|
{
|
|
// This happens if the server API is incompatible with our bindings. This should
|
|
// never happen in production, but will happen during development if a field
|
|
// changes type, for example.
|
|
return Messages.SERVER_API_INCOMPATIBLE;
|
|
}
|
|
else if (error is WebException)
|
|
{
|
|
WebException w = error as WebException;
|
|
if (w.Status == WebExceptionStatus.NameResolutionFailure)
|
|
{
|
|
return string.Format(Messages.CONNECT_RESOLUTION_FAILURE, this.Hostname);
|
|
}
|
|
else if (w.Status == WebExceptionStatus.ConnectFailure)
|
|
{
|
|
return string.Format(Messages.CONNCET_CONNECTION_FAILURE, this.Hostname);
|
|
}
|
|
else if (w.Status == WebExceptionStatus.ReceiveFailure)
|
|
{
|
|
return string.Format(Messages.CONNECT_NO_XAPI_FAILURE, this.Hostname);
|
|
}
|
|
else if (w.Status == WebExceptionStatus.SecureChannelFailure)
|
|
{
|
|
return string.Format(Messages.ERROR_SECURE_CHANNEL_FAILURE, this.Hostname);
|
|
}
|
|
else
|
|
{
|
|
return w.Message;
|
|
}
|
|
}
|
|
else if (error is Failure && error != null && !string.IsNullOrEmpty(error.Message))
|
|
{
|
|
Failure f = error as Failure;
|
|
if (f.ErrorDescription[0] == Failure.RBAC_PERMISSION_DENIED)
|
|
{
|
|
// we use a different error message here from the standard one in friendly names
|
|
return Messages.ERROR_NO_PERMISSION;
|
|
}
|
|
return error.Message;
|
|
}
|
|
else if (error is NullReferenceException && error.Source.StartsWith("CookComputing"))
|
|
{
|
|
return string.Format(Messages.CONNCET_CONNECTION_FAILURE, this.Hostname);
|
|
}
|
|
else if (error != null && !string.IsNullOrEmpty(error.Message))
|
|
{
|
|
return error.Message;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void HandleSuccessfulConnection(string taskHostname, int task_port)
|
|
{
|
|
// add server name to history (if it's not already there)
|
|
XenAdminConfigManager.Provider.UpdateServerHistory(HostnameWithPort);
|
|
|
|
if (!ConnectionsManager.XenConnectionsContains(this))
|
|
{
|
|
lock (ConnectionsManager.ConnectionsLock)
|
|
{
|
|
ConnectionsManager.XenConnections.Add(this);
|
|
}
|
|
|
|
InvokeHelper.Invoke(XenAdminConfigManager.Provider.SaveSettingsIfRequired);
|
|
}
|
|
|
|
log.InfoFormat("Connected to {0} ({1}:{2})", FriendlyName, taskHostname, task_port);
|
|
string name = string.IsNullOrEmpty(FriendlyName) || FriendlyName == taskHostname
|
|
? taskHostname
|
|
: string.Format("{0} ({1})", FriendlyName, taskHostname);
|
|
string title = string.Format(Messages.CONNECTING_NOTICE_TITLE, name);
|
|
string msg = string.Format(Messages.CONNECTING_NOTICE_TEXT, name);
|
|
log.Info(msg);
|
|
|
|
ConnectAction = new ActionBase(title, msg, false, false);
|
|
|
|
ExpectPasswordIsCorrect = true;
|
|
OnConnectionResult(true, null, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check the password isn't null, which happens when the session is restored without remembering passwords.
|
|
/// </summary>
|
|
/// <returns>Whether to continue.</returns>
|
|
private bool HandlePromptForNewPassword()
|
|
{
|
|
if (Password == null && !PromptForNewPassword(Password))
|
|
{
|
|
// if false the user has cancelled, set the password back to null and return
|
|
Password = null;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void HandleConnectionTermination()
|
|
{
|
|
// clean up action so we dont stay open forever
|
|
if (ConnectAction != null)
|
|
ConnectAction.IsCompleted = true;
|
|
}
|
|
|
|
private readonly object PromptLock = new object();
|
|
/// <summary>
|
|
/// Prompts the user for the changed password for a server.
|
|
/// </summary>
|
|
/// <param name="old_password"></param>
|
|
/// <returns></returns>
|
|
private bool PromptForNewPassword(string old_password)
|
|
{
|
|
// Serialise prompting for new passwords, so that we don't get multiple dialogs pop up.
|
|
lock (PromptLock)
|
|
{
|
|
if (Password != old_password)
|
|
{
|
|
// Some other thread has changed the password already. Retry using that one.
|
|
return true;
|
|
}
|
|
|
|
bool result = (_promptForNewPassword != null) ? _promptForNewPassword(this, old_password) : false;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private void ClearEventQueue()
|
|
{
|
|
while (eventQueue.NotEmpty)
|
|
{
|
|
eventQueue.Dequeue();
|
|
}
|
|
|
|
// Suspend the cache update timer
|
|
cacheUpdateTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
}
|
|
|
|
private void EventsPending()
|
|
{
|
|
lock (cacheUpdateTimer)
|
|
{
|
|
if (cacheUpdaterRunning)
|
|
updatesWaiting = true;
|
|
else
|
|
cacheUpdateTimer.Change(50, -1);
|
|
}
|
|
}
|
|
|
|
private void cacheUpdater(object state)
|
|
{
|
|
lock (cacheUpdateTimer)
|
|
{
|
|
if (cacheUpdaterRunning)
|
|
{
|
|
// there is a race-condition here which can be observed under high load: It is possible for the timer to fire even when
|
|
// cacheUpdaterRunning = true. The check ensures we don't get multiple threads calling cacheUpdater
|
|
// on the same connection.
|
|
updatesWaiting = true;
|
|
return;
|
|
}
|
|
|
|
cacheUpdaterRunning = true;
|
|
updatesWaiting = false;
|
|
}
|
|
|
|
try
|
|
{
|
|
cacheUpdater_();
|
|
}
|
|
finally
|
|
{
|
|
bool waiting;
|
|
lock (cacheUpdateTimer)
|
|
{
|
|
waiting = updatesWaiting;
|
|
cacheUpdaterRunning = false;
|
|
}
|
|
if (waiting)
|
|
cacheUpdater(null);
|
|
}
|
|
}
|
|
|
|
private void cacheUpdater_()
|
|
{
|
|
// Copy events off the event queue
|
|
// as we don't want events delivered while we're
|
|
// on the GUI thread to be included in this set of
|
|
// updates, otherwise we might hose the gui thread
|
|
// during an event storm (ie deleting 1000 vms)
|
|
List<ObjectChange> events = new List<ObjectChange>();
|
|
|
|
while (eventQueue.NotEmpty)
|
|
events.Add(eventQueue.Dequeue());
|
|
|
|
if (events.Count > 0)
|
|
{
|
|
InvokeHelper.Invoke(delegate()
|
|
{
|
|
try
|
|
{
|
|
OnBeforeMajorChange(false);
|
|
bool fire = Cache.UpdateFrom(this, events);
|
|
OnAfterMajorChange(false);
|
|
|
|
if (fire)
|
|
OnXenObjectsUpdated();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
log.Error("Exception updating cache.", e);
|
|
log.Debug(e, e);
|
|
#if DEBUG
|
|
if (System.Diagnostics.Debugger.IsAttached)
|
|
throw;
|
|
#endif
|
|
}
|
|
|
|
});
|
|
|
|
if (!cacheIsPopulated)
|
|
{
|
|
cacheIsPopulated = true;
|
|
try
|
|
{
|
|
OnCachePopulated();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
log.Error("Exception calling OnCachePopulated.", e);
|
|
log.Debug(e, e);
|
|
#if DEBUG
|
|
if (System.Diagnostics.Debugger.IsAttached)
|
|
throw;
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// if not called the action will never finish and the gui will never close
|
|
/// </summary>
|
|
private void MarkConnectActionComplete()
|
|
{
|
|
if (ConnectAction != null && !ConnectAction.IsCompleted)
|
|
{
|
|
string title = string.Format(Messages.CONNECTION_OK_NOTICE_TITLE, Hostname);
|
|
string msg = string.Format(Messages.CONNECTION_OK_NOTICE_TEXT, Hostname);
|
|
log.Info(msg);
|
|
ConnectAction.Title = title;
|
|
ConnectAction.Description = msg;
|
|
SetPoolAndHostInAction(ConnectAction);
|
|
|
|
// mark the connect action as completed
|
|
ConnectAction.Finished = DateTime.Now;
|
|
ConnectAction.PercentComplete = 100;
|
|
ConnectAction.IsCompleted = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops this connection's XenMetricsMonitor thread.
|
|
/// Expects to be locked under connectTaskLock.
|
|
/// </summary>
|
|
private void StopMonitor()
|
|
{
|
|
if (heartbeat != null)
|
|
{
|
|
heartbeat.Stop();
|
|
heartbeat = null;
|
|
}
|
|
}
|
|
|
|
private const int DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS = 3;
|
|
|
|
private bool IsSimulatorConnection
|
|
{
|
|
get
|
|
{
|
|
return Helpers.DbProxyIsSimulatorUrl(this.Hostname);
|
|
}
|
|
}
|
|
|
|
private readonly string eventNextConnectionGroupName = Guid.NewGuid().ToString();
|
|
|
|
/// <summary>
|
|
/// The main method for a connection to a XenServer. This method runs until the connection is lost, and
|
|
/// contains a loop that after doing an initial cache fill processes events off the wire when they arrive.
|
|
/// </summary>
|
|
/// <param name="o"></param>
|
|
private void ConnectWorkerThread(object o)
|
|
{
|
|
ConnectTask task = (ConnectTask)o;
|
|
Exception error = null;
|
|
Pool pool = null;
|
|
|
|
try
|
|
{
|
|
log.DebugFormat("IXenConnection: trying to connect to {0}", HostnameWithPort);
|
|
|
|
Session session = NewSession(task.Hostname, task.Port, Username, Password, false);
|
|
// Save the session so we can log it out later
|
|
task.Session = session;
|
|
|
|
if (session.APIVersion <= API_Version.API_1_8)
|
|
throw new ServerNotSupported();
|
|
|
|
// Event.next uses a different session with a shorter timeout: see CA-33145.
|
|
Session eventNextSession = DuplicateSession(EVENT_NEXT_TIMEOUT);
|
|
eventNextSession.ConnectionGroupName = eventNextConnectionGroupName; // this will force the eventNextSession onto its own set of TCP streams (see CA-108676)
|
|
|
|
bool legacyEventSystem = XenObjectDownloader.LegacyEventSystem(session);
|
|
|
|
if (legacyEventSystem)
|
|
XenObjectDownloader.RegisterForEvents(session);
|
|
|
|
cacheIsPopulated = false;
|
|
session.CacheWarming = true;
|
|
|
|
string token = "";
|
|
bool eventsExceptionLogged = false;
|
|
|
|
while (true)
|
|
{
|
|
if (task.Cancelled)
|
|
break;
|
|
|
|
EventNextBlocked = false;
|
|
|
|
if (session.CacheWarming)
|
|
{
|
|
if (!task.Connected)
|
|
{
|
|
// We've started cache sync: update the dialog text
|
|
OnConnectionMessageChanged(string.Format(Messages.LABEL_SYNC, this.Hostname));
|
|
}
|
|
|
|
XenObjectDownloader.GetAllObjects(session, eventQueue, task.GetCancelled, legacyEventSystem, ref token);
|
|
session.CacheWarming = false;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
XenObjectDownloader.GetEvents(eventNextSession, eventQueue, task.GetCancelled, legacyEventSystem, ref token);
|
|
eventsExceptionLogged = false;
|
|
}
|
|
catch (Exception exn)
|
|
{
|
|
if (!ExpectDisruption)
|
|
throw;
|
|
|
|
log.DebugFormat("Exception (disruption is expected) in XenObjectDownloader.GetEvents: {0}", exn.GetType().Name);
|
|
|
|
// ignoring some exceptions when disruption is expected
|
|
if (exn is XmlRpcIllFormedXmlException ||
|
|
exn is System.IO.IOException ||
|
|
(exn is WebException && ((exn as WebException).Status == WebExceptionStatus.KeepAliveFailure || (exn as WebException).Status == WebExceptionStatus.ConnectFailure)))
|
|
{
|
|
if (!eventsExceptionLogged)
|
|
{
|
|
log.Debug("Ignoring keepalive/connect failure, because disruption is expected");
|
|
eventsExceptionLogged = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check if requested to cancel
|
|
if (task.Cancelled)
|
|
break;
|
|
|
|
if (!task.Connected)
|
|
{
|
|
lock (ConnectionsManager.ConnectionsLock)
|
|
{
|
|
pool = ObjectChange.GetPool(eventQueue, out PoolOpaqueRef);
|
|
Host master = ObjectChange.GetMaster(eventQueue);
|
|
|
|
MasterIPAddress = master.address;
|
|
|
|
foreach (IXenConnection iconn in ConnectionsManager.XenConnections)
|
|
{
|
|
XenConnection connection = iconn as XenConnection;
|
|
Trace.Assert(connection != null);
|
|
if (!connection.IsConnected)
|
|
continue;
|
|
|
|
bool sameRef = PoolOpaqueRef == connection.PoolOpaqueRef;
|
|
|
|
if (!sameRef)
|
|
continue;
|
|
|
|
bool sameMaster = MasterIPAddress == connection.MasterIPAddress;
|
|
|
|
if (sameRef && sameMaster)
|
|
{
|
|
throw new ConnectionExists(connection);
|
|
}
|
|
else
|
|
{
|
|
// CA-15633: XenCenter does not allow connection to host
|
|
// on which backup is restored.
|
|
|
|
throw new BadRestoreDetected(connection);
|
|
}
|
|
}
|
|
|
|
task.Connected = true;
|
|
|
|
FriendlyName =
|
|
!string.IsNullOrEmpty(pool.Name) ? pool.Name :
|
|
!string.IsNullOrEmpty(master.Name) ? master.Name :
|
|
task.Hostname;
|
|
} // ConnectionsLock
|
|
|
|
// Remove any other (disconnected) entries for this server from the tree
|
|
List<IXenConnection> existingConnections = new List<IXenConnection>();
|
|
foreach (IXenConnection connection in ConnectionsManager.XenConnections)
|
|
{
|
|
if (connection.Hostname.Equals(task.Hostname) && !connection.IsConnected)
|
|
{
|
|
existingConnections.Add(connection);
|
|
}
|
|
}
|
|
foreach (IXenConnection connection in existingConnections)
|
|
{
|
|
ConnectionsManager.ClearCacheAndRemoveConnection(connection);
|
|
}
|
|
|
|
|
|
log.DebugFormat("Getting server time for pool {0} ({1})...", FriendlyName, PoolOpaqueRef);
|
|
SetServerTimeOffset(session, pool.master.opaque_ref);
|
|
|
|
// add server name to history (if it's not already there)
|
|
XenAdminConfigManager.Provider.UpdateServerHistory(HostnameWithPort);
|
|
|
|
HandleSuccessfulConnection(task.Hostname, task.Port);
|
|
|
|
try
|
|
{
|
|
SetPoolAndHostInAction(ConnectAction, pool, PoolOpaqueRef);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
log.Error(e, e);
|
|
}
|
|
|
|
log.DebugFormat("Completed connection phase for pool {0} ({1}).", FriendlyName, PoolOpaqueRef);
|
|
}
|
|
|
|
EventsPending();
|
|
}
|
|
}
|
|
catch (Failure e)
|
|
{
|
|
if (task.Cancelled && e.ErrorDescription.Count > 0 && e.ErrorDescription[0] == Failure.SESSION_INVALID)
|
|
{
|
|
// Do nothing: this is probably a result of the user disconnecting, and us calling session.logout()
|
|
}
|
|
else
|
|
{
|
|
error = e;
|
|
log.Warn(e);
|
|
}
|
|
}
|
|
catch (WebException e)
|
|
{
|
|
error = e;
|
|
log.Debug(e.Message);
|
|
}
|
|
catch (XmlRpcFaultException e)
|
|
{
|
|
error = e;
|
|
log.Debug(e.Message);
|
|
}
|
|
catch (XmlRpcException e)
|
|
{
|
|
error = e;
|
|
log.Debug(e.Message);
|
|
}
|
|
catch (TargetInvocationException e)
|
|
{
|
|
error = e.InnerException;
|
|
log.Error("TargetInvocationException", e);
|
|
}
|
|
catch (UriFormatException e)
|
|
{
|
|
// This can happen when the user types gobbledy-gook into the host-name field
|
|
// of the add server dialog...
|
|
error = e;
|
|
log.Debug(e.Message);
|
|
}
|
|
catch (CancelledException)
|
|
{
|
|
task.Cancelled = true;
|
|
}
|
|
catch (DisconnectionException e)
|
|
{
|
|
error = e;
|
|
log.Debug(e.Message);
|
|
}
|
|
catch (EventNextBlockedException e)
|
|
{
|
|
EventNextBlocked = true;
|
|
error = e;
|
|
log.Error(e, e);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
error = e;
|
|
log.Error(e, e);
|
|
}
|
|
finally
|
|
{
|
|
HandleConnectionResult(task, error, pool);
|
|
}
|
|
}
|
|
|
|
private void HandleConnectionResult(ConnectTask task, Exception error, Pool pool)
|
|
{
|
|
if (task != connectTask)
|
|
{
|
|
// We've been superceded by a newer ConnectTask. Exit silently without firing events.
|
|
// Can happen when user disconnects while sync is taking place, then reconnects
|
|
// (creating a new _connectTask) before the sync is complete.
|
|
}
|
|
else
|
|
{
|
|
ClearEventQueue();
|
|
|
|
connectTask = null;
|
|
HandleConnectionTermination();
|
|
|
|
if (error is ExpressRestriction)
|
|
{
|
|
EndConnect(true, task);
|
|
|
|
ExpressRestriction e = (ExpressRestriction)error;
|
|
string msg = string.Format(Messages.CONNECTION_RESTRICTED_MESSAGE, e.HostName, e.ExistingHostName);
|
|
// Add an informational log message saying why the connection attempt failed
|
|
log.Info(msg);
|
|
string title = string.Format(Messages.CONNECTION_RESTRICTED_NOTICE_TITLE, e.HostName);
|
|
ActionBase action = new ActionBase(title, msg, false, true, msg);
|
|
SetPoolAndHostInAction(action, pool, PoolOpaqueRef);
|
|
|
|
OnConnectionResult(false, Messages.CONNECTION_RESTRICTED_MESSAGE, error);
|
|
}
|
|
else if (error is ServerNotSupported)
|
|
{
|
|
EndConnect(true, task);
|
|
log.Info(error.Message);
|
|
OnConnectionResult(false, error.Message, error);
|
|
}
|
|
else if (task.Cancelled)
|
|
{
|
|
task.Connected = false;
|
|
log.InfoFormat("IXenConnection: closing connection to {0}", this.HostnameWithPort);
|
|
EndConnect(true, task);
|
|
OnConnectionClosed();
|
|
}
|
|
else if (task.Connected)
|
|
{
|
|
HandleConnectionLost(task, pool, PoolOpaqueRef);
|
|
}
|
|
else
|
|
{
|
|
// We never connected
|
|
string reason = GetReason(error);
|
|
log.WarnFormat("IXenConnection: failed to connect to {0}: {1}", HostnameWithPort, reason);
|
|
|
|
Failure f = error as Failure;
|
|
if (f != null && f.ErrorDescription[0] == Failure.HOST_IS_SLAVE)
|
|
{
|
|
//do not log an event in this case
|
|
}
|
|
else if (error is ConnectionExists)
|
|
{
|
|
//do not log an event in this case
|
|
}
|
|
else
|
|
{
|
|
// Create a new log message to say the connection attempt failed
|
|
string title = string.Format(Messages.CONNECTION_FAILED_TITLE, HostnameWithPort);
|
|
ActionBase n = new ActionBase(title, reason, false, true, reason);
|
|
SetPoolAndHostInAction(n, pool, PoolOpaqueRef);
|
|
}
|
|
|
|
// We only want to continue the master search in certain circumstances
|
|
if (FindingNewMaster && (error is WebException || (f != null && f.ErrorDescription[0] != Failure.RBAC_PERMISSION_DENIED)))
|
|
{
|
|
if (f != null)
|
|
{
|
|
if (f.ErrorDescription[0] == XenAPI.Failure.HOST_IS_SLAVE)
|
|
{
|
|
log.DebugFormat("Found a slave of {0} at {1}; redirecting to the master at {2}",
|
|
LastMasterHostname, Hostname, f.ErrorDescription[1]);
|
|
Hostname = f.ErrorDescription[1];
|
|
OnConnectionMessageChanged(string.Format(Messages.CONNECTION_REDIRECTING, LastMasterHostname, Hostname));
|
|
ReconnectMaster();
|
|
}
|
|
else if (f.ErrorDescription[0] == XenAPI.Failure.HOST_STILL_BOOTING)
|
|
{
|
|
log.DebugFormat("Found a slave of {0} at {1}, but it's still booting; trying the next pool member",
|
|
LastMasterHostname, Hostname);
|
|
MaybeStartNextSlaveTimer(reason, error);
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat("Found a slave of {0} at {1}, but got a failure; trying the next pool member",
|
|
LastMasterHostname, Hostname);
|
|
MaybeStartNextSlaveTimer(reason, error);
|
|
}
|
|
}
|
|
else if (PoolMemberRemaining())
|
|
{
|
|
log.DebugFormat("Connection to {0} failed; trying the next pool member", Hostname);
|
|
MaybeStartNextSlaveTimer(reason, error);
|
|
}
|
|
else
|
|
{
|
|
if (ExpectDisruption || DateTime.Now - FindingNewMasterStartedAt < SEARCH_NEW_MASTER_STOP_AFTER)
|
|
{
|
|
log.DebugFormat("While trying to find a connection for {0}, tried to connect to every remembered host. Will now loop back through pool members again.",
|
|
this.HostnameWithPort);
|
|
lock (PoolMembersLock)
|
|
{
|
|
PoolMemberIndex = 0;
|
|
}
|
|
MaybeStartNextSlaveTimer(reason, error);
|
|
}
|
|
else if (LastMasterHostname != "")
|
|
{
|
|
log.DebugFormat("Stopping search for new master for {0}: timeout reached without success. Trying the old master one last time",
|
|
LastConnectionFullName);
|
|
FindingNewMaster = false;
|
|
Hostname = LastMasterHostname;
|
|
ReconnectMaster();
|
|
}
|
|
else
|
|
{
|
|
OnConnectionResult(false, reason, error);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OnConnectionResult(false, reason, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetServerTimeOffset(Session session, string master_opaqueref)
|
|
{
|
|
DateTime t = Host.get_servertime(session, master_opaqueref);
|
|
ServerTimeOffset = DateTime.UtcNow - t;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a connection that had been made successfully is then lost.
|
|
/// </summary>
|
|
/// <param name="task"></param>
|
|
/// <param name="pool"></param>
|
|
/// <param name="poolopaqueref"></param>
|
|
private void HandleConnectionLost(ConnectTask task, Pool pool, string poolopaqueref)
|
|
{
|
|
task.Connected = false;
|
|
log.WarnFormat("Lost connection to {0}", this.HostnameWithPort);
|
|
|
|
// Cancel all current actions to do with this connection
|
|
if (!ExpectDisruption)
|
|
{
|
|
InvokeHelper.Invoke(() => ConnectionsManager.CancelAllActions(this));
|
|
}
|
|
// Save list of addresses of current hosts in pool
|
|
List<string> members = new List<string>();
|
|
foreach (Host host in Cache.Hosts)
|
|
{
|
|
if (!string.IsNullOrEmpty(host.address))
|
|
members.Add(host.address);
|
|
}
|
|
|
|
// Save master's address so we don't try to reconnect to it first
|
|
Host master = Helpers.GetMaster(this);
|
|
// Save ha_enabled status before we clear the cache
|
|
bool ha_enabled = IsHAEnabled();
|
|
|
|
// NB line below clears the cache
|
|
EndConnect(true, task);
|
|
|
|
string description;
|
|
LastMasterHostname = Hostname;
|
|
string poolName = pool.Name;
|
|
if (string.IsNullOrEmpty(poolName))
|
|
{
|
|
LastConnectionFullName = HostnameWithPort;
|
|
}
|
|
else
|
|
{
|
|
LastConnectionFullName = string.Format("'{0}' ({1})", poolName, HostnameWithPort);
|
|
}
|
|
if (!EventNextBlocked && (MasterMayChange || ha_enabled) && members.Count > 1)
|
|
{
|
|
log.DebugFormat("Will now try to connect to another pool member");
|
|
|
|
lock (PoolMembersLock)
|
|
{
|
|
PoolMembers.Clear();
|
|
PoolMembers.AddRange(members);
|
|
PoolMemberIndex = 0;
|
|
// Don't reconnect to the master straight away, try a slave first
|
|
if (master != null && PoolMembers[0] == master.address && PoolMembers.Count > 1)
|
|
{
|
|
PoolMemberIndex = 1;
|
|
}
|
|
}
|
|
FindingNewMaster = true;
|
|
// Record the time at which we started the new master search.
|
|
FindingNewMasterStartedAt = DateTime.Now;
|
|
StartReconnectMasterTimer();
|
|
description = string.Format(Messages.CONNECTION_LOST_NOTICE_MASTER_IN_X_SECONDS, LastConnectionFullName, XenConnection.SEARCH_NEW_MASTER_TIMEOUT_MS / 1000);
|
|
log.DebugFormat("Beginning search for new master; will give up after {0} seconds", SEARCH_NEW_MASTER_STOP_AFTER.TotalSeconds);
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat("Will retry connection to {0} in {1} ms.", LastConnectionFullName, ReconnectHostTimeoutMs);
|
|
|
|
StartReconnectSingleHostTimer();
|
|
|
|
description = string.Format(Messages.CONNECTION_LOST_RECONNECT_IN_X_SECONDS, LastConnectionFullName, ReconnectHostTimeoutMs / 1000);
|
|
}
|
|
|
|
string title = string.Format(Messages.CONNECTION_LOST_NOTICE_TITLE,
|
|
LastConnectionFullName);
|
|
ActionBase n = new ActionBase(title, description, false, true, description);
|
|
SetPoolAndHostInAction(n, pool, poolopaqueref);
|
|
OnConnectionLost();
|
|
}
|
|
|
|
private bool PoolMemberRemaining()
|
|
{
|
|
lock (PoolMembersLock)
|
|
{
|
|
return PoolMemberIndex < PoolMembers.Count;
|
|
}
|
|
}
|
|
|
|
private bool IsHAEnabled()
|
|
{
|
|
Pool pool = Helpers.GetPoolOfOne(this);
|
|
return pool != null && pool.ha_enabled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// When we lose connection to a non-HA host, the timeout before we try reconnecting.
|
|
/// </summary>
|
|
private const int RECONNECT_HOST_TIMEOUT_MS = 120 * 1000;
|
|
private const int RECONNECT_SHORT_TIMEOUT_MS = 5 * 1000;
|
|
private int ReconnectHostTimeoutMs
|
|
{
|
|
get
|
|
{
|
|
if (EventNextBlocked || IsSimulatorConnection)
|
|
return RECONNECT_SHORT_TIMEOUT_MS;
|
|
else
|
|
return RECONNECT_HOST_TIMEOUT_MS;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When HA is enabled, the timeout after losing connection to the master before we start searching for a new master.
|
|
/// i.e. This should be the time it takes master failover to be sorted out on the server, plus a margin.
|
|
/// NB we already have an additional built-in delay - it takes time for us to decide that the host is not responding,
|
|
/// and kill the connection to the dead host, before starting the search.
|
|
/// </summary>
|
|
private const int SEARCH_NEW_MASTER_TIMEOUT_MS = 60 * 1000;
|
|
/// <summary>
|
|
/// When HA is enabled, and going through each of the slaves to try and find the new master, the time between failing
|
|
/// to connect to one slave and trying to connect to the next in the list.
|
|
/// </summary>
|
|
private const int SEARCH_NEXT_SLAVE_TIMEOUT_MS = 15 * 1000;
|
|
/// <summary>
|
|
/// When going through each of the remembered members of the pool looking for the new master, don't start another pass
|
|
/// through connecting to each of the hosts if we've already been looking for this long.
|
|
/// </summary>
|
|
private static readonly TimeSpan SEARCH_NEW_MASTER_STOP_AFTER = TimeSpan.FromMinutes(6);
|
|
|
|
private void StartReconnectSingleHostTimer()
|
|
{
|
|
ReconnectionTimer =
|
|
new System.Threading.Timer((TimerCallback)ReconnectSingleHostTimer, null,
|
|
ReconnectHostTimeoutMs, ReconnectHostTimeoutMs);
|
|
}
|
|
|
|
private void StartReconnectMasterTimer()
|
|
{
|
|
StartReconnectMasterTimer(SEARCH_NEW_MASTER_TIMEOUT_MS);
|
|
}
|
|
|
|
private void MaybeStartNextSlaveTimer(string reason, Exception error)
|
|
{
|
|
if (PoolMemberRemaining())
|
|
StartReconnectMasterTimer(SEARCH_NEXT_SLAVE_TIMEOUT_MS);
|
|
else
|
|
OnConnectionResult(false, reason, error);
|
|
}
|
|
|
|
private void StartReconnectMasterTimer(int timeout)
|
|
{
|
|
OnConnectionMessageChanged(string.Format(Messages.CONNECTION_WILL_RETRY_SLAVE, LastConnectionFullName.Ellipsise(25) , timeout / 1000));
|
|
ReconnectionTimer =
|
|
new System.Threading.Timer((TimerCallback)ReconnectMasterTimer, null,
|
|
timeout, Timeout.Infinite);
|
|
}
|
|
|
|
private void ReconnectSingleHostTimer(object state)
|
|
{
|
|
if (IsConnected || !ConnectionsManager.XenConnectionsContains(this))
|
|
{
|
|
log.DebugFormat("Host {0} already reconnected", Hostname);
|
|
if (ReconnectionTimer != null)
|
|
{
|
|
ReconnectionTimer.Dispose();
|
|
ReconnectionTimer = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!ExpectDisruption) // only try once unless expect disruption
|
|
{
|
|
if (ReconnectionTimer != null)
|
|
{
|
|
ReconnectionTimer.Dispose();
|
|
ReconnectionTimer = null;
|
|
}
|
|
}
|
|
|
|
log.DebugFormat("Reconnecting to server {0}...", Hostname);
|
|
if (!XenAdminConfigManager.Provider.Exiting)
|
|
{
|
|
InvokeHelper.Invoke(delegate()
|
|
{
|
|
/*ConnectionResult += new EventHandler<ConnectionResultEventArgs>(XenConnection_ConnectionResult);
|
|
CachePopulated += new EventHandler<EventArgs>(XenConnection_CachePopulated);*/
|
|
BeginConnect(false, _promptForNewPassword);
|
|
});
|
|
OnConnectionReconnecting();
|
|
}
|
|
}
|
|
|
|
/*void XenConnection_CachePopulated(object sender, EventArgs e)
|
|
{
|
|
CachePopulated -= new EventHandler<EventArgs>(XenConnection_CachePopulated);
|
|
Program.MainWindow.CommandInterface.TrySelectNewObjectInTree(this, false, true, false);
|
|
}
|
|
|
|
void XenConnection_ConnectionResult(object sender, ConnectionResultEventArgs e)
|
|
{
|
|
if (!e.Connected)
|
|
CachePopulated -= new EventHandler<EventArgs>(XenConnection_CachePopulated);
|
|
}*/
|
|
|
|
private void ReconnectMasterTimer(object state)
|
|
{
|
|
if (IsConnected || !ConnectionsManager.XenConnectionsContains(this))
|
|
{
|
|
log.DebugFormat("Master has been found for {0} at {1}", LastMasterHostname, Hostname);
|
|
return;
|
|
}
|
|
|
|
lock (PoolMembersLock)
|
|
{
|
|
if (PoolMemberIndex < PoolMembers.Count)
|
|
{
|
|
Hostname = PoolMembers[PoolMemberIndex];
|
|
PoolMemberIndex++;
|
|
}
|
|
}
|
|
|
|
OnConnectionMessageChanged(string.Format(Messages.CONNECTION_RETRYING_SLAVE, LastConnectionFullName.Ellipsise(25), Hostname));
|
|
ReconnectMaster();
|
|
}
|
|
|
|
private void ReconnectMaster()
|
|
{
|
|
// Add an informational entry to the log
|
|
string title = string.Format(Messages.CONNECTION_FINDING_MASTER_TITLE, LastConnectionFullName);
|
|
string descr = string.Format(Messages.CONNECTION_FINDING_MASTER_DESCRIPTION, LastConnectionFullName, Hostname);
|
|
ActionBase action = new ActionBase(title, descr, false, true);
|
|
SetPoolAndHostInAction(action, null, PoolOpaqueRef);
|
|
log.DebugFormat("Looking for master for {0} on {1}...", LastConnectionFullName, Hostname);
|
|
|
|
if (!XenAdminConfigManager.Provider.Exiting)
|
|
{
|
|
InvokeHelper.Invoke(delegate()
|
|
{
|
|
/*ConnectionResult += new EventHandler<ConnectionResultEventArgs>(XenConnection_ConnectionResult);
|
|
CachePopulated += new EventHandler<EventArgs>(XenConnection_CachePopulated);*/
|
|
BeginConnect(false, _promptForNewPassword);
|
|
});
|
|
OnConnectionReconnecting();
|
|
}
|
|
}
|
|
|
|
private Pool getAPool(ICache objects, out string opaqueref)
|
|
{
|
|
foreach (Pool pool in objects.Pools)
|
|
{
|
|
opaqueref = pool.opaque_ref;
|
|
return pool;
|
|
}
|
|
System.Diagnostics.Trace.Assert(false);
|
|
opaqueref = null;
|
|
return null;
|
|
}
|
|
|
|
private void OnClearingCache()
|
|
{
|
|
if (ClearingCache != null)
|
|
ClearingCache(this, new EventArgs());
|
|
}
|
|
|
|
private void OnConnectionResult(bool connected, string reason, Exception error)
|
|
{
|
|
if (ConnectionResult != null)
|
|
ConnectionResult(this, new ConnectionResultEventArgs(connected, reason, error));
|
|
OnConnectionStateChanged();
|
|
}
|
|
|
|
private void OnConnectionClosed()
|
|
{
|
|
if (ConnectionClosed != null)
|
|
ConnectionClosed(this, null);
|
|
OnConnectionStateChanged();
|
|
}
|
|
|
|
private void OnConnectionLost()
|
|
{
|
|
if (ConnectionLost != null)
|
|
ConnectionLost(this, null);
|
|
OnConnectionStateChanged();
|
|
}
|
|
|
|
private void OnConnectionReconnecting()
|
|
{
|
|
if (ConnectionReconnecting != null)
|
|
ConnectionReconnecting(this, null);
|
|
OnConnectionStateChanged();
|
|
}
|
|
|
|
private void OnBeforeConnectionEnd()
|
|
{
|
|
if (BeforeConnectionEnd != null)
|
|
BeforeConnectionEnd(this, null);
|
|
}
|
|
|
|
private void OnConnectionStateChanged()
|
|
{
|
|
if (ConnectionStateChanged != null)
|
|
ConnectionStateChanged(this, null);
|
|
}
|
|
|
|
private void OnConnectionMessageChanged(string message)
|
|
{
|
|
if (ConnectionMessageChanged != null)
|
|
ConnectionMessageChanged(this, new ConnectionMessageChangedEventArgs(message));
|
|
}
|
|
|
|
public void OnBeforeMajorChange(bool background)
|
|
{
|
|
if (BeforeMajorChange != null)
|
|
BeforeMajorChange(this, new ConnectionMajorChangeEventArgs(background));
|
|
}
|
|
|
|
public void OnAfterMajorChange(bool background)
|
|
{
|
|
if (AfterMajorChange != null)
|
|
AfterMajorChange(this, new ConnectionMajorChangeEventArgs(background));
|
|
}
|
|
|
|
private void OnXenObjectsUpdated()
|
|
{
|
|
// Using BeginInvoke here means that the XenObjectsUpdated event gets fired after any
|
|
// CollectionChanged events fired by ChangeableDictionary during Cache.UpdateFrom.
|
|
InvokeHelper.BeginInvoke(delegate()
|
|
{
|
|
if (XenObjectsUpdated != null)
|
|
XenObjectsUpdated(this, null);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stub to Cache.Resolve
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="xenRef">May be null, in which case null is returned.</param>
|
|
/// <returns></returns>
|
|
public virtual T Resolve<T>(XenRef<T> xenRef) where T : XenObject<T>
|
|
{
|
|
return Cache.Resolve(xenRef);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve every object in the given list. Skip any references that don't resolve.
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="xenRefs">May be null, in which case the empty list is returned.</param>
|
|
/// <returns></returns>
|
|
public List<T> ResolveAll<T>(IEnumerable<XenRef<T>> xenRefs) where T : XenObject<T>
|
|
{
|
|
List<T> result = new List<T>();
|
|
if (xenRefs != null)
|
|
{
|
|
foreach (XenRef<T> xenRef in xenRefs)
|
|
{
|
|
T o = Resolve(xenRef);
|
|
if (o != null)
|
|
result.Add(o);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public List<VDI> ResolveAllShownXenModelObjects(List<XenRef<VDI>> xenRefs, bool showHiddenObjects)
|
|
{
|
|
List<VDI> result = ResolveAll(xenRefs);
|
|
result.RemoveAll(delegate(VDI vdi)
|
|
{
|
|
return !vdi.Show(showHiddenObjects);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
public static T FindByUUIDXenObject<T>(string uuid) where T : XenObject<T>
|
|
{
|
|
foreach (IXenConnection c in ConnectionsManager.XenConnectionsCopy)
|
|
{
|
|
T o = c.Cache.Find_By_Uuid<T>(uuid);
|
|
if (o != null)
|
|
return o;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find a XenObject corresponding to the given XenRef, or null if no such object is found.
|
|
/// </summary>
|
|
public static T FindByRef<T>(XenRef<T> needle) where T : XenObject<T>
|
|
{
|
|
foreach (IXenConnection c in ConnectionsManager.XenConnectionsCopy)
|
|
{
|
|
T o = c.Resolve<T>(needle);
|
|
if (o != null)
|
|
return o;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static string ConnectedElsewhere(string hostname)
|
|
{
|
|
lock (ConnectionsManager.ConnectionsLock)
|
|
{
|
|
foreach (IXenConnection connection in ConnectionsManager.XenConnections)
|
|
{
|
|
Pool pool = Helpers.GetPoolOfOne(connection);
|
|
if (pool == null)
|
|
continue;
|
|
|
|
Host master = connection.Resolve(pool.master);
|
|
if (master == null)
|
|
continue;
|
|
|
|
if (master.address == hostname)
|
|
{
|
|
// we have tried to connect to a slave that is a member of a pool we are already connected to.
|
|
return pool.Name;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#region IXmlSerializable Members
|
|
|
|
public System.Xml.Schema.XmlSchema GetSchema()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public void ReadXml(System.Xml.XmlReader reader)
|
|
{
|
|
Hostname = reader["Hostname"];
|
|
}
|
|
|
|
public void WriteXml(System.Xml.XmlWriter writer)
|
|
{
|
|
writer.WriteElementString("Hostname",Hostname);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable Members
|
|
|
|
/// <summary>
|
|
/// Disposing this class will make it unusable - make sure you want to do this
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private bool disposed;
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if(!disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
ClearingCache = null;
|
|
CachePopulated = null;
|
|
ConnectionResult = null;
|
|
ConnectionStateChanged = null;
|
|
ConnectionLost = null;
|
|
ConnectionClosed = null;
|
|
ConnectionReconnecting = null;
|
|
BeforeConnectionEnd = null;
|
|
ConnectionMessageChanged = null;
|
|
BeforeMajorChange = null;
|
|
AfterMajorChange = null;
|
|
XenObjectsUpdated = null;
|
|
TimeSkewUpdated = null;
|
|
if (ReconnectionTimer != null)
|
|
ReconnectionTimer.Dispose();
|
|
if (cacheUpdateTimer != null)
|
|
cacheUpdateTimer.Dispose();
|
|
}
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
}
|
|
|
|
|
|
public class ExpressRestriction : DisconnectionException
|
|
{
|
|
public readonly string HostName;
|
|
public readonly string ExistingHostName;
|
|
|
|
public ExpressRestriction(string HostName, string ExistingHostName)
|
|
{
|
|
this.HostName = HostName;
|
|
this.ExistingHostName = ExistingHostName;
|
|
}
|
|
|
|
public override string Message
|
|
{
|
|
get
|
|
{
|
|
return string.Format(Messages.LICENSE_RESTRICTION_MESSAGE, HostName, ExistingHostName);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ServerNotSupported : DisconnectionException
|
|
{
|
|
public override string Message
|
|
{
|
|
get
|
|
{
|
|
return Messages.SERVER_TOO_OLD;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ConnectionExists : DisconnectionException
|
|
{
|
|
public IXenConnection connection;
|
|
|
|
public ConnectionExists(IXenConnection connection)
|
|
{
|
|
this.connection = connection;
|
|
}
|
|
|
|
public override string Message
|
|
{
|
|
get
|
|
{
|
|
if (connection != null)
|
|
return string.Format(Messages.CONNECTION_EXISTS, connection.Hostname);
|
|
else
|
|
return Messages.CONNECTION_EXISTS_NULL;
|
|
}
|
|
}
|
|
|
|
public virtual string GetDialogMessage(IXenConnection _this)
|
|
{
|
|
Pool p = Helpers.GetPool(connection);
|
|
if (p == null)
|
|
return String.Format(Messages.ALREADY_CONNECTED, _this.Hostname);
|
|
|
|
return String.Format(Messages.SLAVE_ALREADY_CONNECTED,
|
|
_this.Hostname, p.Name);
|
|
}
|
|
}
|
|
|
|
class BadRestoreDetected : ConnectionExists
|
|
{
|
|
public BadRestoreDetected(IXenConnection xc)
|
|
: base(xc)
|
|
{
|
|
}
|
|
|
|
public override string Message
|
|
{
|
|
get
|
|
{
|
|
return String.Format(Messages.BAD_RESTORE_DETECTED, connection.Name);
|
|
}
|
|
}
|
|
|
|
public override string GetDialogMessage(IXenConnection _this)
|
|
{
|
|
return Message;
|
|
}
|
|
}
|
|
}
|