powershell runspace - exchange

0

Czesc, toczę ostatnimi czasy walkę z serwerem exchange. Moje główne zadania z tym zwiazane to zarządzanie grupami. Niestety są to mail-enabled security groups - którymi jak wynika z dokumentacji Microsoftu, mozna zarządzać jedynie przez powershella (remote na serwerze) - tutaj odpada zarówno GraphApi jak i EWS (exchange-web-services) api.
Teoria jest prosta - wysyłam komednę powershellową na serwer i wykonuja sie CRUDy na grupach/userach.

Problem mam z runspacem, kod moze wygladac troche dziwnie, ale działa "najlepiej" z dotychczasowych prób.
Ogólnie to stworzyłem klasa abstraykcjna PowershellCommand:

 public abstract class PowershellCommand<T> :IDisposable where T:class 
    {
        private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();

        protected abstract Dictionary<string, object> Parameters { get; set; }
        protected abstract string Command { get; }

        private static Runspace _runspace = null;
        public PowershellCommand(IPowershellCommandEnvironment powershellCommandEnvironment)
        {
            _runspace = powershellCommandEnvironment.GetRunspace();
            Parameters = new Dictionary<string, object>();
            
        }
      
        public T Execute(int attemp=0)
        {
            if (attemp > 2)
                return null;

            try
            {
                using (var powershell = System.Management.Automation.PowerShell.Create())
                {
                    powershell.Runspace = _runspace;
                        
                    powershell.AddCommand(Command);
                    foreach (var param in Parameters)
                    {
                        powershell.AddParameter(param.Key, param.Value);
                    }
                    Collection<PSObject> result = powershell.Invoke();
                    powershell.Runspace.Dispose();
                    return Map(result);
                }
            }
            catch(Exception ex)
            {
                string logMessage = $"Command ${Command} not suceeded.{Environment.NewLine} {ex.Message} {ex.InnerException?.Message}";
                _logger.Log(LogLevel.Error, logMessage);
                int sleep = 5000;
                if (ex.Message.Contains("Please wait for"))
                {
                    sleep = 10000;
                    _logger.Log(LogLevel.Error, "waiting 10000 seconds (powershell command time exceeded");
                 
                }
                Thread.Sleep(sleep);
                return Execute(attemp+1);
            }
        }

        protected abstract T Map(IEnumerable<PSObject> psobj);

        protected bool IsDistinguishedName(string id)
          => id.Contains("OU=") || id.Contains("CN=") || id.Contains("DC=");

        public void Dispose()
        {
            _runspace.Close();
            _runspace.Dispose();
        }
    }

i kolejne powershellowe commandy go interpretuja, np:
Get-Group powershllCommand

public class GetGroupPowershellCommand : PowershellCommand<GetGroupResponse>
    {

        public GetGroupPowershellCommand(IPowershellCommandEnvironment pce, string groupId):base(pce)
        {
            if(IsDistinguishedName(groupId))
            {
                string filter = $"DistinguishedName -eq '{groupId}'";
                Parameters.Add("Filter", filter);
            }
            else
                Parameters.Add("Identity", groupId);
        }

      

        protected override string Command => "Get-Group";

        protected override Dictionary<string, object> Parameters { get; set; }

        protected override GetGroupResponse Map(IEnumerable<PSObject> psobjs)
        {
            if (psobjs == null || !psobjs.Any())
                return null;

            var psobj = psobjs.First();
            DateTime.TryParse(Helper.GetValue(psobj, "WhenChangedUTC"), out var modifDate);
            
            var owners = Helper.GetMembers(psobj, "ManagedBy");
            var members = Helper.GetMembers(psobj, "Members");
            var result = new GetGroupResponse
            {
                Description = Helper.GetValue(psobj, "Notes"),
                DisplayName = Helper.GetValue(psobj, "DisplayName"),
                Mail = Helper.GetValue(psobj, "WindowsEmailAddress"),
                GroupId = Helper.GetValue(psobj, "Identity"),
                LastModificationDate = modifDate,
                Owner = owners.FirstOrDefault(),
                CoOwners = owners.Skip(1)?.ToList(),
            };

            return result;
        }

       
    }

stworzyłem tez klasę PowershellCommandEnvironment - która dostarcza połączenie i runspace (z którym jest kłopot)
Serwer pozwala na utworzenie tylko i wyłącznie 3 runspaceów - w nich mozna wykonywac konkretne komendy powershella.
Byłem zmuszony zarejestrować tą klasę jako singletona w projekcie WebApi - tak aby utworzyc tylko i wyłącznie 1 rusnapce.

public class PowershellCommandEnvironment : IPowershellCommandEnvironment, IDisposable
    {
        readonly (string user, string password) powerShellAuth;
        private static Runspace _runspace = null;
        WSManConnectionInfo _connectionInfo;
        

        public PowershellCommandEnvironment()
        {
            powerShellAuth.user = CloudConfigurationManager.GetSetting("ExchangePowerShellUser");
            powerShellAuth.password = CloudConfigurationManager.GetSetting("ExchangePowerShellPassword");

            SecureString secureStrin = new NetworkCredential("", powerShellAuth.password).SecurePassword;
            var creds = new PSCredential(powerShellAuth.user, secureStrin);
            _connectionInfo = new WSManConnectionInfo(new Uri("https://outlook.office365.com/powershell-liveid/"), "http://schemas.microsoft.com/powershell/Microsoft.Exchange", creds);
            _connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
            _connectionInfo.MaximumConnectionRedirectionCount = 2;
            _runspace = RunspaceFactory.CreateRunspace(_connectionInfo);
            _runspace.StateChanged += _runspace_StateChanged;


        }

        private void _runspace_StateChanged(object sender, RunspaceStateEventArgs e)
        {
            var state = _runspace.RunspaceStateInfo.State;
            switch (state)
            {
                case RunspaceState.Broken:
                    _runspace.Close();
                    _runspace.Dispose();
                    _runspace = RunspaceFactory.CreateRunspace(_connectionInfo);
                    break;

                case RunspaceState.Opening:
                    Thread.Sleep(500);
                    break;

                case RunspaceState.BeforeOpen:
                    _runspace.Open();
                    break;
            }
        }

        public Runspace GetRunspace()
        {
          
                while (_runspace.RunspaceStateInfo.State != RunspaceState.Opened)
                {

                    OpenRunSpaceTimeExceededAttempt(0);
                    Thread.Sleep(100);
                }
                return _runspace;
            
          
        }

        private void OpenRunSpaceTimeExceededAttempt(int attempt)
        {
            if (attempt > 2)
                return;

      
            try
            {
                var state = _runspace?.RunspaceStateInfo.State;
                if (_runspace == null || state == RunspaceState.Closed)
                {
                    _runspace = RunspaceFactory.CreateRunspace(_connectionInfo);
                    _runspace.Open();
                }


                if (state == RunspaceState.BeforeOpen)
                    _runspace.Open();


       
                if (!(state == RunspaceState.Opened))
                {
                    OpenRunSpaceTimeExceededAttempt(attempt+1);
                }
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("Please wait for"))
                {
                    System.Threading.Thread.Sleep(10000);
                }
                OpenRunSpaceTimeExceededAttempt(attempt + 1);
            }
        }

     

        public void Dispose()
        {
            _runspace.Dispose();
        }
    }

Kłopoty jakie napotykam - state rusnapace'a czasem jest "broken" - wtedy próbuje zdisposować runspeace'a i utworzyc go na nowo - z kolei czasem przekraczam 3 dozwolone runspace - jak by te mimo tego ze je disposuje/zamykam polaczenie wisiały gdzieś na serwerze - stad tez robię Thread.Sleepy w takich wypadkach

Drugi kłopot to "zbyt szybkie" wykonywanie commandow - serwer czasem zwraca exceptiony ze trzeba swoje odczekać (;)) - choć jest to wydaje mi się powiązane z problemem z runespacem

Trzeci kłopot - to czasem serwer exchange przy próbie wykonania command'a zwraca info ze wysłałem zlego xmla. Rzadko się to zdarza, ale jednak - nie mam bladego pojęcia dlaczego, być może znów problem z runespacem który się "zawiesił"?

Problemy się pojawiają gdy tych commandów zacznę wykonywać zbyt wiele naraz - w stylu utwórz grupę, dodaj userów do niej, pobierz grupy - pobierz ich userów
Pracuję nad tym żeby ograniczyć liczbę zapytań i szarpać więcej danych naraz, może to troche ograniczy problemy z runspecami

Ogólnie to jeśli ktoś troszkę ogarnia temat - to prosiłbym o poradę - czy w ogóle dobrze robię z tymi runspacemi ?

Najprostsze przykłady z neta na nie wiele się zdają, dlatego tak przekombinowałem trochę z kodem...

0

wygląda na to, że rozwiązałem problem dowalając locka :)
zrobiłem test który w parallel for tworzył, nastepnie pobierał,edytował i na nowo pobierał grupy z exchange (taki test wszystkich operacji CRUD) po 100 razy (= > 400 komend przesyłanych w powershellu) - test trwał wypalenie papierosa i się powiódł :)
problemem było, że czasem przy próbie otwarcia runspace ten już zaczynał być otwierany przez inny wątek (tutaj jak się szybko wyklika w UI a do tego serwer exchange'a zamuli się tak samo działo)

 private void OpenRunSpaceTimeExceededAttempt(int attempt)
        {
            lock(SyncObject)
            {
                Thread.Sleep(500 * attempt);
                if (attempt > 5)
                    return;


                try
                {
                    var state = _runspace?.RunspaceStateInfo.State;
                    if (state.HasValue)
                        _logger.Info("state:", state.Value);

                    if (_runspace == null || state == RunspaceState.Closed)
                    {
                        _runspace = RunspaceFactory.CreateRunspace(_connectionInfo);
                        _runspace.Open();
                    }

                    if (state == RunspaceState.Broken)
                    {
                        _logger.Info("runspace state broken");
                        ReopenRunspace();
                    }



                    if (state == RunspaceState.BeforeOpen)
                        _runspace.Open();

                    if (state == RunspaceState.Opening)
                        Thread.Sleep(500);



                    if (!(state == RunspaceState.Opened))
                    {
                        OpenRunSpaceTimeExceededAttempt(attempt + 1);
                    }
                }
                catch (Exception ex)
                {
                    if (ex.Message.Contains("Please wait for"))
                    {
                        _logger.Error(ex.Message);
                        System.Threading.Thread.Sleep(10000);
                    }
                    OpenRunSpaceTimeExceededAttempt(attempt + 1);
                }
            }
       
        }

ale jeśli ktoś by kiedyś tu zaglądnął i miał jakieś sugesetie dotyczące handlowania z runspecami to chętnie odczytam ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1