/* 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, 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; } } /// /// 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. /// protected RbacMethodList ApiMethodsToRoleCheck = new RbacMethodList(); public virtual RbacMethodList GetApiMethodsToRoleCheck { get { return ApiMethodsToRoleCheck; } } /// /// If the sudoUsername and sudoPassword fields are both not null, then the action will use these credentials when making new sessions. /// public string sudoUsername; /// /// If the sudoUsername and sudoPassword fields are both not null, then the action will use these credentials when making new sessions. /// public string sudoPassword; /// /// 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 /// /// public Session NewSession() { if (Connection == null) return null; if (String.IsNullOrEmpty(sudoPassword) || String.IsNullOrEmpty(sudoUsername)) return Connection.DuplicateSession(); return Connection.ElevatedSession(sudoUsername, sudoPassword); } /// /// 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 /// /// /// 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; } } /// /// 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. /// 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); } /// /// 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. /// public void RunExternal(Session session) { RunWorkerThread(session); if (Exception != null) throw Exception; } protected abstract void Run(); /// A session to use for this action: if null, construct a new session and sudo it if necessary 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); } /// /// Equivalent to PollToCompletion(0, 100). /// public void PollToCompletion() { PollToCompletion(0, 100); } /// /// 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. /// 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 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); } /// /// 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. /// /// /// /// /// 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); } } }