Software Defined Networking met PowerShell en de VMware NSX REST API – Deel 2

Deze blogserie bestaat uit meerdere delen, die in de komende maanden uitkomen. De volgende delen zijn op dit moment uitgebracht of gepland:

  1. Wat is de VMware NSX REST API
  2. Aanroepen van de VMware NSX REST API met PowerShell (deze blogpost)
  3. Met PowerShell aanmaken van een eenvoudig 3-tier netwerk in VMware NSX
  4. PowerNSX
  5. Van virtueel naar visueel: Genereer een netwerkdiagram met PowerNSX
  6. Kloon uw netwerk: van productie naar test met REST

Deel 2: Aanroepen van de VMware NSX REST API met PowerShell

Nu we in deel 1 de basisprincipes van de REST API hebben uitgelegd en gedemonstreerd, door gebruik te maken van een REST-client, gaan we in dit deel daadwerkelijk PowerShell gebruiken.

In PowerShell moeten we eerst dezelfde uitdagingen oplossen als met de REST-client:

  1. We moeten er voor zorgen dat PowerShell eventuele ongeldige certificaten negeert (uiteraard alleen in ons lab omgeving, toch?);
  2. Er moet een geldige gebruikersnaam met bijbehorend wachtwoord worden meegegeven;

Voor uitdaging 1 moeten we een truc uithalen, die gebaseerd is op het feit dat PowerShell werkt op basis van het Microsoft .NET Framework en dat je met de Add-Type gewoon een C#-class toe kan voegen aan PowerShell (credits naar Brian Scholer):

if (-not("SSLValidator" -as [Type]))
{
    Add-Type @"
        using System.Collections.Generic;
        using System.Net;
        using System.Net.Security;
        using System.Security.Cryptography.X509Certificates;

        public static class SSLValidator
        {
            private static Stack funcs = new Stack();

            private static bool OnValidateCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
            {
                return true;
            }

            public static void OverrideValidation()
            {
                funcs.Push(ServicePointManager.ServerCertificateValidationCallback);
                ServicePointManager.ServerCertificateValidationCallback = OnValidateCertificate;
            }

            public static void RestoreValidation()
            {
                if (funcs.Count > 0)
                {
                    ServicePointManager.ServerCertificateValidationCallback = funcs.Pop();
                }
            }
        }
"@
} 

Hierna kun je de controle op ongeldige certificaten (alleen voor deze sessie) uitzetten met het commando:

[SSLValidator]::OverrideValidation()

En weer aanzetten met:

[SSLValidator]::RestoreValidation()

De C#-programmacode achter ‘Add-Type’ is gedefinieerd als een zogenaamde ‘Here-String’. Een ‘Here-String’ gebruik je meestal bij variabelen die speciale karakters bevatten zoals regeleinden, enkele of dubbele acculades en andere tekens waar PowerShell mogelijk van in de war raakt. Een ‘Here-String’ begint met @” met op een nieuw regel de inhoud. De inhoud eindigt met “@ aan het begin van weer een nieuwe regel:

$x = @"
Ja, hier kan "echt" bijna 'alles' instaan,
zoals lege regels en andere speciale tekens zoals:
!@#$%^&*()"'/<> en ook @” en “@.
"@

De laatste regel mag niet beginnen met spaties, tabs of andere tekens. Deze constructie ga ik later ook gebruiken om XML-berichten in variabelen te zetten.

Uitdaging 2 is wel met PowerShell op te lossen, maar ook hierbij hebben we het .NET Framework nodig. Het is namelijk gebruikelijk om wachtwoorden niet plat in variabelen te zetten, maar gebruik te maken van het type SecureString. Het ophalen van een wachtwoord als SecureString kan eenvoudig met:

$password = Read-Host -Prompt  "Geef wachtwoord: " -AsSecureString

Maar als we toch ook een gebruikersnaam op moeten vragen dan kunnen we dat beter doen met Get-Credential:

$credentials = Get-Credential -UserName "admin" -Message "Geef de logingegevens van NSX Manager"

Om de credentials om te zetten naar de vorm gebruikersnaam:wachtwoord gebruiken we eerst de .NET Framework class PSCredential:

$nsxCredentials = New-Object System.Management.Automation.PSCredential $credentials.UserName, $credentials.Password

Deze class heeft namelijk een functie GetNetworkCredential, waarmee we de (niet beveiligde) waarde van het wachtwoord weer op kunnen halen. Om te voorkomen dat dit wachtwoord blijft ‘rondslingeren’ zetten we deze gelijk om naar Base64-formaat:

$authorizationValue = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($credentials.UserName + ":" + $nsxCredentials.GetNetworkCredential().password))

Het resultaat in $authorizationValue is dezelfde waarde die ik in deel 1 van deze serie zwart had gemaakt. Deze waarde wordt vervolgens opgeslagen in $headers:

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Basic $authorizationValue")

Ik voeg aan $headers ook gelijk expliciet het formaat toe wat ik als antwoord terug verwacht (Accept) en die ik zelf aanlever (Content-Type). Misschien overbodig (NSX Manager gebruikt alleen maar XML) maar wel zo duidelijk:

$headers.Add("Accept", "application/xml")
$headers.Add("Content-Type", "application/xml")

Waarna we de REST API kunnen aanroepen:

$baseURI = "https://nsxmanager"
$URI = $baseURI + "/api/2.0/vdn/scopes"
[SSLValidator]::OverrideValidation()
$result = Invoke-WebRequest -Method Get -Uri $URI -Headers $headers -ErrorAction Stop
[SSLValidator]::RestoreValidation()

De waarde $result is een HtmlWebResponseObject:

StatusCode        : 200
StatusDescription : OK
Content           : 
                    vdnscope-1VdnScope42163726-D410-D5B8-F1B7-E8DFFE81778E477df...
RawContent        : HTTP/1.1 200 OK
                    Cache-Control: private,no-cache
                    Expires: Thu, 01 Jan 1970 01:00:00 CET
                    Server: 
                    Set-Cookie: JSESSIONID=4673FC1A894554CC13A37A1B3255BE41; Path=/; Secure; HttpOnly
                    Date: Tue, 23 Feb...
Forms             : {}
Headers           : {[Cache-Control, private,no-cache], [Expires, Thu, 01 Jan 1970 01:00:00 CET], [Server, ], [Set-Cookie, JSESSIONID=4673FC1A894554CC13A37A1B3255BE41; Path=/; Secure; HttpOnly
                    ]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 1751 

Dus om te controleren of de aanroep gelukt is:

"De status is $($result.StatusCode), oftewel: $($result.StatusDescription)"

Geeft als resultaat:

De status is 200, oftewel: OK

Het antwoord wat door NSX Manager wordt teruggegeven kan direct worden omgezet naar XML:

[xml]$resultXML = $result.Content

En dit kan vervolgens gebruikt worden om bijvoorbeeld alle transport zones te tonen met extra informatie en welke clusters deze omvat:

foreach ($scope in $resultXML.vdnScopes.vdnScope)
{
    "Transport zone: $($scope.name)"
    "    Omschrijving      : $($scope.description)"
    "    Aantal switches   : $($scope.virtualWireCount)"
    "    Control plane mode: $($scope.controlPlaneMode)"
    ""
    [string]::Format("    {0,-20}{1,-20}", "Datacenter", "Cluster")
    "    ----------------------------------------"
    foreach ($cluster in $scope.clusters.cluster.cluster)
    {
        [string]::Format("    {0,-20}{1,-20}", $cluster.scope.name, $cluster.name)
    }
}

Wat bij ons het volgende overzicht oplevert:

Transport zone: MetisIT Transport Pool
    Omschrijving      : Onze transport pool
    Aantal switches   : 8
    Control plane mode: UNICAST_MODE

    Datacenter          Cluster             
    ----------------------------------------
    MetisITLab          Resources           
    MetisITLab          Management 

De volledige code, variabelen vooraan, inclusief foutafhandeling:

$baseURI = "https://nsxmanager"
try
{
    if (-not("SSLValidator" -as [Type]))
    {
        Add-Type @"
            using System.Collections.Generic;
            using System.Net;
            using System.Net.Security;
            using System.Security.Cryptography.X509Certificates;

            public static class SSLValidator
            {
                private static Stack funcs = new Stack();

                private static bool OnValidateCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
                {
                    return true;
                }

                public static void OverrideValidation()
                {
                    funcs.Push(ServicePointManager.ServerCertificateValidationCallback);
                    ServicePointManager.ServerCertificateValidationCallback = OnValidateCertificate;
                }

                public static void RestoreValidation()
                {
                    if (funcs.Count > 0)
                    {
                        ServicePointManager.ServerCertificateValidationCallback = funcs.Pop();
                    }
                }
            }
"@
    }
    $credentials = Get-Credential -UserName "admin" -Message "Geef de logingegevens van NSX Manager"
    $nsxCredentials = New-Object System.Management.Automation.PSCredential $credentials.UserName, $credentials.Password
    $authorizationValue = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($credentials.UserName + ":" + $nsxCredentials.GetNetworkCredential().password))
    $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add("Authorization", "Basic $authorizationValue")
    $headers.Add("Accept", "application/xml")
    $headers.Add("Content-Type", "application/xml")
    $URI = $baseURI + "/api/2.0/vdn/scopes"
    [SSLValidator]::OverrideValidation()
    $result = Invoke-WebRequest -Method Get -Uri $URI -Headers $headers -ErrorAction Stop
    [SSLValidator]::RestoreValidation()
    "De status is $($result.StatusCode), oftewel: $($result.StatusDescription)"
    [xml]$resultXML = $result.Content
    foreach ($scope in $resultXML.vdnScopes.vdnScope)
    {
        "Transport zone: $($scope.name)"
        "    Omschrijving      : $($scope.description)"
        "    Aantal switches   : $($scope.virtualWireCount)"
        "    Control plane mode: $($scope.controlPlaneMode)"
        ""
        [string]::Format("    {0,-20}{1,-20}", "Datacenter", "Cluster")
        "    ----------------------------------------"
        foreach ($cluster in $scope.clusters.cluster.cluster)
        {
            [string]::Format("    {0,-20}{1,-20}", $cluster.scope.name, $cluster.name)
        }
    }
}
catch
{
    "Er is iets fout gegaan:"
    $error[0]
}

In het volgende deel gaan we een eenvoudig 3-tier netwerk aanmaken.

10 maart 2016
Theo Hardendood