Tuesday, July 6, 2021

PowerShell: Practical Pester Unit Tests

The previous post, PowerShell: Safely Converting Strings to by Value Types, contained code to test the conversion of strings to value types (int, bool, double, and DateTime). The PowerShell code is contained in file, Convert.ps1, so Convert.Tests.ps1 was added to include the Pester unit tests.

Pester is fully integrated with PowerShell and Visual Studio Code when the PowerShell Extension is installed. Right clicking on Convert.Tests.ps1 in Visual Studio Code's File Explorer displays a context menu which includes menu items Debug Pester Tests and Run Pester Tests:


The Pester code to test Covert.ps1 is as follows where each instance of the Pester Should command is highlighted in boldface:

Set-StrictMode -Version 3.0

Describe 'class Convert' {
  BeforeAll {
    . "$PSScriptRoot\Convert.ps1"
  }

  Context 'method GetDateTime' {
    BeforeAll {
      # O and o format as string in a manner that is parsible
      [string] $dateTimeFormat = 'o'
    }
      
    It 'GetDateTime parsable date' {
      [string] $currentDateTime = (Get-Date).ToUniversalTime().ToString($dateTimeFormat)

      [Nullable[DateTime]] $result = [Covert]::GetDateTime($currentDateTime)

      $result | 
        Should -Not -Be $null -Because "$currentDateTime is a parsable DateTime"
      $result.ToUniversalTime().ToString($dateTimeFormat) | 
        Should -Be $currentDateTime -Because "$currentDateTime is the same at value returned by GetDateTime"
    }

    It 'GetDateTime parsable DateTime' {
      [datetime[]] $originalValues = 
        [DateTime]::MinValue, [DateTime]::MaxValue,
        [DateTime]::new(1066, 10, 16), [DateTime]::new(1944, 6, 6, 10, 30, 30),
        [DateTime]::new(10, 10, 11, 9, 9, 9), [DateTime]::new(1944, 6, 6, 23, 59, 59), 
        [DateTime]::new(10, 10, 11, 0, 0, 0)

      foreach ($originalValue in $originalValues) {
        [Nullable[datetime]] $result = [Covert]::GetDateTime($originalvalue.ToString($dateTimeFormat))

        $result | 
          Should -Not -Be $null -Because "$originalvalue is a parsable DateTime"
        $result | 
          Should -Be $originalvalue -Because "$originalvalue is the same at value returned by GetDateTime"
      }
    }

    It 'GetDateTime unparsable date $null' {
      [Nullable[DateTime]] $result = [Covert]::GetDateTime($null)

      $result | 
        Should -Be $null -Because '$null is not a parsable DateTime'
    }

    It "GetDateTime unparsable date" {
      [string] $unparsableDate = 'abc'      
      [Nullable[DateTime]] $result = [Covert]::GetDateTime($unparsableDate)

      $result | 
        Should -Be $null -Because "$currentDateTime is not a parsable DateTime"
    }
  }

  Context 'method GetBool' {
    It 'GetBool parsable bool' {
      [string[]] $trueTextValues = 'True', 'TRUE', 'true', 'TrUe'

      foreach ($trueTextValue in $trueTextValues) {
        [Nullable[bool]] $result = [Covert]::GetBool($trueTextValue)

        $result | 
          Should -Not -Be $null -Because "$trueTextValue is a parsable bool"
        $result | 
          Should -BeTrue -Because "$trueTextValue is the same at value returned by GetBool"
      }

      [string[]] $falseTextValues = 'False', 'FALSE', 'false', 'FaLsE'

      foreach ($falseTextValue in $falseTextValues) {
        [Nullable[bool]] $result = [Covert]::GetBool($falseTextValue)

        $result | 
          Should -Not -Be $null -Because "$falseTextValue is a parsable bool"
        $result | 
          Should -BeFalse -Because "$falseTextValue is the same at value returned by GetBool"
      }
    }

    It 'GetBool unparsable bool $null' {
      [Nullable[bool]] $result = [Covert]::GetBool($null)

      $result | 
        Should -Be $null -Because '$null is not a parsable bool'
    }

    It "GetBool unparsable bool" {
      [string] $unparsableBool = 'abc'      
      [Nullable[bool]] $result = [Covert]::GetBool($unparsableDate)

      $result | 
        Should -Be $null -Because "$unparsableBool is not a parsable bool"
    }
  }  
  
  Context 'method GetInt' {
    It 'GetInt parsable int' {
      [int[]] $originalValues = [int]::MaxValue, [int]::MinValue, 0, 123, 456, 
      [short]::MinValue, [short]::MaxValue

      foreach ($originalValue in $originalValues) {
        [Nullable[int]] $result = [Covert]::GetInt($originalvalue.ToString())

        $result | 
          Should -Not -Be $null -Because "$originalvalue is a parsable int"
        $result | 
          Should -Be $originalvalue -Because "$originalvalue is the same at value returned by GetInt"
      }
    }

    It 'GetInt unparsable int $null' {
      [Nullable[int]] $result = [Covert]::GetInt($null)

      $result | 
        Should -Be $null -Because '$null is not a parsable int'
    }

    It "GetInt unparsable int" {
      [string] $unparsableInt = 'abc'      
      [Nullable[int]] $result = [Covert]::GetInt($unparsableDate)

      $result | 
        Should -Be $null -Because "$unparsableInt is not a parsable int"
    }
  }

  Context 'method GetDouble' {
    It 'GetDouble parsable double' {
      [double[]] $originalValues = 
            [double]::MaxValue, [double]::MinValue, 0, 123.33, 456.55, 
            [float]::MinValue, [float]::MaxValue, 
            [decimal]::MinValue, [decimal]::MaxValue

      foreach ($originalValue in $originalValues) {
        [Nullable[double]] $result = [Covert]::GetDouble($originalvalue.ToString())

        $result | 
          Should -Not -Be $null -Because "$originalvalue is a parsable double"
        $result | 
          Should -Be $originalvalue -Because "$originalvalue is the same at value returned by GetDouble"
      }
    }

    It 'GetDouble unparsable double $null' {
      [Nullable[double]] $result = [Covert]::GetDouble($null)

      $result | 
        Should -Be $null -Because '$null is not a parsable double'
    }

    It "GetDouble unparsable double" {
      [string] $unparsableDouble = 'abc'      
      [Nullable[double]] $result = [Covert]::GetDouble($unparsableDate)

      $result | 
        Should -Be $null -Because "$unparsableDouble is not a parsable double"
    }
  }    
}

The Covert.ps1 code was include in text form in the post, PowerShell: Safely Converting Strings to by Value Types, but is included follows:





Monday, July 5, 2021

PowerShell: Safely Converting Strings to by Value Types

Converting strings to value types like int, char, double, bool, DateTime, TimeSpan, and Guid without throwing an exception when invalid data is encountered requires some finesse.  One approach would be to return a $null of the conversion from string to value type fails. In a previous post, PowerShell: Nullable Types, nullable PowerShell types were introduced which facilitates returning a $null when a conversion from string to value type fails. Also in a previous post, the ref keyword was introduced (PowerShell: Passing Values by Reference (the ref Keyword)) which allowed PowerShell to take advantage of .NET's TryParse methods.

The PowerShell class below, Converts, demonstrates how to covert a string to type int, double, bool, and DateTime without generating an error/exception (using nullable types and the ref keyword):

class Convert {
  static [Nullable[DateTime]] GetDateTime([string] $candidate) {
    # Trying to pass in $result=$null causes TryParse to fail if 
    # $candidate is invalid
    Nullable[DateTime]] $result = Get-Date
    [bool] $valid = [DateTime]::TryParse( 
                            $candidate, 
                            [ref]$result)

    if (!($valid)) {
      $result = $null
    }

      return $result                
  }

  static [Nullable[int]] GetInt([string] $candidate) {
    [Nullable[int]] $result = $null
    [bool] $valid = [int]::TryParse( 
              $candidate, 
              [ref]$result)

    if (!($valid)) {
      $result = $null
    }

    return $result        
  }
  
  static [Nullable[bool]] GetBool([string] $candidate) {
    [Nullable[bool]] $result = $null
    [bool] $valid = [bool]::TryParse( 
              $candidate, 
              [ref]$result)

    if (!($valid)) {
      $result = $null
    }

    return $result        
  }
  
  static [Nullable[double]] GetDouble([string] $candidate) {
    [Nullable[double]] $result = $null
    [bool] $valid = [double]::TryParse( 
              $candidate, 
              [ref]$result)

    if (!($valid)) {
      $result = $null
    }

    return $result        
  }  
}

Code to test the above class is as follows:

[string] $badCandidate = 'abc'
[string] $goodCandidate = '12/25/2021 08:09:11'
[Nullable[DateTime]] $dateTimeResult = [Covert]::GetDateTime($badCandidate)

if ($null -ne $dateTimeResult) {
  throw "$badCandidate should result in an invalid DateTime"
}

$dateTimeResult = [Covert]::GetDateTime($null)

if ($null -ne $dateTimeResult) {
  throw "`$null should result in an invalid DateTime"
}

$dateTimeResult = [Covert]::GetDateTime($goodCandidate)
if ($null -eq $dateTimeResult) {
  throw "$goodCandidate should result in a valid DateTime"
}

$goodCandidate = '123'
[Nullable[int]] $intResult = [Covert]::GetInt($badCandidate)

if ($null -ne $intResult) {
  throw "$badCandidate should result in an invalid int"
}

$intResult = [Covert]::GetInt($null)

if ($null -ne $intResult) {
  throw "`$null should result in an invalid int"
}

$intResult = [Covert]::GetInt($goodCandidate)
if ($null -eq $intResult) {
  throw "$goodCandidate should result in a valid int"
}

$goodCandidate = 'False'
[Nullable[bool]] $boolResult = [Covert]::GetBool($badCandidate)

if ($null -ne $boolResult) {
  throw "$badCandidate should result in an invalid bool"
}

$boolResult = [Covert]::GetBool($null)

if ($null -ne $boolResult) {
  throw "`$null should result in an invalid bool"
}

$boolResult = [Covert]::GetBool($goodCandidate)
if ($null -eq $boolResult) {
  throw "$goodCandidate should result in a valid bool"
}

$goodCandidate = '123.123'
[Nullable[double]] $doubleResult = [Covert]::GetDouble($badCandidate)

if ($null -ne $doubleResult) {
  throw "$badCandidate should result in an invalid double"
}

$doubleResult = [Covert]::GetDouble($null)

if ($null -ne $doubleResult) {
  throw "`$null should result in an invalid double"
}

$doubleResult = [Covert]::GetDouble($goodCandidate)
if ($null -eq $doubleResult) {
  throw "$goodCandidate should result in a valid double"
}

Sunday, July 4, 2021

PowerShell: Passing Values by Reference (the ref Keyword)

In PowerShell parameters passed to functions and class method are by value meaning the value of the underlying reference cannot be change when the function or method is invoked. There are functions and methods that change the underlying reference using the C# ref or out keyword. .NET's TryParse methods, for example, use out parameters:

public static bool TryParse (string? s, out int result);
public static bool TryParse (string? s, out double result);
public static bool TryParse (string? s, out DateTime result);

PowerShell's ref keyword can be used pass a value to TryParse's result parameter which is decorated by out as is shown in boldface below:

[string] $candidate = 'abc'
[Nullable[int]] $result = $null
[bool] $isValid = [int]::TryParse($candidate, [ref] $result)

Write-Host "Value: $result, Is valid: $isValid, Is null: $($null -eq $result)"

$candidate = '123'
$isValid = [int]::TryParse($candidate, [ref] $result)
Write-Host "Value: $result, Is valid: $isValid, Is null: $($null -eq $result)"

PowerShell's ref keyword works for both the C# ref and out parameters.

The code sample above worked when TryParse was invoked for the int data type. Attempting the same code for the DateTime type is as follows and this case actually fails:

[string] $candidate = 'abc'
[Nullable[DateTime]] $result = $null
[bool] $isValid = [DateTime]::TryParse($candidate, [ref] $result)

Write-Host "Value: $result, Is valid: $isValid, Is null: $($null -eq $result)"

The output from invoking the code above is as follows:


As it turns out, in order for [DateTime]::TryParse to work with invalid parse strings, the value passed to the second parameter must be assigned to valid value of type DateTime as is shown below were $result is assigned the value of Get-Date as is shown in boldface:

[string] $candidate = 'abc'
[Nullable[DateTime]] $result = Get-Date
[bool] $isValid = [DateTime]::TryParse($candidate, [ref] $result)

if (!($isValid)) {
    $result = $null # $result was not set by TryParse
}

Write-Host "Value: $result, Is valid: $isValid, Is null: $($null -eq $result)"
$candidate = '12/25/2021 08:09:11'
$result = Get-Date
$isValid = [DateTime]::TryParse($candidate, [ref] $result)

if (!($isValid)) {
    $result = $null # $result was not set by TryParse
}

Write-Host "Value: $result, Is valid: $isValid, Is null: $($null -eq $result)"

The above code executes without an error.

Saturday, July 3, 2021

PowerShell: Nullable Types

When a variable or parameter is assigned a value of $null it is a convenient way to indicate that the value has not yet been assigned. For by value types such as int, float, char, Guid, DateTime, and TimeSpan the $null assignment has come caveats. This post will introduce PowerShell's Nullable types but first will show why Nullable types are needed.

PowerShell does not generate a warning or error when $null is assigned to an int, double or char variable. 

[int] $anyInt = $null
[double] $anyDouble = $null
[char] $anyChar = $null

Write-Host "AnyInt Value = $anyInt, Is `$null = $($anyInt -eq $null)" 
Write-Host "AnyDouble Value = $anyDouble, Is `$null = $($anyDouble -eq $null)" 
Write-Host "AnyChar Value = $anyChar, Is `$null = $($anyChar -eq $null), Integer Value = $([int]$anyChar)" 

The output of the above Write-Host cmdlets show that $null assignment results in a value of zero being assigned to each of the variables rather than a value of $null:


Assigning $null to variables of type Guid, DateTime, and TimeSpan is shown below:

[Guid] $anyGuid = $null
[DateTime] $anyDateTime = $null
[TimeSpan] $anyTimeSpan = $null

PowerShell displays a warning indicating a $null assignment is not valid for the above code:


Each of the by value data types (int, double, char, Guid, DateTime, and TimeSpan) can be assigned as nullable as shown below using the aptly named Nullable keyword:

[Nullable[int]] $anyInt = $null
[Nullable[double]] $anyDouble = $null
[Nullable[char]] $anyChar = $null
[Nullable[Guid]] $anyGuid = $null
[Nullable[DateTime]] $anyDateTime = $null
[Nullable[TimeSpan]] $anyTimeSpan = $null

Write-Host "AnyInt Value = $anyInt, Is `$null = $($anyInt -eq $null)" 
Write-Host "AnyDouble Value = $anyDouble, Is `$null = $($anyDouble -eq $null)" 
Write-Host "AnyChar Value = $anyChar, Is `$null = $($anyChar -eq $null)" 
Write-Host "AnyGuid Value = $anyGuid, Is `$null = $($anyGuid -eq $null)" 
Write-Host "AnyDateTime Value = $anyDatetime, Is `$null = $($anyDatetime -eq $null)" 
Write-Host "AnyTimeSpan Value = $anyTimeSpan, Is `$null = $($anyTimeSpan -eq $null)" 

When the above Write-Host cmdlets are invoked the value of $null is recognized as being assigned to the variables:







Sunday, May 16, 2021

Azure/C#: Interview Questions (Restarting a Virtual Machine and Changing the Configuration)

Overview

As part of the interview process for a position, I was asked to write the following code samples 

  1. Write a C# program for restarting a VM
  2. Write a C# program for reading a specific node from XML
  3. Write a C# program for configurating an Azure VM

Write a C# program for Reading a Specific Node from XML

The obvious solution to such a piece of code is XPATH in this case following the theme of managing Azure Virtual Machines.

The boldface code in the following class selects a specific node:

using System.Collections.Generic;
using System.Xml;

namespace AzureVMChallenge
{  
  class VirtualMachineManager
  {
    public const string VirtualMachineElement = 
                          "VirtualMachine";

    public const string ResourceGroupNameElement = 
                          "ResourceGroupName";

    public const string VirtualMachineNameElement = 
                          "VirtualMachineName";


    /* Filename XML format (too simple):
<VirtualMachine>
  <ResourceGroupName>resource group name</ResourceGroupName>
  <VirtualMachineName>virtual machine name</VirtualMachineName>
</VirtualMachine>
    */
    public static VirtualMachineIdentity 
                    GetVirtualMachine(string filename) 
    {
      var document = new XmlDocument();

      document.Load(filename);

      var virtualMachine = document.SelectSingleNode(
                  VirtualMachineElement);

      return new VirtualMachineIdentity(
        virtualMachine.SelectSingleNode(
          ResourceGroupNameElement).InnerText, 
        virtualMachine.SelectSingleNode(
          VirtualMachineNameElement).InnerText);
    }
  }
}

Write a C# program for Reading a Multiple Nodes from XML

A more interesting example is an XPATH query that select multiple nodes based off of specific attribute values

The code in boldface below selects multiple nodes:

using System.Collections.Generic;
using System.Xml;

namespace AzureVMChallenge
{  
  class VirtualMachineManager
  {
    /* Filename XML format (more interesting XPATH):
      <VirtualMachines>
        <VirtualMachine Action="Restart" IsActive="False">
          <ResourceGroupName>AzureChellenge</ResourceGroupName>
          <VirtualMachineName>AzureChellengeVM09</VirtualMachineName>
        </VirtualMachine>
        <VirtualMachine Action="Stop" IsActive="False">
          <ResourceGroupName>AzureChellenge</ResourceGroupName>
          <VirtualMachineName>AzureChellengeVM01</VirtualMachineName>
        </VirtualMachine>
        <VirtualMachine Action="Start" IsActive="True">
          <ResourceGroupName>AzureChellenge</ResourceGroupName>
          <VirtualMachineName>AzureChellengeVM01</VirtualMachineName>
        </VirtualMachine>
        <VirtualMachine Action="Restart" IsActive="False">
          <ResourceGroupName>AzureChellenge</ResourceGroupName>
          <VirtualMachineName>AzureChellengeVM08</VirtualMachineName>
        </VirtualMachine>
        <VirtualMachine Action="Restart" IsActive="True">
          <ResourceGroupName>AzureChellenge</ResourceGroupName>
          <VirtualMachineName>AzureChellengeVM00</VirtualMachineName>
        </VirtualMachine>
      </VirtualMachines>
     */
    public static IEnumerable<VirtualMachineIdentity> 
                    GetVirtualMachines(string filename) 
    {
      var document = new XmlDocument();

      document.Load(filename);

      var virtualMachinesNodes = 
        document.SelectNodes(
          "/VirtualMachines/VirtualMachine[@Action='Restart' and " +            "@IsActive='True']");
      var virtualMachines = new List<VirtualMachineIdentity>();

      foreach (XmlNode virtualMachinesNode in virtualMachinesNodes) {
        virtualMachines.Add(new VirtualMachineIdentity(
          virtualMachinesNode.SelectSingleNode(
            ResourceGroupNameElement).InnerText, 
          virtualMachinesNode.SelectSingleNode(
            VirtualMachineNameElement).InnerText));
      }

      return virtualMachines;
    }
  }
}

Write a C# program for Restarting a VM

A program, driven by XML files, that restarts VMs is as follows with the actual Azure credential setup and virtual machine restart code demarcated in boldface:

using System;

using Microsoft.Azure.Management.Compute.Fluent;
using Microsoft.Azure.Management.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;

namespace AzureVMChallenge
{  
  class Program
  {
    private const string EnvVarAzureChallengeSubscriptionId = 
                           "AZURECHALLENGE_SUBSCRIPTION_ID";

    private const string EnvVarAzureChallengeTenantId = 
                           "AZURECHALLENGE_TENANT_ID";

    private const string EnvVarAzureChallengeClientId = 
                           "AZURECHALLENGE_CLIENT_ID";

    private const string EnvVarAzureChallengeClientSecret = 
                           "AZURECHALLENGE_CLIENT_SECRET";

    public static void ManageVirtualMachine(
                IAzure azure, 
                VirtualMachineIdentity virtualMachineIdentity)
    {
      var vm = azure.VirtualMachines.GetByResourceGroup(
            virtualMachineIdentity.ResourceGroupName, 
            virtualMachineIdentity.VirtualMachineName);

      if  (PowerState.Running == vm.PowerState)
      {
        vm.Restart();
      }

      else 
      {
        Console.WriteLine(
          $"VM, {virtualMachineIdentity.VirtualMachineName}, " + 
           "not running so cannot restart.");        
      }
    }

    private static IAzure GetAzure() {
      var subscriptionId = Environment.GetEnvironmentVariable(
                  EnvVarAzureChallengeSubscriptionId, 
                  EnvironmentVariableTarget.User);
      var tenantId = Environment.GetEnvironmentVariable(
              EnvVarAzureChallengeTenantId, 
              EnvironmentVariableTarget.User);
      var clientId = Environment.GetEnvironmentVariable(
                EnvVarAzureChallengeClientId, 
                EnvironmentVariableTarget.User);
      var clientSecret = Environment.GetEnvironmentVariable(
                  EnvVarAzureChallengeClientSecret, 
                  EnvironmentVariableTarget.User);
      var credentials = new AzureCredentialsFactory().
                FromServicePrincipal(
                  clientId, 
                  clientSecret, 
                  tenantId, 
                  AzureEnvironment.AzureGlobalCloud);

      return Azure.Authenticate(credentials).
                  WithSubscription(subscriptionId);
    }

    private static void SimpleVMRestart()
    {
      var azure = GetAzure();
      var virtualMachineIdentity = 
          VirtualMachineManager.GetVirtualMachine(
              "TargetVirualMachine.xml");

      ManageVirtualMachine(azure, virtualMachineIdentity);
    }

    private static void MoreInterestiongRestart()
    {
      var azure = GetAzure();

      foreach (var virtualMachineIdentity in 
                 VirtualMachineManager.GetVirtualMachines(
                   "TargetVirualMachines.xml"))
      {
        ManageVirtualMachine(azure, virtualMachineIdentity);
      }
    }

    static void Main(string[] args)
    {
      try
      {
        SimpleVMRestart();
        MoreInterestiongRestart();
      }

      catch (Exception ex)
      {
        Console.WriteLine($"{ex.Message}");
        Console.WriteLine($"{ex.StackTrace}");
      }
    }
  }
}

Write a C# Program for Configuring an Azure VM

The requirement is ambiguous. It may mean modifying the configuration of the VM in Azure and it may be mean modifying the guest OS.

C# Program to Change the VM Size

An example of configuring an Azure VM by changing its size is as follows where the code to change the VM's size is identified by boldface:

    private static void ResizeVMUser(
              IAzure azure, 
              VirtualMachineIdentity virtualMachineIdentity) 
    {
      var vm = azure.VirtualMachines.GetByResourceGroup(
            virtualMachineIdentity.ResourceGroupName, 
            virtualMachineIdentity.VirtualMachineName);

      // Toggle size to show changing VM configuration
      var size =
         (VirtualMachineSizeTypes.StandardD2sV3 == 
                  vm.Inner.HardwareProfile.VmSize) ?
            VirtualMachineSizeTypes.StandardD4sV3 : 
            VirtualMachineSizeTypes.StandardD2sV3;

      vm.Inner.HardwareProfile.VmSize = size;
      vm.Inner.Validate();
      vm.Update()
        .Apply();
    }

    private static void ResizeVMExample()
    {
      var azure = GetAzure();
      var virtualMachineIdentity = 
          VirtualMachineManager.GetVirtualMachine(
            "TargetVirualMachine.xml");

      ResizeVMUser(azure, virtualMachineIdentity);      
    }

C# Program to Change the Guest OS

The following snippet adds a user to Azure VM running Linux where the code in boldface actually accesses Azure and adds the user:

    private const string LinuxVmAccessExtensionName = 
                           "VMAccessForLinux";

    private const string LinuxVmAccessExtensionPublisherName = 
                           "Microsoft.OSTCExtensions";

    private const string LinuxVmAccessExtensionTypeName = 
                           "VMAccessForLinux";

    private const string LinuxVmAccessExtensionVersionName = "1.4";

    private const string SettingUsernameName = "username";

    private const string SettingPasswordName = "password";

    private const string SettingExpirationName = "expiration";

    private static string ExpiratioNDateFormat(DateTime expiration)
    {
      return expiration.ToString("yyyy-MM-dd");
    }

    private static void AddVMUser(
                IAzure azure, 
                VirtualMachineIdentity virtualMachineIdentity,
                string newUsername,
                string newPassword,
                DateTime userExpiration) 
    {
      var vm = azure.VirtualMachines.GetByResourceGroup(
            virtualMachineIdentity.ResourceGroupName, 
            virtualMachineIdentity.VirtualMachineName);

      if (vm.ListExtensions().ContainsKey(
                          LinuxVmAccessExtensionName))
      {
        vm.Update().UpdateExtension(LinuxVmAccessExtensionName
              .WithProtectedSetting(SettingUsernameName, newUsername)
              .WithProtectedSetting(SettingPasswordName, newPassword)
              .WithProtectedSetting(
                  SettingExpirationName, 
                  ExpiratioNDateFormat(userExpiration))
            .Parent()
            .Apply();
      }

      else
      {                        
        vm.Update()
          .DefineNewExtension(LinuxVmAccessExtensionName)
              .WithPublisher(LinuxVmAccessExtensionPublisherName)
              .WithType(LinuxVmAccessExtensionTypeName)
              .WithVersion(LinuxVmAccessExtensionVersionName)
              .WithProtectedSetting(SettingUsernameName, newUsername)
              .WithProtectedSetting(SettingPasswordName, newPassword)
              .WithProtectedSetting(
                 SettingExpirationName, 
                 ExpiratioNDateFormat(userExpiration))
              .Attach()
            .Apply();
      }
    }

    private static void ConfigureVMExample()
    {
      var azure = GetAzure();
      var virtualMachineIdentity = 
          VirtualMachineManager
             .GetVirtualMachine("TargetVirualMachine.xml");
      var userExpiration = DateTime.Now.AddMonths(3);

      AddVMUser(
        azure, 
        virtualMachineIdentity, 
        "anyuser02", 
        "anypassword", 
        userExpiration);      
    }


Sunday, February 7, 2021

Microsoft Documentation: Does Microsoft follow up on User Feedback for their Documentation (Azure, Docker, ACR)?

Working through the sample code in Tutorial: Deploy and use Azure Container Registry, I noticed that a command prompt (a dollar sign) was included in a script snippet that could be copied from the aforementioned tutorial:


Clicking on the copy button copied the leading $ and the command, docker images:

$ docker images

The above issue is innocuous and most developers would immediately notice the issue when they tried to invoke "$ docket images." Still, the correct text to be copied is:

docker images

At the bottom of each page of documentation Microsoft provides feedback buttons including the This page button to provided feedback on the current page:


I submitted the issue to Microsoft and was pleasantly surprised fives hours later when Bhargavi Annadevara of Microsoft sent an email saying she had submitted a pull request (PR) to fix the issue (see below):



Friday, February 5, 2021

Azure CLI: Resource Groups, Resource Clean Up (Docker, ACR, Kubernetes, AKS)

Overview

In the post, Azure/PowerShell: Resource Groups, Resource Clean Up (Docker, ACR, Kubernetes, AKS), a demonstration on how to clean up Azure resources was given using PowerShell. The current post will present the same strategy (using the removal of an Azure resource group to clean up resources) but instead will use the Azure CLI. Bash accesses Azure using the Azure CLI. The concepts introduced in the previous post are as follows:


All source code (Azure CLI) is provided in text form in Appendix A: Source Code at the end of this post.

Azure CLI

Like Azure PowerShell, the Azure CL can be accessed using Cloud Shell which can be launched using the url, https://shell.azure.com.

Using Cloud Shell and Azure CLI a resource group can be created as follows (in location West US 2 with name rgdockerkubernetes00) using Azure CLI's az group create:

 
The code to remove the resource group is as follows using Azure CLI's az group delete:


It should be noted in the previous examples that the resource group was created at line 7 and the resource group removed at line 33. Any Azure objects (such as am ACR or AKS) created after line 7 and before line 33  for the same resource are cleaned up when the resource group is removed.

The code to create an ACR associated with resource group, rgdockerkubernetes00, is as follows using Azure CLI's az acr create:
 

The ACR will be removed when the resource group is removed.

The code to create an AKS associated with resource group, rgdockerkubernetes00, is as follows using Azure CLI's az aks create (line 17 and line 23):

 

The AKS will be removed when the resource group is removed. After line 29 code could be added in order experiment with Docker/Kubernetes. The ultimate invocation of Azure CLI's az group delete would insure that all Azure resources are cleaned up for the resource group.

A bit of explanation is needed of the above code. A Kubernetes cluster internally uses Linux virtual machines. These virtual machines require an SSH key in order to be accessed. Line 16 above detects if the key exists. If the key does not exist then Azure CLI's az aks create is invoked with the generate-ssh-keys parameter. When this parameter is specified no user response is required as the SSH keys are automatically created. The following text is generated when az aks create is invoked with the generate-ssh-keys parameter:


The generate-ssh-keys parameter was useful given this was a sample script. A more real world approach would be to create the SSH key backup the SSH key before creating the AKS. The following link from Microsoft, Quick steps: Create and use an SSH public-private key pair for Linux VMs in Azure., demonstrates how to create an SSH key.

Since the Azure CLI's az group delete cleans up the ACR and AKS resources, there is no need to explicitly invoke az acr delete or az aks delete.

Appendix A: Source Code

The Azure CLI source code for this post is as follows:

#!/bin/bash -x
resource_group_name='rgdockerkubernetes00'
acr_name='crdockerkubernetes00'
aks_name='ksdockerkubernetes00'
node_count=2

az group create \
     --name $resource_group_name \
     --location 'West US 2'

az acr create \
    --resource-group $resource_group_name \
    --name $acr_name \
    --sku Basic

if [ -f ~/.ssh/id_rsa ]; then
    az aks create \
        --resource-group $resource_group_name \
        --name $aks_name \
        --node-count $node_count \
        --attach-acr $acr_name
else    
    az aks create \
        --resource-group $resource_group_name \
        --name $aks_name \
        --node-count $node_count \
        --attach-acr $acr_name \
        --generate-ssh-keys
fi

# Manipulate Azure resources here

az group delete \
    --name $resource_group_name \
    --yes


Thursday, February 4, 2021

Azure: Toggling Cloud Shell between PowerShell and Bash

Azure Cloud Shell, https://shell.azure.com, supports both PowerShell and Bash but just not both at the same time. When Cloud Shell is invoked, there is a dropdown in the upper left corner that identifies the current scripting environment. Below (see upper left) the scripting environment for Cloud Shell is PowerShell:

The term PowerShell above is a dropdown. Clicking on the dropdown allows the supported scripting of Cloud Shell to be changed to Bash (see below):



Azure/PowerShell: Resource Groups, Resource Clean Up (Docker, ACR, Kubernetes, AKS)

Overview

Developers and DevOps engineers should be conscious of the resource they create under Azure as these resources come at a cost. Engineers with MSDN subscription receive a $150 per-month in Azure credit and engineers who sign up for Azure receive a $200 credit for their first month (Create your Azure free account today) can quickly burn up their complimentary allotment. A simple approach to controlling Azure costs is to:
  • Create a new Azure resource group
  • Perform a development/devops task using Azure resources associated with the newly created resource group
  • Delete the newly created resource group
By deleting the resource group the resources are released and hence Azure will no longer charge for said resources. 

A specific scenario using a resource group to insure Azure object clean up is:
  • Create a new Azure resource group
  • Create an Azure Container Registry (ACS) that would be used to manage Docker containers,
  • Create an Azure Kubernetes Service (AKS) cluster
  • Perform specific Docker/Kubernetes tasks 
  • Delete the Azure resource group thus cleaning up the ACS and the AKS

The post demonstrates the above sequence of tasks using PowerShell. All source code is provided in text form in Appendix A: Source Code at the end of this post.

PowerShell

PowerShell can access Azure from a physical or virtual host but an elegant way to access Azure with PowerShell is to login to the Azure Portal (https://portal.azure.com/) and launch Cloud Shell. Once logged into the Azure Portal the button for launch Cloud Shell is highlighted by an ellipse below:


The https://shell.azure.com url brings up Cloud Shell directly.

Using Cloud Shell and PowerShell a resource group can be created as follows (in location West US 2 with name rgdockerkubernetes00) using the New-AzResourceGroup cmdlet:


The code to remove the resource group using Powershell is as follows using the Remove-AzResourceGroup cmdlet:


It should be noted in the previous examples that the resource group was created at line 6 and the resource group removed at line 37. Any Azure objects (such as a container registry or a Kubernetes service) created after line 8 and before line 37 for the same resource are cleaned up when the resource group is removed.

The code to create an ACR associated with resource group, rgdockerkubernetes00, is as follows using the New-AzContainerRegistry cmdlet:


The ACR will be removed when the resource group is removed.

The code to create an AKS associated with resource group, rgdockerkubernetes00, is as follows using the New-AzAksCluster cmdlet (line 17 and line 27):


The AKS will be removed when the resource group is removed. After line 33 code could be added in order experiment with Docker/Kubernetes. The ultimate invocation of Remove-AzResourceGroup would insure that all Azure resources are cleaned up for the resource group.

A bit of explanation is needed for the above code. A Kubernetes cluster internally uses Linux virtual machines. These virtual machines require an SSH Key in order to be accessed. Line 15 above detects if the key exists. If the key does not exist then the New-AzAksCluster cmdlet is invoked with the GeneratesSshKey parameter. When this parameter is specified a user is required to respond to the following two prompts used in creating the SSH key:


Most Azure PowerShell scripts are not meant to be run with user interaction. A practical approach would be to create the SSH key in advance and appropriately backup the SSH keys. Microsoft provides an excellent tutorial on creating an SSH key at Quick steps: Create and use an SSH public-private key pair for Linux VMs in Azure.

Since Remove-AzResourceGroup cleans up the ACR and AKS resources, there is no need to explicitly invoke Remove-AzContainerRegistry or Remove-AzAksCluster.

Appendix A: Source Code

The source code in its entirety is as follows:

[string] $resourceGroupName = 'rgdockerkubernetes00'
[string] $acrName = 'crdockerkubernetes00'
[string] $aksName = 'ksdockerkubernetes00'
[int] $nodeCount = 2

New-AzResourceGroup `
    -Name $resourceGroupName `
    -Location 'West US 2' | Out-Null

New-AzContainerRegistry `
    -ResourceGroupName $resourceGroupName `
    -Name $acrName `
    -Sku 'Basic' | Out-Null

if (Test-Path '~/.ssh/id_rsa' -PathType Leaf) {
    Write-Host 'SSH Keys Exist'
    New-AzAksCluster `
        -ResourceGroupName $resourceGroupName `
        -AcrNameToAttach $acrName `
        -NodeCount $nodeCount `
        -Name $aksName
}

else {
    # -GenerateSshKey generates a prompt
    Write-Host 'Generate SSH Keys'
    New-AzAksCluster `
        -ResourceGroupName $resourceGroupName `
        -AcrNameToAttach $acrName `
        -NodeCount $nodeCount `
        -Name $aksName `
        -GenerateSshKey
}

<# code here that uses Docker/Kubernetes #>

Remove-AzResourceGroup `
    -Name $resourceGroupName `
    -Force # | Out-Null