# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT

<#
.SYNOPSIS
Returns whether there's a name collision in the resource group.

.DESCRIPTION
Find-ResourceGroupNameCollision takes a list of resources, and checks if $Test
collides names with any of the resources.

.PARAMETER Test
The name to test.

.PARAMETER Resources
The list of resources.
#>
function Find-ResourceGroupNameCollision {
  [CmdletBinding()]
  Param([string]$Test, $Resources)

  foreach ($resource in $Resources) {
    if ($resource.ResourceGroupName -eq $Test) {
      return $true
    }
  }

  return $false
}

<#
.SYNOPSIS
Attempts to find a name that does not collide with any resources in the resource group.

.DESCRIPTION
Find-ResourceGroupName takes a set of resources from Get-AzResourceGroup, and finds the
first name in {$Prefix, $Prefix-1, $Prefix-2, ...} such that the name doesn't collide with
any of the resources in the resource group.

.PARAMETER Prefix
The prefix of the final name; the returned name will be of the form "$Prefix(-[1-9][0-9]*)?"
#>
function Find-ResourceGroupName {
  [CmdletBinding()]
  Param([string] $Prefix)

  $resources = Get-AzResourceGroup
  $result = $Prefix
  $suffix = 0
  while (Find-ResourceGroupNameCollision -Test $result -Resources $resources) {
    $suffix++
    $result = "$Prefix-$suffix"
  }

  return $result
}

<#
.SYNOPSIS
Returns whether there's a name collision for an image in the resource group.

.DESCRIPTION
Find-ImageNameCollision takes a list of images, and checks if $Test
collides names with any of the image names.

.PARAMETER Test
The name to test.

.PARAMETER Images
The list of images.
#>
function Find-ImageNameCollision {
  [CmdletBinding()]
  Param([string]$Test, $Images)

  foreach ($resource in $Images) {
    if ($resource.Name -eq $Test) {
      return $true
    }
  }

  return $false
}

<#
.SYNOPSIS
Attempts to find a name that does not collide with any images in the resource group.

.DESCRIPTION
Find-ResourceGroupName takes a set of resources from Get-AzResourceGroup, and finds the
first name in {$Prefix, $Prefix-1, $Prefix-2, ...} such that the name doesn't collide with
any of the resources in the resource group.

.PARAMETER Prefix
The prefix of the final name; the returned name will be of the form "$Prefix(-[1-9][0-9]*)?"
#>
function Find-ImageName {
  [CmdLetBinding()]
  Param([string]$ResourceGroupName, [string]$Prefix)

  $images = Get-AzImage -ResourceGroupName $ResourceGroupName
  $result = $Prefix
  $suffix = 0
  while (Find-ImageNameCollision -Test $result -Images $images) {
    $suffix++
    $result = "$Prefix-$suffix"
  }

  return $result
}

<#
.SYNOPSIS
Generates a random password.

.DESCRIPTION
New-Password generates a password, randomly, of length $Length, containing
only alphanumeric characters, underscore, and dash.

.PARAMETER Length
The length of the returned password.
#>
function New-Password {
  Param ([int] $Length = 32)

  # This 64-character alphabet generates 6 bits of entropy per character.
  # The power-of-2 alphabet size allows us to select a character by masking a random Byte with bitwise-AND.
  $alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
  $mask = 63
  if ($alphabet.Length -ne 64) {
    throw 'Bad alphabet length'
  }

  [Byte[]]$randomData = [Byte[]]::new($Length)
  $rng = $null
  try {
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    $rng.GetBytes($randomData)
  }
  finally {
    if ($null -ne $rng) {
      $rng.Dispose()
    }
  }

  $result = ''
  for ($idx = 0; $idx -lt $Length; $idx++) {
    $result += $alphabet[$randomData[$idx] -band $mask]
  }

  return $result
}

<#
.SYNOPSIS
Waits for the shutdown of the specified resource.

.DESCRIPTION
Wait-Shutdown takes a VM, and checks if there's a 'PowerState/stopped'
code; if there is, it returns. If there isn't, it waits ten seconds and
tries again.

.PARAMETER ResourceGroupName
The name of the resource group to look up the VM in.

.PARAMETER Name
The name of the virtual machine to wait on.
#>
function Wait-Shutdown {
  [CmdletBinding()]
  Param([string]$ResourceGroupName, [string]$Name)

  Write-Host "Waiting for $Name to stop..."
  while ($true) {
    $Vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -Status
    $highestStatus = $Vm.Statuses.Count
    for ($idx = 0; $idx -lt $highestStatus; $idx++) {
      if ($Vm.Statuses[$idx].Code -eq 'PowerState/stopped') {
        return
      }
    }

    Write-Host "... not stopped yet, sleeping for 10 seconds"
    Start-Sleep -Seconds 10
  }
}

<#
.SYNOPSIS
Sanitizes a name to be used in a storage account.

.DESCRIPTION
Sanitize-Name takes a string, and removes all of the '-'s and
lowercases the string, since storage account names must have no
'-'s and must be completely lowercase alphanumeric. It then makes
certain that the length of the string is not greater than 24,
since that is invalid.

.PARAMETER RawName
The name to sanitize.
#>
function Sanitize-Name {
  [CmdletBinding()]
  Param(
    [string]$RawName
  )

  $result = $RawName.Replace('-', '').ToLowerInvariant()
  if ($result.Length -gt 24) {
    Write-Error 'Sanitized name for storage account $result was too long.'
    throw
  }

  return $result
}

<#
.SYNOPSIS
Creates a new Azure virtual network with locked down firewall rules.

.PARAMETER ResourceGroupName
The name of the resource group in which the virtual network should be created.

.PARAMETER Location
The location (region) where the network is to be created.
#>
function Create-LockedDownNetwork {
  [CmdletBinding()]
  Param(
    [parameter(Mandatory=$true)]
    [string]$ResourceGroupName,
    [parameter(Mandatory=$true)]
    [string]$Location
  )

  $allFirewallRules = @()

  $allFirewallRules += New-AzNetworkSecurityRuleConfig `
    -Name AllowHTTP `
    -Description 'Allow HTTP(S)' `
    -Access Allow `
    -Protocol Tcp `
    -Direction Outbound `
    -Priority 1008 `
    -SourceAddressPrefix * `
    -SourcePortRange * `
    -DestinationAddressPrefix * `
    -DestinationPortRange @(80, 443)

  $allFirewallRules += New-AzNetworkSecurityRuleConfig `
    -Name AllowSFTP `
    -Description 'Allow (S)FTP' `
    -Access Allow `
    -Protocol Tcp `
    -Direction Outbound `
    -Priority 1009 `
    -SourceAddressPrefix * `
    -SourcePortRange * `
    -DestinationAddressPrefix * `
    -DestinationPortRange @(21, 22)

  $allFirewallRules += New-AzNetworkSecurityRuleConfig `
    -Name AllowDNS `
    -Description 'Allow DNS' `
    -Access Allow `
    -Protocol * `
    -Direction Outbound `
    -Priority 1010 `
    -SourceAddressPrefix * `
    -SourcePortRange * `
    -DestinationAddressPrefix * `
    -DestinationPortRange 53

  $allFirewallRules += New-AzNetworkSecurityRuleConfig `
    -Name AllowGit `
    -Description 'Allow git' `
    -Access Allow `
    -Protocol Tcp `
    -Direction Outbound `
    -Priority 1011 `
    -SourceAddressPrefix * `
    -SourcePortRange * `
    -DestinationAddressPrefix * `
    -DestinationPortRange 9418

  $allFirewallRules += New-AzNetworkSecurityRuleConfig `
    -Name DenyElse `
    -Description 'Deny everything else' `
    -Access Deny `
    -Protocol * `
    -Direction Outbound `
    -Priority 1013 `
    -SourceAddressPrefix * `
    -SourcePortRange * `
    -DestinationAddressPrefix * `
    -DestinationPortRange *

  $NetworkSecurityGroupName = $ResourceGroupName + 'NetworkSecurity'
  $NetworkSecurityGroup = New-AzNetworkSecurityGroup `
    -Name $NetworkSecurityGroupName `
    -ResourceGroupName $ResourceGroupName `
    -Location $Location `
    -SecurityRules $allFirewallRules

  $SubnetName = $ResourceGroupName + 'Subnet'
  $Subnet = New-AzVirtualNetworkSubnetConfig `
    -Name $SubnetName `
    -AddressPrefix "10.0.0.0/16" `
    -NetworkSecurityGroup $NetworkSecurityGroup `
    -ServiceEndpoint "Microsoft.Storage"

  $VirtualNetworkName = $ResourceGroupName + 'Network'
  $VirtualNetwork = New-AzVirtualNetwork `
    -Name $VirtualNetworkName `
    -ResourceGroupName $ResourceGroupName `
    -Location $Location `
    -AddressPrefix "10.0.0.0/16" `
    -Subnet $Subnet

  return $VirtualNetwork
}

function Invoke-AzVMRunCommandWithRetries {
  $result = $null
  $success = $false
  $attempt = 0
  while ($success -eq $false) {
    try {
      ++$attempt
      Write-Host "Command attempt $attempt..."
      $result = Invoke-AzVMRunCommand @args
      $success = $true
    } catch {
      Write-Host "Running command failed. $_ Retrying after 10 seconds..."
      Start-Sleep -Seconds 10
      if ($attempt -eq 5) {
        Write-Error "Running command failed too many times. Giving up!"
        throw $_
      }
    }
  }

  return $result
}

Export-ModuleMember -Function Find-ResourceGroupName
Export-ModuleMember -Function Find-ImageName
Export-ModuleMember -Function New-Password
Export-ModuleMember -Function Wait-Shutdown
Export-ModuleMember -Function Sanitize-Name
Export-ModuleMember -Function Create-LockedDownNetwork
Export-ModuleMember -Function Invoke-AzVMRunCommandWithRetries
