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...