xenadmin/XenModel/Actions/AsyncAction.cs
Konstantina Chremmou 62586ef89c CP-5750: Removed ActionType; the actions should be categorised by status (in progress,
succeeded, failed; cancelled at the moment counts as failed, we may need to distinguish
in future). As a consequence the filter checkboxes from the top of the Log tab
were removed and some temporary changes were made to the drawing of action rows
(the controls will change completely in a subsequent commit).

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
2013-08-09 17:20:38 +01:00

508 lines
18 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.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using CookComputing.XmlRpc;
using XenAdmin.Core;
using XenAdmin.Network;
using XenAPI;
namespace XenAdmin.Actions
{
public abstract class AsyncAction : CancellingAction
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private readonly Func<List<Role>, IXenConnection, string, SudoElevationResult> sudoDialog = XenAdminConfigManager.Provider.SudoDialogDelegate;
private string _result;
protected AsyncAction(IXenConnection connection, string title, string description, bool suppressHistory)
: base(title, description, suppressHistory)
{
this.Connection = connection;
}
protected AsyncAction(IXenConnection connection, string title, string description)
: this(connection, title, description, false)
{
}
protected AsyncAction(IXenConnection connection, string title)
: this(connection, title, "", false)
{
}
protected AsyncAction(IXenConnection connection, string title, bool suppress_history)
: this(connection, title, "", suppress_history)
{
}
public string Result
{
get
{
if (Exception != null)
{
throw Exception;
}
else
{
return _result;
}
}
set { _result = value; }
}
/// <summary>
/// If you wish the action to run a pre-check that the current user can perform all the necessary api calls, list them under this field.
/// If empty, then no checks will be run.
/// </summary>
protected RbacMethodList ApiMethodsToRoleCheck = new RbacMethodList();
public virtual RbacMethodList GetApiMethodsToRoleCheck
{
get { return ApiMethodsToRoleCheck; }
}
/// <summary>
/// If the sudoUsername and sudoPassword fields are both not null, then the action will use these credentials when making new sessions.
/// </summary>
public string sudoUsername;
/// <summary>
/// If the sudoUsername and sudoPassword fields are both not null, then the action will use these credentials when making new sessions.
/// </summary>
public string sudoPassword;
/// <summary>
/// Checks to see if we are using elevated credentials for this action. Returns a session using them if they exist, otherwise
/// using the basic credentials of the IXenConnection. Important - will throw exceptions similar to connection.NewSession
/// </summary>
/// <returns></returns>
public Session NewSession()
{
if (Connection == null)
return null;
if (String.IsNullOrEmpty(sudoPassword) || String.IsNullOrEmpty(sudoUsername))
return Connection.DuplicateSession();
return Connection.ElevatedSession(sudoUsername, sudoPassword);
}
/// <summary>
/// Used for cross connection actions (e.g adding a host to a pool, we need to get a session from the connection we are joining)
/// Checks to see if we are using elevated credentials for this action. Returns a session using them if they exist, otherwise
/// using the basic credentials of the _supplied_ IXenConnection. Important - will throw exceptions similar to connection.NewSession
/// </summary>
/// <param name="xc"></param>
/// <returns></returns>
public Session NewSession(IXenConnection xc)
{
if (Connection == null)
return null;
if (String.IsNullOrEmpty(sudoPassword) || String.IsNullOrEmpty(sudoUsername))
return xc.DuplicateSession();
return xc.ElevatedSession(sudoUsername, sudoPassword);
}
public static bool ForcedExiting
{
get { return XenAdminConfigManager.Provider.ForcedExiting; }
}
/// <summary>
/// Prepare the action's task for exit by removing the XenCenterUUID.
/// A call here just before exit will mean that the task will get picked
/// up as a meddling action on restart of xencenter, and thus reappear in the log.
/// </summary>
public void PrepareForLogReloadAfterRestart()
{
try
{
Task.RemoveXenCenterUUID(Session, RelatedTask.opaque_ref);
}
catch(KeyNotFoundException)
{
log.Debug("Removing XenCenterUUID failed - KeyNotFound");
}
catch(NullReferenceException)
{
log.Debug("Removing XenCenterUUID failed - NullReference");
}
catch (WebException)
{
log.Debug("Removing XenCenterUUID failed - Could not connect through http");
}
}
public virtual void RunAsync()
{
AuditLogStarted();
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(RunWorkerThread), null);
}
/// <summary>
/// Use this function to run this action non-async, but do the appropriate tidy up code.
/// If a session is passed in, which it always should be if called from another Action,
/// use that session for the action: it is then the responsibility of the calling function
/// to make sure the session has the appropriate privileges and tidy it up afterwards.
/// </summary>
public void RunExternal(Session session)
{
RunWorkerThread(session);
if (Exception != null)
throw Exception;
}
protected abstract void Run();
/// <param name="o">A session to use for this action: if null, construct a new session and sudo it if necessary</param>
private void RunWorkerThread(object o)
{
StartedRunning = true;
if (Cancelled) // already cancelled before it's started
return;
try
{
// Check that the current user credentials are enough to complete the api calls in this action (if specified)
if (o != null)
Session = (Session)o;
else
SetSessionByRole();
Run();
AuditLogSuccess();
MarkCompleted();
}
catch (CancelledException e)
{
Cancelled = true;
AuditLogCancelled();
MarkCompleted(e);
}
catch (Exception e)
{
Failure f = e as Failure;
if (f != null && Connection != null && f.ErrorDescription[0] == Failure.RBAC_PERMISSION_DENIED)
{
Failure.ParseRBACFailure(f, Connection, Session ?? Connection.Session);
}
log.Error(e);
log.Error(e.StackTrace);
AuditLogFailure();
MarkCompleted(e);
}
finally
{
Clean();
if (Exception != null)
CleanOnError();
if (o == null && Session != null && Session.IsElevatedSession)
{
// The session is a new, sudo-ed session: we need to log these ones out
try
{
Session.logout();
}
catch(Failure ex)
{
log.Debug("Session.logout() failed for Session uuid " + Session.uuid, ex);
}
}
Session = null;
LogoutCancelSession();
}
}
public virtual void DestroyTask()
{
//Program.AssertOffEventThread();
if (Session == null || string.IsNullOrEmpty(Session.uuid) || RelatedTask == null)
return;
try
{
PerformSilentTaskOp(delegate() { XenAPI.Task.destroy(Session, RelatedTask); });
}
finally
{
RelatedTask = null;
}
}
public void PollToCompletion(int start, int finish)
{
new TaskPoller(this, start, finish).PollToCompletion();
}
public void PollToCompletion(double start, double finish)
{
PollToCompletion((int)start, (int)finish);
}
/// <summary>
/// Equivalent to PollToCompletion(0, 100).
/// </summary>
public void PollToCompletion()
{
PollToCompletion(0, 100);
}
/// <summary>
/// If the action has detailed the Api calls that it will make then find the set of roles that can complete the entire action. If the
/// current user's role is not on that list then show the Role Elevation Dialog to give them the oppertunity to change to a new user.
/// </summary>
private void SetSessionByRole()
{
if (Connection == null
|| Connection.Session == null
|| Session != null) // We have been pre-seeded with a Session to use
return;
RbacMethodList rbacMethodList;
if (Connection.Session.IsLocalSuperuser || !Helpers.MidnightRideOrGreater(Connection) || XenAdminConfigManager.Provider.DontSudo) // don't need to / can't / don't want to sudo
rbacMethodList = new RbacMethodList();
else
rbacMethodList = GetApiMethodsToRoleCheck;
if (rbacMethodList.Count == 0)
{
Session = NewSession();
return;
}
List<Role> rolesAbleToCompleteAction;
bool ableToCompleteAction = Role.CanPerform(rbacMethodList, Connection, out rolesAbleToCompleteAction);
log.DebugFormat("Roles able to complete action: {0}", Role.FriendlyCSVRoleList(rolesAbleToCompleteAction));
log.DebugFormat("Subject {0} has roles: {1}", Connection.Session.UserLogName, Role.FriendlyCSVRoleList(Connection.Session.Roles));
if (ableToCompleteAction)
{
log.Debug("Subject authorized to complete action");
Session = Connection.Session;
return;
}
log.Debug("Subject not authorized to complete action, showing sudo dialog");
var result = sudoDialog(rolesAbleToCompleteAction, Connection, Title);
if (result.Result)
{
sudoUsername = result.ElevatedUsername;
sudoPassword = result.ElevatedPassword;
Session = result.ElevatedSession;
return;
}
else
{
log.Debug("User cancelled sudo dialog, cancelling action");
throw new CancelledException();
}
}
public class SudoElevationResult
{
public readonly string ElevatedUsername;
public readonly string ElevatedPassword;
public readonly Session ElevatedSession;
public readonly bool Result;
public SudoElevationResult(bool result, string user, string password, Session session)
{
Result = result;
ElevatedUsername = user;
ElevatedPassword = password;
ElevatedSession = session;
}
}
public static bool AllActionsFinishedOrCanceling(IXenConnection connection, IEnumerable<ActionBase> actionBases)
{
foreach (ActionBase action in actionBases)
{
IXenObject xo = (action.Pool as IXenObject) ?? (action.Host as IXenObject) ?? (action.VM as IXenObject) ?? (action.SR as IXenObject);
if (xo == null || xo.Connection != connection)
continue;
AsyncAction a = action as AsyncAction;
if (!action.IsCompleted && (a == null || !a.Cancelling))
{
return false;
}
}
return true;
}
protected static void BestEffort(ref Exception caught, bool expectDisruption, Action func)
{
try
{
func();
}
catch (Exception exn)
{
if (expectDisruption &&
exn is WebException && ((WebException)exn).Status == WebExceptionStatus.KeepAliveFailure) // ignore keep-alive failures if disruption is expected
{
return;
}
log.Error(exn, exn);
if (caught == null)
{
caught = exn;
}
}
}
protected void BestEffort(ref Exception caught, Action func)
{
BestEffort(ref caught, Connection != null && Connection.ExpectDisruption, func);
}
/// <summary>
/// Overload for use by actions, using any sudo credentials on the retry.
/// Try and run the delegate.
/// If it fails with a web exception or invalid session, try again.
/// Only retry 60 times.
/// </summary>
/// <param name="session"></param>
/// <param name="f"></param>
/// <param name="p"></param>
/// <returns></returns>
public object DoWithSessionRetry(ref Session session, Delegate f, params object[] p)
{
int retries = 60;
while (true)
{
try
{
object[] ps = new object[p.Length + 1];
ps[0] = session;
for (int i = 0; i < p.Length; i++)
{
ps[i + 1] = p[i];
}
try
{
return f.DynamicInvoke(ps);
}
catch (TargetInvocationException exn)
{
throw exn.InnerException;
}
}
catch (XmlRpcNullParameterException xmlExcept)
{
log.ErrorFormat("XmlRpcNullParameterException in DoWithSessionRetry, retry {0}", retries);
log.Error(xmlExcept, xmlExcept);
throw new Exception(Messages.INVALID_SESSION);
}
catch (WebException we)
{
log.ErrorFormat("WebException in DoWithSessionRetry, retry {0}", retries);
log.Error(we, we);
if (retries <= 0)
throw;
}
catch (Failure failure)
{
log.ErrorFormat("Failure in DoWithSessionRetry, retry {0}", retries);
log.Error(failure, failure);
if (retries <= 0)
throw;
if (failure.ErrorDescription.Count < 1 || failure.ErrorDescription[0] != XenAPI.Failure.SESSION_INVALID)
throw;
}
Session newSession;
try
{
// try to create a new TCP stream to use, as the other one has failed us
newSession = NewSession();
session = newSession;
}
catch (DisconnectionException e)
{
if (!Connection.ExpectDisruption)
{
//this was not expected, throw the d/c exception
throw e;
}
// We are expecting disruption on this connection. We need to wait for the hearbeat to recover.
// Though after 60 retries we will give up in the previous try catch block
}
catch
{
// do nothing
}
retries--;
Thread.Sleep(Connection.ExpectDisruption ? 500 : 100);
}
}
protected void AddCommonAPIMethodsToRoleCheck()
{
ApiMethodsToRoleCheck.AddRange(Role.CommonTaskApiList);
ApiMethodsToRoleCheck.AddRange(Role.CommonSessionApiList);
}
}
}