Implémenter un snap-in PowerShell Core

Pour les besoins d’administration de nos solutions, nous avons, depuis plusieurs années, des composants Powershell.

Nous les avions regroupés dans un snap-in non publié sur Powershell Gallery, pour des raisons de sécurité “un peu courte” : suffisante pour l’exploitation OnPremise, mais clairement pas à proposer en libre service Smile

Et puis… ceci est arrivé sur Azure :

azure-cloud-shell

(si vous ne savez pas ce dont il s’agit)

Et nous nous sommes dit que d’avoir un système permettant, nous aussi de lancer une session d’administration à distnce sans n’avoir rien à installer, et directement dans le browser, ce serait top.

Créer un Snap-in PowershellCore

La première étape avant d’essayer de reproduire tout ça, c’est d’avoir un snap-in propre, débarrassé de nos vieilles API en mode SOAP et développé en .net standard, histoire de fonctionner aussi à partir d’un container Docker Nano Server ou même Linux.

Pour ça, rien de plus simple : il suffit de créer un nouveau projet dll en .net standard 2 (ne pas faire l’erreur de créer une dll .net core 2.0, sinon … eh bien en gros, ça ne marchera pas Smile with tongue out) :

creer-projet-powershellcore

D’y ajouter le nuget PowerShellStandard.Library. Il n’existe qu’en pre-release, alors pensez bien à cocher la case ou à ajouter le flag -IncludePrerelease pour être sûr de le trouver. Il vous faudra au minimum une version 5.1. J’ai utilisé la version 5.1.0-preview-01 dans ce projet.

Install-Package PowerShellStandard.Library -Version 5.1.0-preview-01

Implémenter quelques Cmdlet

Maintenant que le projet est créé, il reste à écrire le métier Smile Pour ce billet, je ne vais pas vous faire l’affront de coder nos composants, et nous allons donc faire une simple cmdlet qui récupère un Chuck Norris Fact,  et qui l’envoi dans la pipeline.

Powershell étant prévu pour être extensible de façon simple, écrire de nouvelles fonctionnalités et de nouvelles commandes est assez simple : il suffit de créer une classe, implémentant la classe de base System.Management.Automation.Cmdlet, de surcharger la méthode ProcessRecord (ou les méthodes BeginProcessing/EndProcessing) et de définir le nom de la commande en ajoutant un attribut CmdletAttribute.

Le code est assez simple, il n’y a qu’une subtilité : comme il s’agit d’une API retournant du JSON, nous avons fait l’effort d’implémenter la classe de de-sérialisation en utilisant le minimum de dépendances. Json.net est un nuget extrêmement utile, mais dans le cadre d’une dll d’extensibilité, il pourrait très vite devenir un cauchemar de type “dll hell”. Vous ne pouvez en effet pas être sûr qu’une autre Cmdlet n’utilisera pas une version non compatible.

Le code se résume à quelques lignes :

using System;
using System.Management.Automation;
using System.Net;
using System.Runtime.Serialization;


namespace MonSnapinPowershell
{


    [DataContract]
public class ChuckNorrisFact
{
[DataMember(Name = »category »)]
public string[] Categories { get; set; }


        [DataMember(Name = « id »)]
public string Id { get; set; }


        [DataMember(Name = « value »)]
public string Fact { get; set; }
}


     [Cmdlet(VerbsCommon.Get, « chucknorrisfact »)]
public class GetChuckNorrisFactCmdlet : Cmdlet
{
protected override void ProcessRecord()
{
using (var cli = new WebClient())
{
var s = cli.DownloadString(« 
https://api.chucknorris.io/jokes/random »);
                 var obj = JsonHelper.DeserializeObject<ChuckNorrisFact>(s);
WriteObject(obj);
}
}
}
}

internal static class JsonHelper
{
public static string SerializeObject<T>(T data)
{
using (MemoryStream ms = new MemoryStream())
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
ser.WriteObject(ms, data);
byte[] json = ms.ToArray();
ms.Close();
return Encoding.UTF8.GetString(json, 0, json.Length);
}
}
public static T DeserializeObject<T>(string json) where T:class, new()
{
T deserializedUser = default(T);
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
deserializedUser = ser.ReadObject(ms) as T;
ms.Close();
}
return deserializedUser;
}
}

 

Pour vérifier que votre Cmdlet fonctionne, il suffit de lance Powershell, d’importer votre dll et d’appeler votre Cmdlet :

powershell-test

Si vous n’avez jamais développé de snapin Powershell, Import-Module (ipmo) permet de charger un module et de rendre ses Cmdlets disponibles. Toutes les Cmdlets (et d’autres classes dont nous parlerons peut-être un autre jour) seront chargés de votre assembly et mis à disposition de l’utilisateur.

Créer le manifest

Plus qu’une étape pour avoir un snap-in digne de ce nom : créer un manifest décrivant le snap-in. Ce manifest est un fichier texte, écrit dans un format “Powershellien” et détaillant toutes les informations importantes. Il doit être dans le même dossier que votre dll et se trouver dans un fichier .psd1.

Pour notre exemple :

@{
Author = ‘Michael’
CompanyName = ‘Michael’


    ModuleVersion = ‘1.0’
GUID = ’24EA6A96-2EE5-49B1-9C8A-858E761F3BD6′


    Copyright = ‘(c) moi’
Description = ‘…’


    PowerShellVersion = ‘5.1’
CompatiblePSEditions = @(‘Desktop’, ‘Core’)


    NestedModules = @(‘MonSnapinPowershell.dll’)


    # infos pour Powershell Gallery
PrivateData = @{
PSData = @{
Tags = @(‘Samples’)
ProjectUri = ‘https://github.com/mcarbenay/powershell-samples

             IconUri =  »
}
}
}

Le seul point qui ne soit pas de la description dans ce fichier est la ligne :

NestedModules = @(‘MonSnapinPowershell.dll’)

qui définit les dlls à utiliser. Il existe d’autres façon de déclarer où se trouve votre code, mais c’est la solution qui semble la plus stable entre les différentes versions de Powershell.