roeprofile.psm1

Function Clear-UserVariables {
    #Clear any variable not defined in the $SysVars variable
    $UserVars = get-childitem variable: | Where {$SysVars -notcontains $_.Name} 
    ForEach ($var in $UserVars) {
        Write-Host ("Clearing $" + $var.name)
        Remove-Variable $var.name -Scope 'Global'
    }
}
Function Connect-EXOPartner {
    param(
        [parameter(Mandatory = $false)]
        [System.Management.Automation.CredentialAttribute()] 
        $Credential, 
        [parameter(Mandatory = $false)]
        [string] 
        $TenantDomain 
    )
    if (-not $TenantDomain) {
        $TenantDomain = Read-Host -Prompt "Input tenant domain, e.g. hosters.com"
    }
    if (-not $Credential) {
        $Credential = Get-Credential -Message "Credentials for CSP delegated admin, e.g. ""bm@klestrup.dk""/""password"""
    }
    $ExSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$TenantDomain" -Credential $Credential -Authentication Basic -AllowRedirection
    if ($ExSession) {Import-PSSession $ExSession}
}
Function ConvertTo-HashTable {
    # https://stackoverflow.com/questions/3740128/pscustomobject-to-hashtable
    param (
        [Parameter(ValueFromPipeline)]
        $PSObject
    )
  
    process {
        if ($null -eq $PSObject -and $null -eq $JSON ) { 
            return $null 
        }
          
        if ($PSObject -is [string]) {
            $PSObject = $PSObject | ConvertFrom-Json
        }
  
        if ($PSObject -is [System.Collections.IEnumerable] -and $PSObject -isnot [string]) {
            $collection = @(
                foreach ($object in $PSObject) { ConvertTo-HashTable $object }
            )
            Write-Output -NoEnumerate $collection
        }
        elseif ($PSObject -is [psobject]) {
            $hash = [ordered]@{}
            foreach ($property in $PSObject.PSObject.Properties) {
                $hash[$property.Name] = ConvertTo-HashTable $property.Value
            }
            return $hash
        }
        else {
            return $PSObject
        }
    }
}
Function Get-COMObjects {
    if (-not $IsLinux) {
        $Objects = Get-ChildItem HKLM:\Software\Classes -ErrorAction SilentlyContinue | Where-Object {$_.PSChildName -match '^\w+\.\w+$' -and (Test-Path -Path "$($_.PSPath)\CLSID")}
        $Objects | Select-Object -ExpandProperty PSChildName
    }
}
Function Get-NetIPAdapters {
    Param(
    [Parameter(Mandatory = $false)]
        [String[]]$ComputerName
    )
    if ($ComputerName.length -lt 1 -or $computername.Count -lt 1) {
        $computername = @($env:COMPUTERNAME)
    }
    $OutPut = @()
    #Vis netkort med tilhørende IP adr.
    foreach ($pc in $Computername) {
        $OutPut += Get-NetAdapter -CimSession $pc | Select-Object Name,InterfaceDescription,IfIndex,Status,MacAddress,LinkSpeed,@{N="IPv4";E={(Get-NetIpaddress -CimSession $pc -InterfaceIndex $_.ifindex -AddressFamily IPv4 ).IPAddress}},@{N="IPv6";E={(Get-NetIpaddress -CimSession $pc -InterfaceIndex $_.ifindex -AddressFamily IPv6 ).IPAddress}},@{N="Computer";E={$pc}} | Sort-Object -Property Name
    }
    $OutPut
}
Function Get-StringASCIIValues {
[CMDLetbinding()]
    param (
        [string]$String
   )

   return $String.ToCharArray() | ForEach-Object {$_ + " : " + [int][Char]$_}
}
Function Get-StringHash {
    #https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.1 (ex. 4)
    [CMDLetBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$String,
        [Parameter(Mandatory = $false)]
        [ValidateSet("SHA1","SHA256","SHA384","SHA512","MACTripleDES","MD5","RIPEMD160")]
        [string]$Algorithm = "SHA256",
        [Parameter(Mandatory = $false)]
        [int]$GroupCount = 2,
        [Parameter(Mandatory = $false)]
        [String]$Seperator = "-"
    )

    $Result = @()
    Write-Verbose "Received $($String.count) string(s)"
    Write-Verbose "Using algorithm: $Algorithm"
    $stringAsStream = [System.IO.MemoryStream]::new()
    $writer = [System.IO.StreamWriter]::new($stringAsStream)
    foreach ($t in $String) {
        $writer.write($t)
        $writer.Flush()
        $stringAsStream.Position = 0
        $HashString = Get-FileHash -InputStream $stringAsStream -Algorithm $Algorithm | Select-Object -ExpandProperty Hash
        Write-Verbose "$($HashString.length) characters in hash"
    }

    Write-Verbose "Dividing string to groups of $GroupCount characters, seperated by $Seperator"
    for ($x = 0 ; $x -lt $HashString.Length ; $x = $x + $GroupCount) {
        $Result += $HashString[$x..($x + ($GroupCount -1))] -join ""
    }
    Write-Verbose "$($Result.count) groups"
    $Result = $Result -join $Seperator
    Write-Verbose "Returning $($Result.length) character string"

    return $Result
}
Function Get-UNCPath {
    param(
        [parameter(Mandatory=$true)]
        [String]$Path
    )
    if (-not $IsLinux) {
        $DriveLetter = ($Path)[0]
        if ($DriveLetter -match "[a-z]") {
            $PSDrive = Get-PSDrive | Where-Object {$_.Name -eq $DriveLetter}
            if ($PSDrive.DisplayRoot) {
                $Path = $Path.Substring(3,($Path.length -3))
                $Path = $PSdrive.DisplayRoot + "\" + $Path
            }
        }
    }
    return $Path
}
Function Get-UserVariables {
    #Get, and display, any variable not defined in the $SysVars variable
    get-childitem variable: | Where-Object {$SysVars -notcontains $_.Name}
}
Function Get-WebRequestError {
<#
        .SYNOPSIS
        Read more detailed error from failed Invoke-Webrequest and Invoke-RestMethod
        https://stackoverflow.com/questions/35986647/how-do-i-get-the-body-of-a-web-request-that-returned-400-bad-request-from-invoke

        .Parameter ErrorObject
        $_ from a Catch block

    #>


    [CMDLetbinding()]
    param (
        [object]$ErrorObject
    )

    $streamReader = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream())
    $ErrResp = $streamReader.ReadToEnd() | ConvertFrom-Json
    $streamReader.Close()
    return $ErrResp
}
Function New-YAMLTemplate {
<#
        .SYNOPSIS
        Generates Azure Pipelines based on the comment based help in the input script

        .Description
        Generates Azure Pipelines based on the comment based help in the input script.
        For help on comment based help see https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.2
        Script parameters are parsed, and YAML template, pipeline and, optional, variable files are generated with default values, and validateset values prepopulated.

        Default variables that will be defined in pipeline yaml:
          deployment: DeploymentDisplayName converted to lower-case, spaces replaced with underscores, and non-alphanumeric characters removed.
          deploymentDisplayName: Value of DeploymentDisplayName parameter

        For each supplied environment, default variables will be created:
          <ENV>_ServiceConnection: '<ENV>_<ServiceConnectionSuffix parameter value>' If no value is supplied, default value is simply "ServiceConnection"
          <ENV>_EnvironmentName: '<ENV>'
        
        Outputfiles will be placed in the same directory as the source script.
        The template for the script will have the extension .yml and the sample files for pipelines will have the extension .yml.sample

    
        .Parameter ScriptPath
        Path to script to generate YAML templates for

        .Parameter DeploymentDisplayName
        Display Name of deployment when pipeline is run.

        .Parameter Environment
        Name of environment(s) to deploy to

        .Parameter Overwrite
        Overwrite existing YAML files

        .Parameter PipelineVariablesToFile
        Determines if variables are written to separate YAML file(s). If omitted all variables are declared in the pipeline YAML.

        .Parameter ServiceConnectionSuffix
        Suffix to apply to serviceconnection variable values.

        .Parameter PowerShell7
        Use PowerShell 7

        .Example
        PS> $ScriptPath = "C:\Scripts\AwesomeScript.ps1"
        PS> New-YAMLTemplate -ScriptPath $ScriptPath -Environment "DEV","TEST" -Overwrite

        This will generate a template file and a pipeline file for deployment of C:\Scripts\AwesomeScript.ps1 to DEV and TEST environments.
        Existing files will be overwritten.
        All variables will be declared in the pipeline YAML.


        .Example
        PS> $ScriptPath = "C:\Scripts\AwesomeScript.ps1"
        PS> New-YAMLTemplate -ScriptPath $ScriptPath -Environment "DEV","TEST" -PipelineVariablesToFile

        This will generate a template file, a pipeline file and two variable files for deployment of C:\Scripts\AwesomeScript.ps1 to DEV and TEST environments.
        If files already exist the script will throw an error.
        Variables will be declared in separate files for each environment.


        #>

    [CMDLetbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Get-ChildItem -File -Path $_})]
        [String]$ScriptPath,
        [Parameter(Mandatory = $false)]
        [String]$DeploymentDisplayName = "Deployment of $(Split-Path -Path $ScriptPath -Leaf)",
        [Parameter(Mandatory = $false)]
        [String[]]$Environment = "Dev",
        [Parameter(Mandatory = $false)]
        [Switch]$Overwrite,
        [Parameter(Mandatory = $false)]
        [Switch]$PipelineVariablesToFile,
        [Parameter(Mandatory = $false)]
        [String]$ServiceConnectionSuffix = "ServiceConnection",
        [Parameter(Mandatory = $False)]
        [switch]$pwsh
    )

    # Pipeline PowerShell task: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/powershell?view=azure-devops
    # Workaround to make sure we get the correct case from the filename, and not just whatever is passed to us as parameter value
    $ScriptName = Split-Path -Path $ScriptPath -Leaf
    $ScriptDirectory = Split-Path -Path $ScriptPath -Parent
    $ScriptFile = Get-Item -Path "$ScriptDirectory\*.*" | Where-Object {$_.Name -eq $ScriptName}

    # Declare variables
    $ScriptHelp                 = Get-Help -Name $ScriptPath
    $ScriptCommand              = (Get-Command -Name $ScriptPath)
    $ScriptCommandParameters    = $ScriptCommand.Parameters
    $ScriptHelpParameters       = $ScriptHelp.parameters
    $ScriptBaseName             = $ScriptFile.BaseName
    $VariablePrefix             = $ScriptBaseName.replace("-","_").ToLower()
    # File names in lower case in accordance with https://dev.azure.com/itera-dk/Mastercard.PaymentsOnboarding/_wiki/wikis/Mastercard.PaymentsOnboarding.wiki/435/Repository-structure-(suggestion)
    $TemplateFileName = $ScriptFile.DirectoryName + "\" + $ScriptFile.Name.Replace(".ps1",".yml").ToLower()
    $PipelineFileName = (Split-Path -Path $TemplateFileName -Parent) + "\deploy-" + (Split-Path -Path $TemplateFileName -Leaf) + ".sample"
    $PipelineVariablesFileNameTemplate = $PipelineFileName.Replace(".yml.sample","-#ENV#-vars.yml.sample").ToLower() #Template for variable files. #ENV# is replace with corresponding environment name.

    # Optional header for the variables and pipeline files.
    $PipelineVariablesHeader = @()
    $PipelineVariablesHeader += "# If using double quotes, remember to escape special characters"
    $PipelineVariablesHeader += "# Booleans, and Numbers, must be passed in {{ variables.<variablename>}} to template to retain data type when received by template."
    $PipelineVariablesHeader += "# Booleans still need to be prefixed with $ when passed to script, because Pipelines sucks (https://www.codewrecks.com/post/azdo/pipeline/powershell-boolean/)"
    $PipelineVariablesHeader += "# Split long strings to multiple lines by using >- , indenting value-lines ONE level and NO single-quotes surrounding entire value (https://yaml-multiline.info/ - (folded + strip)) "

    

    # Get repo-relative paths of script if possible.
    Push-Location -Path (Split-Path -Path $ScriptPath -Parent) -StackName RelativePath
    try {
        $ScriptRelativePath     =  & git ls-files --full-name $ScriptPath
        if ([string]::IsNullOrWhiteSpace($ScriptRelativePath)) {
            $ScriptRelativePath = & git ls-files --full-name $ScriptPath --others
        }
    }
    catch {
        Write-Warning "Unable to run git commands"
        Write-Warning $_.Exception.Message 
    }
    Pop-Location -StackName RelativePath

    if ([string]::IsNullOrWhiteSpace($ScriptRelativePath)) {
        $ScriptRelativePath = (Resolve-Path -Path $ScriptPath -Relative).replace("\","/")
        Write-Warning "Couldn't find path relative to repository. Is file in a repo? Are there differences in casing? Relative references will fall back to $ScriptRelativePath"
    }

    # Get relative paths for use for references in pipeline yaml
    $TemplateRelativePath = $ScriptRelativePath.replace(".ps1",".yml")
    $RelativePath = (Split-Path -Path $ScriptRelativePath -Parent).Replace("\","/")

    if ($PipelineVariablesToFile) {
        $RelativePipelineVariablesFileNameTemplate  =  (Split-Path -Path $PipelineVariablesFileNameTemplate -Leaf) 
        $PipelineVariablesFileNames                 = @{}
        $RelativePipelineVariablesFileNames         = @{}
        foreach ($e in $Environment) {
            $RelativePipelineVariablesFileNames.add($e,($RelativePipelineVariablesFileNameTemplate -replace("#ENV#",$e)).tolower())  # Used as reference in pipeline
            $PipelineVariablesFileNames.add($e,($PipelineVariablesFileNameTemplate -replace("#ENV#",$e)).ToLower())                  # Used to write the files
        }
    }


    # Parse the parameters and get necessary values for YAML generation
    $ScriptParameters = @()
    foreach ($param in $ScriptHelpParameters.parameter) {
        $Command = $ScriptCommandParameters[$param.name]
        $Props = [ordered]@{"Description"       = $param.description 
                            "Name"              = $param.name
                            "HelpMessage"       = ($Command.Attributes | Where-Object {$_.GetType().Name -eq "ParameterAttribute"}).HelpMessage
                            "Type"              = $param.type
                            "Required"          = $param.required
                            "DefaultValue"      = $param.defaultValue
                            "ValidateSet"       = ($Command.Attributes | Where-Object {$_.GetType().Name -eq "ValidateSetAttribute"}).ValidValues
                            "ValidateScript"    = ($Command.Attributes | Where-Object {$_.GetType().Name -eq "ValidateScriptAttribute"}).scriptblock
                            }
        # Build a description text to add to variables, and parameters, in YAML files
        $YAMLHelp = ""
        if ($props.Description.length -gt 0) {
            $YAMLHelp += "Description: $((($props.Description | foreach-object {$_.Text}) -join " ") -replace ("`r`n|`n|`r", " "))"
        }
    
        $YAMLHelp += " Required: $($param.required)"

        if ($Props.HelpMessage.Length -gt 0) {
            $YAMLHelp += " Help: $($Props.HelpMessage)"
        }

        if ($Props.ValidateSet.Count -gt 0) {
            $YAMLHelp += " ValidateSet: ($($Props.ValidateSet -join ","))"
        }

        if ($Props.ValidateScript.Length -gt 0) {
            $YAMLHelp += " ValidateScript: {$($Props.ValidateScript)}"
        }

        if ($YAMLHelp.Length -gt 0) {
            $Props.add("YAMLHelp",$YAMLHelp.Trim())
        }
    
        $ScriptParameters += New-Object -TypeName PSObject -Property $Props
    }

    if ($ScriptParameters.count -eq 0) {
        Write-Warning "No parameters found for $ScriptPath. Make sure comment based help is correctly entered: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.2"
    }

    # Build the YAMLParameters object containing more YAML specific information (could be done in previous loop... to do someday)
    $YAMLParameters = @()
    $ScriptArguments = ""
    foreach ($param in $ScriptParameters) {
        $ParamType = $ParamDefaultValue = $null 
        # There are really only 3 parameter types we can use when running Powershell in a pipeline
        switch ($Param.Type.Name ) {
            "SwitchParameter"                                   {$ParamType = "boolean"}
            {$_ -match"Int|Int32|long|byte|double|single"}      {$ParamType = "number"}
            default                                             {$ParamType = "string"} # Undeclared parameters will be of type Object and treated as string
        }

        # Not a proper switch, but this is where we figure out the correct default value
        switch ($Param.DefaultValue) {
            {$_ -match "\$"}                                                                            {$ParamDefaultValue = "'' # Scriptet default: $($Param.DefaultValue)" ; break} # If default value contains $ it most likely references another parameter.
            {(-not ([string]::IsNullOrWhiteSpace($Param.DefaultValue)) -and $ParamType -eq "String")}   {$ParamDefaultValue = "'$($param.defaultValue)'" ; break} # Add single quotes around string values
            {$ParamType -eq "number"}                                                                   {$ParamDefaultValue = "$($param.defaultValue)" ; break} # No quotes around numbers as that would make it a string
            {$ParamType -eq "boolean"}                                                                  {if ($param.defaultvalue -eq $true) {$ParamDefaultValue = "true"} else {$ParamDefaultValue = "false"} ; break} # Set a default value for booleans as well
            default {$ParamDefaultValue = "''"} # If all else fails, set the default value to empty string
        }

        $YAMLParameterProps = @{"Name"          = $Param.name
                                "YAMLHelp"   = $Param.YAMLHelp
                                "Type"          = $ParamType
                                "Default"       = $ParamDefaultValue
                                "ValidateSet"   = $param.validateSet 
                                "VariableName"  = "#ENV#_$($VariablePrefix)_$($param.name)" # Property to use as variable name in YAML. #ENV# will be replaced with the different environments to deploy to
                                }
        $YAMLParameters += New-Object -TypeName PSObject -Property $YAMLParameterProps

        # Define the scriptarguments to pass to the script. The name of the variable will correspond with the name of the parameter
        if ($ParamType -eq "boolean") {
            $ScriptArguments += ("-$($Param.Name):`$`${{parameters.$($Param.name)}} ") # Add additional $ to turn "false" into "$false"
        }
        elseif ($param.type.name -eq "String") {
            $ScriptArguments += ("-$($Param.Name) '`${{parameters.$($Param.name)}}' ") # Make sure string values has single quotes around them so spaces and special characters survive
        } 
        else { #integer type
            $ScriptArguments += ("-$($Param.Name) `${{parameters.$($Param.name)}} ") # Numbers as-is
        } 
    
    } 


    # Initialize PipelineVariables and set the corresponding template as comment
    $PipelineVariables = @()
    $PipelineVariables += " # $(Split-Path -Path $TemplateFilename -leaf)"

    # Default template parameters independent of script parameters
    $TemplateParameters = @()
    $TemplateParameters += " - name: serviceConnection # The name of the service Connection to use"
    $TemplateParameters += " type: string"
    $TemplateParameters += " default: false"

    # Build the template parameters
    foreach ($param in $YAMLParameters) {
        $TemplateParameters += ""
        $TemplateParameters += " - name: $($param.Name) # $($Param.YAMLHelp)"
        $TemplateParameters += " type: $($Param.type)"
        $TemplateParameters += " default: $($Param.Default)"
        if ($param.validateset) {
            $TemplateParameters += " values:"
            foreach ($value in $param.validateset) {
                if ($param.Type -eq "number") {
                    $TemplateParameters += " - $value"
                }
                else {
                    $TemplateParameters += " - '$value'"
                }
            }
        }
    
        $PipelineVariables += " $($Param.VariableName): $($param.Default) # $($Param.YAMLHelp)"
    
    }

    #region BuildTemplate
    $Template = @()
    $Template += "# Template to deploy $($ScriptFile.Name):"

    # Add script synopsis to template file if available
    if ($ScriptHelp.Synopsis) {
        if ($ScriptHelp.Synopsis.length -gt 0) {
            $Template += "# Synopsis:"
            $TempLine = ($ScriptHelp.Synopsis | foreach-object {$_ | Out-String} ).split("`r`n") 
            foreach ($line in $TempLine) {
                $Template += "#`t" + $line
            }
        }
    }

    # Add script description to template file if available
    if ($ScriptHelp.description) {
        if ($ScriptHelp.description[0].Text.length -gt 0) {
            $Template += "# Description:"
            $TempLine = ($ScriptHelp.description | foreach-object {$_ | Out-String} ) -split("`r`n") 
            foreach ($line in $TempLine) {
                $Template += "#`t" + $line
            }
        }
    }
    
    # Add script notes to template file if available
    if ($Scripthelp.Alertset) {
        if ($ScriptHelp.alertset.alert[0].Text.length -gt 0) {
            $Template += "# Notes:"
            $TempLine =  ($ScriptHelp.alertset.alert[0] | foreach-object {$_ | Out-String} ).split("`r`n") | ForEach-Object {if ( -not [string]::isnullorwhitespace($_)) {$_}}
            foreach ($line in $TempLine) {
                $Template += "#`t" + $line
            }
        }
    }

    $Template += ""
    $Template += "parameters:"
    $Template += $TemplateParameters
    $Template += ""
    $Template += "steps:"
    $Template += " - task: AzurePowerShell@5"
    $Template += " displayName: ""PS: $($ScriptFile.Name)"""
    $Template += " inputs:"
    $Template += ' azureSubscription: "${{parameters.serviceConnection}}"'
    $Template += ' scriptType: "FilePath"'
    $Template += " scriptPath: ""$ScriptRelativePath"" # Relative to repo root"
    $Template += " azurePowerShellVersion: latestVersion"
    $Template += " scriptArguments: $ScriptArguments"
    if ($PowerShell7) {
        $Template += " pwsh: true # Run in PowerShell 7"
    }
    else {
        $Template += " pwsh: false # Run in PowerShell 7"
    }
    #endregion #BuildTemplate

    #region BuildPipeline
    $Pipeline = @()
    $Pipeline += "# Pipeline to deploy $(Split-Path -Path $TemplateFileName -Leaf)"
    $Pipeline += ""
    $Pipeline += "trigger: none"
    $Pipeline += ""
    $Pipeline += $PipelineVariablesHeader
    $Pipeline += "variables:"
    $Pipeline += " # Pipeline variables"
    $Pipeline += " deployment: '$((($DeploymentDisplayName.ToCharArray() | Where-Object {$_ -match '[\w| ]'}) -join '').replace(" ","_").tolower())' # Name of deployment"
    $Pipeline += " deploymentDisplayName: '$DeploymentDisplayName' # Name of deployment"
    foreach ($environ in $Environment) {
        $Pipeline += " $($environ)_ServiceConnection: '$($environ)_$($ServiceConnectionSuffix)' # Name of DevOps connection to use for $environ environment"
        $Pipeline += " $($environ)_EnvironmentName: '$environ'"
    
    }

    if (-not $PipelineVariablesToFile) {
        $Pipeline += $PipelineVariables[0]
        foreach ($e in $environment) {
            foreach ($var in $PipelineVariables[1..10000]) {
                $Pipeline += $var -Replace("#ENV#",$e)
            }
        }
    } 
    else {
        $Pipeline += ""
        $Pipeline += "# Variable file locations:"
        foreach ($e in $environment) {
            $Pipeline += "# $($e): $($RelativePipelineVariablesFileNames[$e])"
        }
    }

    $Pipeline += ""
    $Pipeline += "pool:"
    $Pipeline += " vmImage: windows-latest"
    $Pipeline += ""
    $Pipeline += "stages:"
    foreach ($e in $Environment) {
        $Pipeline += ""
        $Pipeline += "# $e"
        $Pipeline += " - stage: `${{variables.$($e)_EnvironmentName}}"
        $Pipeline += " displayName: '`${{variables.$($e)_EnvironmentName}}'"
    if ($PipelineVariablesToFile) {
        $Pipeline += " variables:"
        $Pipeline += " - template: $($RelativePipelineVariablesFileNames[$e])"
    }
        $Pipeline += " jobs:"
        $Pipeline += " - deployment: `${{variables.deployment}}"
        $Pipeline += " displayName: `${{variables.deploymentDisplayName}}"
        $Pipeline += " environment: `${{variables.$($e)_EnvironmentName}}"
        $Pipeline += " strategy:"
        $Pipeline += " runOnce: #rolling, canary are the other strategies that are supported"
        $Pipeline += " deploy:"
        $Pipeline += " steps:"
        $Pipeline += " - checkout: self"
        $Pipeline += " - template: ""/$TemplateRelativePath"" #Template paths should be relative to the file that does the including. For specific path use /path/to/template"
        $Pipeline += " parameters:"
        $Pipeline += " serviceConnection: `$`{{variables.$($e)_ServiceConnection}}"
        foreach ($param in $YAMLParameters) {
            $ParamValue = "`${{variables.$($param.VariableName)}}" -replace "#ENV#",$e
            $Pipeline += " $($param.Name): $ParamValue"
        }
    }
    #endregion BuildPipeline

    #Finally output the files
    try {
        if ($PipelineVariablesToFile) {
            foreach ($e in $Environment) {
                $PipelineVariablesHeader += "variables:"
                for ($x = 0 ; $x -lt $PipelineVariables.count ; $x++) {
                    $PipelineVariables[$x] = $PipelineVariables[$x] -replace("#ENV#",$e)
                }
                $PipelineVariables = $PipelineVariablesHeader + $PipelineVariables
        
                $PipelineVariables | Out-File -FilePath $PipelineVariablesFileNames[$e] -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
            }
        }

        $Pipeline | Out-File -FilePath $PipelineFileName -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
        $Template | Out-File -FilePath $TemplateFileName -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
    }
    catch {
        Write-Warning "Unable to write YAML files"
        Write-Warning $_.Exception.Message 
    }
}
Function Remove-OldModules {
[ CMDLetbinding()]
  Param ()

  $Modules = Get-ChildItem "$($ENV:ProgramFiles)\WindowsPowerShell\Modules" | Where-Object {$_.mode -match "^d"} 
  foreach ($Module in $Modules) {
    Write-Host "Removing old versions of $($Module.Name)"
    $Versions = Get-ChildItem -Path $Module.FullName | Sort-Object -Property Name -Descending
    $LastVersion =  $Versions | Select-Object -First 1
    $OldVersions =  @($Versions | Where-Object {$_ -ne $LastVersion})
    Remove-Module -Name $Module.Name -Force -Confirm:$false -ErrorAction SilentlyContinue
    if ($OldVersions.count -gt 0) {
        Write-Host "`t$($OldVersions.Count) old versions found"
        try {
            $OldVersions | Remove-Item -Recurse -Force -ErrorAction Stop
            Write-Host "`tRemoved"
        }
        catch {
            Write-Warning "`tCouldn't remove old versions: $($_.Exception.Message)"
        }
    }
    else {
        Write-Host "`tNo old versions found"
    }
  }
}
Function Set-AzTestSetup {
[   CMDLetbinding()]
    param(
        [Parameter(Mandatory =$true)]
        [String[]]$ResourceGroupName,
        [string]$Prefix,
        [int]$NumWinVMs,
        [int]$NumLinuxVMs,
        [string]$VMAutoshutdownTime,
        [string]$WorkspaceName,
        [string]$AutomationAccountName,
        [String]$Location,
        [string]$KeyvaultName,
        [switch]$Force,
        [switch]$Remove

    )
    foreach ($RG in $ResourceGroupName) {
        if (-not $PSBoundParameters.ContainsKey("Prefix")) {[string]$Prefix = $RG}
        if (-not $PSBoundParameters.ContainsKey("NumWinVMs")) {[int]$NumWinVMs = 2}
        if (-not $PSBoundParameters.ContainsKey("NumLinuxVMs")) {[int]$NumLinuxVMs = 0}
        if (-not $PSBoundParameters.ContainsKey("VMAutoshutdownTime")) {[string]$VMAutoshutdownTime = "2300"}
        if (-not $PSBoundParameters.ContainsKey("WorkspaceName")) {[string]$WorkspaceName = ($Prefix + "-workspace")}
        if (-not $PSBoundParameters.ContainsKey("AutomationAccountName")) {[string]$AutomationAccountName = ($Prefix + "-automation")}
        if (-not $PSBoundParameters.ContainsKey("Location")) {[String]$Location = "westeurope"}
        if (-not $PSBoundParameters.ContainsKey("KeyvaultName")) {[string]$KeyvaultName = ($Prefix + "-keyvault")}

        try {
            if (Get-AzResourceGroup -Name $RG -ErrorAction SilentlyContinue) {
                Write-Host "$RG exist"
                if ($Force -or $Remove) {
                    Write-Host "`tWill be deleted"
                    $WorkSpace = Get-AzOperationalInsightsWorkspace -ResourceGroupName $RG -Name $WorkspaceName -ErrorAction SilentlyContinue
                    $Keyvault = Get-AzKeyVault -VaultName $KeyvaultName -ResourceGroupName $RG -ErrorAction SilentlyContinue
                    if ($null -eq $Keyvault) {
                        $keyvault = Get-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Location $location 
                        if ($null -ne $Keyvault) {
                            Write-Host "`tDeleting $KeyvaultName"
                            $null = Remove-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Force -Confirm:$false -Location $Location
                        }
                    }
                    else {
                        Write-Host "`tDeleting $KeyvaultName"
                        Remove-AzKeyVault -VaultName $KeyvaultName -Force -Confirm:$false -Location $location
                        Start-Sleep -Seconds 1 
                        Remove-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Force -Confirm:$false -Location $location
                    }
                    if ($WorkSpace) {
                        Write-Host "`tDeleting Workspace"
                        $workspace | Remove-AzOperationalInsightsWorkspace -ForceDelete -Force -Confirm:$false 
                    }
                    Write-Host "`tDeleting Resourcegroup and contained resources"
                    Remove-AzResourceGroup -Name $RG -Force -Confirm:$false
                }
                else {
                    Write-Host "Nothing to do"
                    continue
                }
            }
            if ($Remove) {
                Write-Host "Remove specified. Exiting"
                continue 
            }

            Write-Host "Creating $RG"
            New-AzResourceGroup -Name $RG -Location $Location 
            Write-Host "Creating $AutomationAccountName"
            New-AzAutomationAccount -ResourceGroupName $RG -Name $AutomationAccountName -Location $Location -Plan Basic -AssignSystemIdentity 
            Write-Host "Creating $KeyvaultName"
            New-AzKeyVault -Name $KeyvaultName -ResourceGroupName $RG -Location $Location -EnabledForDeployment -EnabledForTemplateDeployment -EnabledForDiskEncryption -SoftDeleteRetentionInDays 7 -Sku Standard 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -UserPrincipalName "robert.eriksen_itera.com#EXT#@roedomlan.onmicrosoft.com" -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -ObjectId (Get-AzAutomationAccount -ResourceGroupName $RG -Name $AutomationAccountName).Identity.PrincipalId -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -ServicePrincipalName 04e7eb7d-da63-4c13-b5ba-04331145fdff -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Write-Host "Creating $WorkspaceName"
            New-azOperationalInsightsWorkspace -ResourceGroupName $RG -Name $WorkspaceName -Location $location -Sku pergb2018
            $VMCredentials = [pscredential]::new("roe",("Pokemon1234!" | ConvertTo-SecureString -AsPlainText -Force))
            # https://www.powershellgallery.com/packages/HannelsToolBox/1.4.0/Content/Functions%5CEnable-AzureVMAutoShutdown.ps1
            $ShutdownPolicy = @{}
            $ShutdownPolicy.Add('status', 'Enabled')
            $ShutdownPolicy.Add('taskType', 'ComputeVmShutdownTask')
            $ShutdownPolicy.Add('dailyRecurrence', @{'time'= "$VMAutoshutdownTime"})
            $ShutdownPolicy.Add('timeZoneId', "Romance Standard Time")
            $ShutdownPolicy.Add('notificationSettings', @{status='enabled'; timeInMinutes=30; emailRecipient="robert.eriksen@itera.com" })
            $VMPrefix = "$($RG[0])$($RG[-1])"
            if ($NumWinVMs -gt 0) {
                (1..$NumWinVMs) | ForEach-Object {
                    $VMName = ([string]"$($VMPrefix)-Win-$( $_)")
                    Write-Host "Deploying $VMName"
                    $null = New-AzVm -ResourceGroupName $RG -Name $VMName -Location $Location -Credential $VMCredentials -VirtualNetworkName "$($RG)-vnet" -SubnetName "$($RG)-Subnet" -SecurityGroupName "$($RG)-nsg" -PublicIpAddressName "$($VMName)-Public-ip" -OpenPorts 80,3389 -Size "Standard_DS1_v2" -Image Win2019Datacenter 
                    $vm = Get-AzVM -ResourceGroupName $RG -Name $VMName 
                    $rgName = $vm.ResourceGroupName
                    $vmName = $vm.Name
                    $location = $vm.Location
                    $VMResourceId = $VM.Id
                    $SubscriptionId = ($vm.Id).Split('/')[2]
                    $ScheduledShutdownResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$rgName/providers/microsoft.devtestlab/schedules/shutdown-computevm-$vmName"
                    if ($VMAutoshutdownTime -ne "Off") {
                        Write-Host "Setting autoshutdown: $VMAutoshutdownTime"
                        $ShutdownPolicy['targetResourceId'] = $VMResourceId
                        $null = New-azResource -Location $location -ResourceId $ScheduledShutdownResourceId -Properties $ShutdownPolicy -Force  
                    }
                }
            }
            if ($NumLinuxVMs -gt 0) {
                (1..$NumLinuxVMs) | ForEach-Object {
                    $VMName = ([string]"$($VMPrefix)-Lin-$($_)")
                    Write-Host "Deploying $VMName"
                    $null = New-AzVm -ResourceGroupName $RG -Name $VMName -Location $Location -Credential $VMCredentials -VirtualNetworkName "$($RG)-vnet" -SubnetName "$($RG)-Subnet" -SecurityGroupName "$($RG)-nsg" -PublicIpAddressName "$($VMName)-Public-ip" -OpenPorts 80,3389 -Size "Standard_DS1_v2" -Image UbuntuLTS
                    $vm = Get-AzVM -ResourceGroupName $RG -Name $VMName 
                    $rgName = $vm.ResourceGroupName
                    $vmName = $vm.Name
                    $location = $vm.Location
                    $VMResourceId = $VM.Id
                    $SubscriptionId = ($vm.Id).Split('/')[2]
                    $ScheduledShutdownResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$rgName/providers/microsoft.devtestlab/schedules/shutdown-computevm-$vmName"
                    if ($VMAutoshutdownTime -ne "Off") {
                        Write-Host "Setting autoshutdown: $VMAutoshutdownTime"
                        $ShutdownPolicy['targetResourceId'] = $VMResourceId
                        $null = New-azResource -Location $location -ResourceId $ScheduledShutdownResourceId -Properties $ShutdownPolicy -Force  
                    }
                }
            }
        }
        catch {
            throw $_ 
        }
    }
}
Function Set-SystemVariables {
    #Collect all variables and store them, so userdefined variables can be easily cleared without restarting the PowerShell session
    New-Variable -Name 'SysVars' -Scope 'Global' -Force
    $global:SysVars = Get-Variable | Select-Object -ExpandProperty Name
    $global:SysVars += 'SysVars'
}