grimboite/articles/dev/2013-07-16-runspace-powersh...

5.1 KiB

Title Date Slug Tags
Exécuter du code Powershell au travers d'un service WCF 2013-07-16 powershell-code-execution-through-wcf-service powershell, wcf, dotnet, code, runspace

PowerShell permet d'écrire et d'exécuter des scripts avec un modèle objet plutôt complet, et se basant sur le framework .Net. En gros, cela remplace (avantageusement) les scripts .bat, tout en se rapprochant de ce que les shells Unix permettent de faire depuis 20 ans.

Les dépendances PowerShell étant spécifiques à la machine hôte (et aux librairies qui y sont installées, forcément), impossible d'accéder à certaines fonctionnalités depuis n'importe quel poste client. C'est notamment le cas pour tout ce qui concerne les actions liées à l'Active Directory. Pour bypasser ce mécanisme, je fais en sorte que ce soit la machine sur laquelle les outils d'administration AD sont installés qui exécute les scripts PowerShell. J'accède ensuite au lancement et à la gestion de ces scripts au travers d'un service WCF déployé sur cette même machine, au travers d'IIS.

La difficulté principale est donc d'écrire les scripts PowerShell, de leur passer des paramètres et de récupérer le résultat.

Interface et client WCF

Le service WCF ne présente pas vraiment de difficulté, puisqu'il ne fait qu'exposer les paramètres et les différentes méthodes au travers du protocole existant. Une interface de management ([ServiceContract]) expose toutes les méthodes disponibles. Celles-ci sont taggées par un attribut [OperationContract].

[ServiceContract]
public interface IManagement
{
	[OperationContract]
	Result CreateAdAccount(int uid, string samaccountname, int? regimental, string displayname, string name, string firstname, string service, string defaultPassword);

	[OperationContract]
	Result CreateHomeFolder(string samAccountName);

	[OperationContract]
	Result CreateEmailAddress(string samAccountName, string emailAddress);

	[OperationContract]
	Result GetADUser(string samAccountName);
}

Côté client, on appellera ces différentes méthodes grâce à une instance d'un client WCF (après l'avoir ajouté dans les dépendances du projet):

var client = new ADServiceReference.ManagementClient();

client.GetADUser('james_bond');

/* ... */

Script PowerShell

On construit ensuite un script get-aduser.ps1 que l'on place dans le répertoire Scripts:


<#
Get-ADUser et ses propriétés, sur base du sam account name.
#>

param(
	[Parameter(Mandatory = $true)] [string] $SAMAccount
)

Import-Module ActiveDirectory

$user = Get-ADUser -Identity $SAMAccount -Properties *
$MaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.TotalDays
$passwordExpirationDate = $user.PassWordLastSet + $MaxPasswordAge

Write-Output "DisplayName:", $user.DisplayName
Write-Output "Password last set: ", $user.PasswordLastSet
Write-Output "Password Expiration: ", $passwordExpirationDate
Write-Output ""
Write-Output "Member of: ", $user.MemberOf
Write-Output ""
Write-Output "when changed:", $user.whenChanged
Write-Output "When created:", $user.whenCreated

Runspace .Net

Retour au code .Net, dans l'implémentation des méthodes du service WCF: on construit un runspace en lui passant les paramètres qui collent avec ce que le script attend. La première étape est de forcer le chargement des modules et des snapins PowerShell. On ouvre ensuite le pipeline pour y enquiller les paramètres, avant de lancer l'exécution proprement dite:

using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.ServiceModel.Activation;

public class Management : IManagement
{
	public Result GetADUser(string samAccountName)
	{
		string SCRIPT_PATH = @"Scripts/get-aduser.ps1";

		StringBuilder stringBuilder = new StringBuilder();

		try
		{
			InitialSessionState initial = InitialSessionState.CreateDefault();

			PSSnapInException snapinException = new PSSnapInException();

			initial.ImportPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapinException);
			initial.ImportPSModule(new string[] { "ActiveDirectory" });

			using (Runspace runspace = RunspaceFactory.CreateRunspace(initial))
			{
				runspace.ApartmentState = System.Threading.ApartmentState.STA;
				runspace.ThreadOptions = PSThreadOptions.UseNewThread;

				runspace.Open();

				Pipeline pipeline = runspace.CreatePipeline();

				Command cmd = new Command(System.Web.HttpContext.Current.Server.MapPath(SCRIPT_PATH));

				cmd.Parameters.Add(new CommandParameter("SAMAccount", samAccountName));

				pipeline.Commands.Add(cmd);

				var result = pipeline.Invoke();

				foreach (var res in result)
				{
					stringBuilder.AppendLine(res.ToString());
				}

				ManageErrors(pipeline);
			}
		}
		catch (Exception ex)
		{
			stringBuilder.Append(ex.ToString());

			return new Result() { Status = "NOK", Content = stringBuilder.ToString() };
		}

		return new Result() { Status = "OK", Content = stringBuilder.ToString() };
	}
}