mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-25 06:16:37 +01:00
cb7dcc81f4
- Set the pool details in the action to allow proper filtering by location on the Events page - Also, when possible, set the Host property: when setting the SR in the action, we also set the Host to SR.Home Signed-off-by: Mihaela Stoica <mihaela.stoica@citrix.com>
492 lines
18 KiB
C#
492 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;
|
|
Pool = Helpers.GetPoolOfOne(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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|