Common.psm1

#########################################################################################
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# Common/Shared Functions, Types, and Variables
#
#########################################################################################

#requires -runasadministrator

#region Enums

Add-Type -TypeDefinition @"
using System.Management.Automation;
 
public enum InstallState
{
    NotInstalled,
    Installing,
    InstallFailed,
    Updating,
    UpdateFailed,
    Uninstalling,
    UninstallFailed,
    Installed
};
 
public enum DeploymentType
{
    None,
    SingleNode,
    MultiNode
};
 
public enum LoadBalancerType
{
    unstacked_haproxy,
    stacked_kube_vip
};
 
public enum OsType
{
    Linux,
    Windows
};
 
public enum VmSize
{
    Default,
    Standard_A2_v2,
    Standard_A4_v2,
    Standard_D2s_v3,
    Standard_D4s_v3,
    Standard_D8s_v3,
    Standard_D16s_v3,
    Standard_D32s_v3,
    Standard_DS2_v2,
    Standard_DS3_v2,
    Standard_DS4_v2,
    Standard_DS5_v2,
    Standard_DS13_v2,
    Standard_K8S_v1,
    Standard_K8S2_v1,
    Standard_K8S3_v1,
    Standard_NK6,
    Standard_NV6,
    Standard_NV12
};
 
public enum LoadBalancerSku
{
    HAProxy,
    None,
    KubeVIP,
    SDNLoadBalancer
};
 
public class LoadBalancerSettings
{
    public string Name;
    public LoadBalancerSku LoadBalancerSku;
    public LoadBalancerSku ServicesLoadBalancerSku;
    public VmSize VmSize;
    public int LoadBalancerCount;
 
    public LoadBalancerSettings (
        string Name,
        LoadBalancerSku LoadBalancerSku,
        LoadBalancerSku ServicesLoadBalancerSku,
        VmSize VmSize,
        int LoadBalancerCount
    )
    {
        this.Name = Name;
        this.LoadBalancerSku = LoadBalancerSku;
        this.ServicesLoadBalancerSku = ServicesLoadBalancerSku;
        this.VmSize = VmSize;
        this.LoadBalancerCount = LoadBalancerCount;
    }
}
 
public class VirtualNetwork
{
    public string Name;
    public string VswitchName;
    public string IpAddressPrefix;
    public string Gateway;
    public string[] DnsServers;
    public string MacPoolName;
    public int Vlanid;
    public string VipPoolStart;
    public string VipPoolEnd;
    public string K8snodeIPPoolStart;
    public string K8snodeIPPoolEnd;
 
    public VirtualNetwork (
        string Name,
        string VswitchName,
        string IpAddressPrefix,
        string Gateway,
        string[] DnsServers,
        string MacPoolName,
        int Vlanid,
        string VipPoolStart,
        string VipPoolEnd,
        string K8snodeIPPoolStart,
        string K8snodeIPPoolEnd
 
    )
    {
        this.Name = Name;
        this.VswitchName = VswitchName;
        this.IpAddressPrefix = IpAddressPrefix;
        this.Gateway = Gateway;
        this.DnsServers = DnsServers;
        this.MacPoolName = MacPoolName;
        this.Vlanid = Vlanid;
        this.VipPoolStart = VipPoolStart;
        this.VipPoolEnd = VipPoolEnd;
        this.K8snodeIPPoolStart = K8snodeIPPoolStart;
        this.K8snodeIPPoolEnd = K8snodeIPPoolEnd;
 
    }
    public override string ToString()
    {
        return string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}\n{9}",
            this.Name,
            this.IpAddressPrefix,
            this.Gateway,
            this.DnsServers != null ? string.Join(",", this.DnsServers) : "",
            this.MacPoolName,
            this.Vlanid,
            this.VipPoolStart,
            this.VipPoolEnd,
            this.K8snodeIPPoolStart,
            this.K8snodeIPPoolEnd);
    }
}
 
public class ProxySettings
{
    public string Name;
    public string HTTP;
    public string HTTPS;
    public string NoProxy;
    public string CertFile;
    public PSCredential Credential;
 
    public ProxySettings (
        PSCredential Credential,
        string Name = "",
        string HTTP = "",
        string HTTPS = "",
        string NoProxy = "",
        string CertFile = ""
    )
    {
        this.Name = Name;
        this.HTTP = HTTP;
        this.HTTPS = HTTPS;
        this.NoProxy = NoProxy;
        this.CertFile = CertFile;
        this.Credential = Credential;
    }
    public override string ToString()
    {
        return string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}",
            this.Name,
            this.HTTP,
            this.HTTPS,
            this.NoProxy,
            this.CertFile,
            this.Credential.ToString());
    }
}
 
public class ContainerRegistry
{
    public string Server;
    public PSCredential Credential;
 
    public ContainerRegistry (
        PSCredential Credential,
        string Server = ""
    )
    {
        this.Server = Server;
        this.Credential = Credential;
    }
    public override string ToString()
    {
        return string.Format("{0}\n{1}",
            this.Server,
            this.Credential.ToString());
    }
}
 
public class VipPoolSettings
{
    public string Name;
    public string VipPoolStart;
    public string VipPoolEnd;
 
    public VipPoolSettings (
        string Name,
        string VipPoolStart,
        string VipPoolEnd
    )
    {
        this.Name = Name;
        this.VipPoolStart = VipPoolStart;
        this.VipPoolEnd = VipPoolEnd;
 
    }
    public override string ToString()
    {
        return string.Format("{0}\n{1}\n{2}",
            this.Name,
            this.VipPoolStart,
            this.VipPoolEnd);
    }
}
 
"@


Add-Type -Language CSharp -ReferencedAssemblies "System.Numerics.dll" @"
using System;
using System.Numerics;
using System.Net;
 
namespace AKSHCI
{
    public class IPRange
    {
        public IPAddress start, end;
        public override string ToString()
        {
            return string.Format("{0} - {1}", start, end);
        }
    }
    public static class IPUtilities
    {
        public static int CompareIpAddresses(IPAddress ipAddress1, IPAddress ipAddress2)
        {
            byte[] ipAddress1Bytes = ipAddress1.GetAddressBytes();
            byte[] ipAddress2Bytes = ipAddress2.GetAddressBytes();
            Array.Reverse(ipAddress1Bytes);
            Array.Reverse(ipAddress2Bytes);
            Array.Resize<byte>(ref ipAddress1Bytes, 5); // Make sure the first byte is a 0 so BigInterger considers the number as unsigned
            Array.Resize<byte>(ref ipAddress2Bytes, 5); // Make sure the first byte is a 0 so BigInterger considers the number as unsigned
            BigInteger ipAddress1BigInt = new BigInteger(ipAddress1Bytes);
            BigInteger ipAddress2BigInt = new BigInteger(ipAddress2Bytes);
            return BigInteger.Compare(ipAddress1BigInt, ipAddress2BigInt);
        }
         
        public static IPAddress GetLastIpInCidr(IPAddress ipAddressStr, int prefixLength)
        {
            BigInteger fullMask = new BigInteger(0xFFFFFFFF);
            BigInteger mask = ((fullMask >> (prefixLength)) & fullMask);
            byte[] endAddress = mask.ToByteArray();
            Array.Resize<byte>(ref endAddress, 4);
            Array.Reverse(endAddress);
            byte[] ipAddress = ipAddressStr.GetAddressBytes();
            
            if(ipAddress.Length != endAddress.Length)
            {
                throw new System.InvalidOperationException("Address and prefix length are both expected to be IPv4 (" + ipAddress.Length + " != " + endAddress.Length + ")");
            }
             
            for(int i = 0; i < ipAddress.Length; i++)
            {
                endAddress[i] = (byte) (endAddress[i] | ipAddress[i]);
            }
 
            return new IPAddress(endAddress);
        }
 
        public static IPAddress ToIPAddress(BigInteger bi)
        {
            var bytes = bi.ToByteArray();
            Array.Resize<byte>(ref bytes, 4);
            Array.Reverse(bytes);
            return new IPAddress(bytes);
        }
 
        public static BigInteger ToBigInteger(IPAddress ip)
        {
            var ipBytes = ip.GetAddressBytes();
            Array.Reverse(ipBytes);
            Array.Resize<byte>(ref ipBytes, ipBytes.Length + 1); // Make sure the first byte is a 0 so BigInterger considers the number as unsigned
            return new BigInteger(ipBytes);
        }
 
        public static void ToRange(string CIDR, out IPAddress start, out IPAddress end)
        {
            var s = CIDR.Split('/');
            start = IPAddress.Parse(s[0]);
            var prefixLength = int.Parse(s[1]);
            end = AKSHCI.IPUtilities.GetLastIpInCidr(start, prefixLength);
        }
 
        public static bool ValidateRange(string rangeStart, string rangeEnd)
        {
            var start = IPAddress.Parse(rangeStart);
            var startBI = ToBigInteger(start);
 
            var end = IPAddress.Parse(rangeEnd);
            var endBI = ToBigInteger(end);
 
            if (endBI < startBI)
            {
                return false;
            }
            return true;
        }
 
        public static bool ValidateIPInCIDR(string ip, string CIDR)
        {
            var ipaddress = IPAddress.Parse(ip);
            var ipBI = ToBigInteger(ipaddress);
 
            IPAddress cidrStart, cidrEnd;
            ToRange(CIDR, out cidrStart, out cidrEnd);
            var cidrStartBI = ToBigInteger(cidrStart);
            var cidrEndBI = ToBigInteger(cidrEnd);
 
            if ((ipBI >= cidrStartBI) && (ipBI <= cidrEndBI))
            {
                return true;
            }
            return false;
 
        }
        public static bool ValidateRangeInCIDR(string rangeStart, string rangeEnd, string CIDR)
        {
            if (ValidateIPInCIDR(rangeStart, CIDR) && ValidateIPInCIDR(rangeEnd, CIDR))
            {
                return true;
            }
            return false;
        }
 
        public static IPRange[] GetVMIPPool(string vippoolStart, string vippoolEnd, string CIDR)
        {
            var start = IPAddress.Parse(vippoolStart);
            var startBI = ToBigInteger(start);
 
            var end = IPAddress.Parse(vippoolEnd);
            var endBI = ToBigInteger(end);
 
            IPAddress cidrStart, cidrEnd;
            ToRange(CIDR, out cidrStart, out cidrEnd);
            var cidrStartBI = ToBigInteger(cidrStart);
            var cidrEndBI = ToBigInteger(cidrEnd);
 
            if ((startBI == cidrStartBI) && (endBI == cidrEndBI))
            {
                throw new Exception(string.Format("The VIP pool range ({0} - {1}) is too large. There is no space to allocate IP addresses for VM's. Try decreasing the size of the VIP pool.", vippoolStart, vippoolEnd));
            }
 
            if (startBI == cidrStartBI)
            {
                var ippoolstart = ToIPAddress(endBI + 1);
                var ippoolend = ToIPAddress(cidrEndBI);
                return new IPRange[] { new IPRange{ start = ippoolstart, end = ippoolend } };
            }
            else if (endBI == cidrEndBI)
            {
                var ippoolstart = ToIPAddress(cidrStartBI);
                var ippoolend = ToIPAddress(startBI - 1);
                return new IPRange[] { new IPRange { start = ippoolstart, end = ippoolend } };
            }
            else
            {
                var ippool1start = ToIPAddress(cidrStartBI);
                var ippool1end = ToIPAddress(startBI - 1);
                var ippool2start = ToIPAddress(endBI + 1);
                var ippool2end = ToIPAddress(cidrEndBI);
                return new IPRange[] { new IPRange { start = ippool1start, end = ippool1end }, new IPRange { start = ippool2start, end = ippool2end } };
            }
        }
        //This function checks if an IP address is part of an IP pool.
        //Example : IP is 10.10.10.1 and IP pool is 10.10.0.0 to 10.10.255.255 -> Return True -> overlap.
        public static bool CheckIPInIPPool(string ippoolStart, string ippoolEnd, string ipAddressStr)
        {
            //Get BigInteger for pool's Start IP
            var start = IPAddress.Parse(ippoolStart);
            var startBI = ToBigInteger(start);
 
            //Get BigInteger for pool's End IP
            var end = IPAddress.Parse(ippoolEnd);
            var endBI = ToBigInteger(end);
 
            //Get BigInteger for CIDR IP
            var ipAddr = IPAddress.Parse(ipAddressStr);
            var ipAddrBI = ToBigInteger(ipAddr);
 
            if ((ipAddrBI <= endBI) && (ipAddrBI >= startBI))
            {
                //IP is present in IP Pool
                return true;
            }
            return false;
        }
    }
}
"@
;

#endregion

#region Module constants

$global:AksHciModule = "AksHci"
$global:MocModule = "Moc"
$global:KvaModule = "Kva"
$global:DownloadModule = "DownloadSdk"
$global:CommonModule = "Common"

$global:configurationKeys = @{
    $global:AksHciModule =  "HKLM:SOFTWARE\Microsoft\${global:AksHciModule}PS";
    $global:MocModule =  "HKLM:SOFTWARE\Microsoft\${global:MocModule}PS";
    $global:KvaModule =  "HKLM:SOFTWARE\Microsoft\${global:KvaModule}PS";
}

$global:repositoryName        = "PSGallery"
$global:repositoryNamePreview = "PSGallery"
$global:repositoryUser        = ""
$global:repositoryPass        = ""
$global:loadBalancerTypeStr = @('unstacked-haproxy','unmanaged','stacked-kube-vip','sdn-load-balancer')


#endregion

#region VM size definitions

$global:vmSizeDefinitions =
@(
    # Name, CPU, MemoryGB
    ([VmSize]::Default, "4", "4"),
    ([VmSize]::Standard_A2_v2, "2", "4"),
    ([VmSize]::Standard_A4_v2, "4", "8"),
    ([VmSize]::Standard_D2s_v3, "2", "8"),
    ([VmSize]::Standard_D4s_v3, "4", "16"),
    ([VmSize]::Standard_D8s_v3, "8", "32"),
    ([VmSize]::Standard_D16s_v3, "16", "64"),
    ([VmSize]::Standard_D32s_v3, "32", "128"),
    ([VmSize]::Standard_DS2_v2, "2", "7"),
    ([VmSize]::Standard_DS3_v2, "2", "14"),
    ([VmSize]::Standard_DS4_v2, "8", "28"),
    ([VmSize]::Standard_DS5_v2, "16", "56"),
    ([VmSize]::Standard_DS13_v2, "8", "56"),
    ([VmSize]::Standard_K8S_v1, "4", "2"),
    ([VmSize]::Standard_K8S2_v1, "2", "2"),
    ([VmSize]::Standard_K8S3_v1, "4", "6")
    # Dont expose GPU size until its supported
    #([VmSize]::Standard_NK6, "6", "12"),
    #([VmSize]::Standard_NV6, "6", "64"),
    #([VmSize]::Standard_NV12, "12", "128")
)

#endregion

#region Pod names and selectors

$global:managementPods =
@(
    ("Cloud Operator", "cloudop-system", "control-plane=controller-manager"),
    ("Cluster API core", "capi-system", "cluster.x-k8s.io/provider=cluster-api"),
    ("Bootstrap kubeadm", "capi-kubeadm-bootstrap-system", "cluster.x-k8s.io/provider=bootstrap-kubeadm"),
    ("Control Plane kubeadm", "capi-kubeadm-control-plane-system", "cluster.x-k8s.io/provider=control-plane-kubeadm"),
    ("Cluster API core Webhook", "capi-webhook-system", "cluster.x-k8s.io/provider=cluster-api"),
    ("Bootstrap kubeadm Webhook", "capi-webhook-system", "cluster.x-k8s.io/provider=bootstrap-kubeadm"),
    ("Control Plane kubeadm Webhook", "capi-webhook-system", "cluster.x-k8s.io/provider=control-plane-kubeadm"),
    ("AzureStackHCI Provider", "caph-system", "cluster.x-k8s.io/provider=infrastructure-azurestackhci"),
    ("AzureStackHCI Provider Webhook", "capi-webhook-system", "cluster.x-k8s.io/provider=infrastructure-azurestackhci")
)

#endregion

# -ErrorAction:SilentlyContinue
Import-LocalizedData -BindingVariable "GenericLocMessage" -FileName commonLocalizationMessages

#region Classes

#region to capture CustomException error details

class CustomException: Exception {
    [bool] $UserErrorFlag

   CustomException([string] $message, [bool] $UserErrorFlag) : base($message) {
        $this.UserErrorFlag = $UserErrorFlag
     }
   CustomException() {}
}

#endregion

class NetworkPlugin {
    
    [string] $Name
    static [string] $Default = "calico"

    NetworkPlugin(
        [string] $name
    )
    {
        $curatedName = $name.ToLower()
        if ($curatedName -ne "flannel" -and $curatedName -ne "calico" -and $curatedName -ne "cilium")
        {
            throw [CustomException]::new("Invalid CNI '$curatedName'. The only supported CNIs are 'flannel', 'calico' and 'cilium'", $true)
        }
        $this.Name = $curatedName
    }

    NetworkPlugin()
    {
        $this.Name = [NetworkPlugin]::Default
    }
}
#endregion

#region Script Constants
$global:installDirectoryName           = "AksHci"
$global:workingDirectoryName           = "AksHci"
$global:imageDirectoryName             = "AksHciImageStore"
$global:yamlDirectoryName              = "yaml"
$global:cloudConfigDirectoryName       = "wssdcloudagent"
$global:nodeConfigDirectoryName        = "wssdagent"

$global:installDirectory               = $($env:ProgramFiles + "\" + $global:installDirectoryName)
$global:defaultworkingDir              = $($env:SystemDrive + "\" + $global:workingDirectoryName)
$global:defaultStagingShare            = ""

$global:nodeAgentBinary                = "wssdagent.exe"
$global:cloudAgentBinary               = "wssdcloudagent.exe"
$global:nodeCtlBinary                  = "nodectl.exe"
$global:cloudCtlBinary                 = "mocctl.exe"
$global:kubectlBinary                  = "kubectl.exe"
$global:kvactlBinary                   = "kvactl.exe"
$global:cloudOperatorYaml              = "cloud-operator.yaml"

$global:nodeAgentFullPath              = [io.Path]::Combine($global:installDirectory, $global:nodeAgentBinary)
$global:cloudAgentFullPath             = [io.Path]::Combine($global:installDirectory, $global:cloudAgentBinary)
$global:nodeCtlFullPath                = [io.Path]::Combine($global:installDirectory, $global:nodeCtlBinary)
$global:cloudCtlFullPath               = [io.Path]::Combine($global:installDirectory, $global:cloudCtlBinary)
$global:kubeCtlFullPath                = [io.Path]::Combine($global:installDirectory, $global:kubectlBinary)
$global:kvaCtlFullPath                 = [io.Path]::Combine($global:installDirectory, $global:kvactlBinary)

$script:psConfigKeyName                = "psconfig"
$script:psConfigJson                   = "psconfig.json"
$global:psConfigDirectoryRoot          = $($env:USERPROFILE)
$global:mocMetadataRoot                = $($env:USERPROFILE + "\.wssd")
$global:mocMetadataDirectory           = [io.Path]::Combine($global:mocMetadataRoot, "mocctl" )
$global:kvaMetadataDirectory           = [io.Path]::Combine($global:mocMetadataRoot, "kvactl" )
$global:accessFileLocation             = [io.Path]::Combine($global:mocMetadataDirectory, "cloudconfig")
$global:accessFileDir                  = "CloudCfg"
$global:accessFileDirMoc               = "mocctl"
$global:accessFileDirKva               = "kvactl"
$global:accessFileName                 = "cloudconfig"
$global:multiAdminRelease              = "1.0.13.10907"

$global:defaultCloudConfigLocation     = $($env:SystemDrive + "\programdata\" + $global:cloudConfigDirectoryName)
$global:defaultNodeConfigLocation      = $($env:SystemDrive + "\programdata\" + $global:nodeConfigDirectoryName)

$global:defaultTargetK8Version         = "v1.24.6"
$global:defaultMgmtReplicas            = 1

$global:defaultMgmtControlPlaneVmSize  = [VmSize]::Standard_A4_v2
$global:defaultControlPlaneVmSize      = [VmSize]::Standard_A4_v2
$global:defaultLoadBalancerVmSize      = [VmSize]::Standard_A4_v2
$global:defaultWorkerVmSize            = [VmSize]::Standard_K8S3_v1

$global:defaultNodePoolName            = "nodepool1"
$global:defaultWorkerNodeCount         = 1
$global:defaultWorkerNodeOS            = [OsType]::Linux

$global:defaultNodeAgentPort       = 45000
$global:defaultNodeAuthorizerPort  = 45001

$global:defaultCloudAgentPort      = 55000
$global:defaultCloudAuthorizerPort = 65000

$global:defaultVipPoolName    = "clusterVipPool"
$global:defaultMacPoolStart   = ""
$global:defaultMacPoolEnd     = ""
$global:defaultVlanID         = 0

$global:failoverCluster         = $null
$global:cloudAgentAppName       = "ca"

$global:cloudName               = "moc-cloud"
$global:defaultCloudLocation    = "MocLocation"
$global:cloudGroupPrefix        = "clustergroup"
$global:cloudStorageContainer   = "MocStorageContainer"
$global:cloudMacPool            = "MocMacPool"

$global:defaultCreateAutoConfigContainers    = $true

$global:defaultPodCidr         = "10.244.0.0/16"
$global:mgmtClusterCidr        = "10.200.0.0/16"
$global:mgmtControlPlaneCidr   = "10.240.0.0/24"

$global:workloadPodCidr        = "10.244.0.0/16"
$global:workloadServiceCidr    = "10.96.0.0/12"

$global:defaultProxyExemptions = "localhost,127.0.0.1,.svc,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
$global:credentialKey          = (24,246,163,38,50,244,215,218,223,10,65,98,19,1,149,106,190,141,144,180,157,135,211,143)

$global:clusterNameRegex       = "^[a-z0-9][a-z0-9-]*[a-z0-9]$"

$global:defaultLogLineCount = 500000

$global:operatorTokenValidity = 90
$global:addonTokenValidity = 90
$global:certificateValidityFactor = 1.0
$global:caCertificateValidityFactor = 1.0
$global:nodeCertificateValidityFactor = 1.0

$global:caCertRotationThreshold      = 90

# Temporary until cross-platform signing is available
$global:expectedAuthResponse = @{
    "Status" = "Valid";
    "SignatureType" = "Authenticode";
    "StatusMessage" = ""
}

$global:cloudAgentTimeout = 300

#endregion

#region User Configuration and Defaults

$global:autoScalerProfileConfigToKvaYamlKeys =
@{
    "min-node-count" = "minnodecount";
    "max-node-count" = "maxnodecount";
    "max-nodes-total" = "maxnodestotal";
    "scale-down-enabled" = "scaledownenabled";
    "scan-interval" = "scaninterval";
    "scale-down-delay-after-add" = "scaledowndelayafteradd";
    "scale-down-delay-after-delete" = "scaledowndelayafterdelete";
    "scale-down-delay-after-failure" = "scaledowndelayafterfailure";
    "scale-down-unneeded-time" = "scaledownunneededtime";
    "scale-down-unready-time" = "scaledownunreadytime";
    "scale-down-utilization-threshold" = "scaledownutilizationthreshold";
    "max-graceful-termination-sec" = "maxgracefulterminationsec";
    "balance-similar-node-groups" = "balancesimilarnodegroups";
    "expander" = "expander";
    "skip-nodes-with-local-storage" = "skipnodeswithlocalstorage";
    "skip-nodes-with-system-pods" = "skipnodeswithsystempods";
    "max-empty-bulk-delete" = "maxemptybulkdelete";
    "new-pod-scale-up-delay" = "newpodscaleupdelay";
    "max-total-unready-percentage" = "maxtotalunreadypercentage";
    "max-node-provision-time" = "maxnodeprovisiontime";
    "ok-total-unready-count" = "oktotalunreadycount"
}

#endregion

#region Configuration Functions

function Set-VNetConfiguration
{
    param (
        [Parameter(Mandatory=$true)]
        [String] $module,
        [Parameter(Mandatory=$true)]
        [VirtualNetwork] $vnet
    )
    Set-ConfigurationValue -name "vnetName" -value $vnet.Name -module $module
    Set-ConfigurationValue -name "vswitchName" -value $vnet.VswitchName -module $module
    Set-ConfigurationValue -name "ipaddressprefix" -value $vnet.IpAddressPrefix -module $module
    Set-ConfigurationValue -name "gateway" -value $vnet.Gateway -module $module
    Set-ConfigurationValue -name "dnsservers" -value ($vnet.DnsServers -join ",") -module $module
    Set-ConfigurationValue -name "macpoolname" -value $vnet.MacPoolName -module $module
    Set-ConfigurationValue -name "vlanid" -value $vnet.Vlanid -module $module
    Set-ConfigurationValue -name "vnetvippoolstart" -value $vnet.VipPoolStart -module $module
    Set-ConfigurationValue -name "vnetvippoolend" -value $vnet.VipPoolEnd -module $module
    Set-ConfigurationValue -name "k8snodeippoolstart" -value $vnet.K8snodeIPPoolStart -module $module
    Set-ConfigurationValue -name "k8snodeippoolend" -value $vnet.K8snodeIPPoolEnd -module $module        

}

function Get-VNetConfiguration
{
    param (
        [Parameter(Mandatory=$true)]
        [String] $module
    )
    # vnetName and vswitchName are mandatory fields.
    # So if both are empty we can assume the vnet was not set.
    $vnet_name = Get-ConfigurationValue -name "vnetName" -module $module
    $vnet_vswitchname = Get-ConfigurationValue -name "vswitchName" -module $module
    
    $isvnetEmpty = ([string]::IsNullOrWhiteSpace($vnet_name) -and [string]::IsNullOrWhiteSpace($vnet_vswitchname))
    if ($isvnetEmpty) {
        return $null
    }
    $vnet_ipaddressprefix = Get-ConfigurationValue -name "ipaddressprefix" -module $module
    $vnet_gateway = Get-ConfigurationValue -name "gateway" -module $module
    $vnet_dnsservers = (Get-ConfigurationValue -name "dnsservers" -module $module) -split ","
    $vnet_macpoolname = Get-ConfigurationValue -name "macpoolname" -module $module
    $vnet_vlanid = Get-ConfigurationValue -name "vlanid" -module $module
    $vnet_vippoolstart = Get-ConfigurationValue -name "vnetvippoolstart" -module $module
    $vnet_vippoolend = Get-ConfigurationValue -name "vnetvippoolend" -module $module
    $vnet_k8snodeippoolstart = Get-ConfigurationValue -name "k8snodeippoolstart" -module $module
    $vnet_k8snodeippoolend = Get-ConfigurationValue -name "k8snodeippoolend" -module $module

    return [VirtualNetwork]::new($vnet_name, $vnet_vswitchname, $vnet_ipaddressprefix, $vnet_gateway, $vnet_dnsservers, $vnet_macpoolname, $vnet_vlanid, $vnet_vippoolstart, $vnet_vippoolend, $vnet_k8snodeippoolstart, $vnet_k8snodeippoolend)
}

function New-VirtualNetwork
{
    <#
    .DESCRIPTION
        A wrapper around [VirutalNetwork]::new that Validates parameters before returning a VirtualNetwork object
 
    .PARAMETER name
        The name of the vnet
 
    .PARAMETER vswitchName
        The name of the vswitch
 
    .PARAMETER MacPoolName
        The name of the mac pool
 
    .PARAMETER vlanID
        The VLAN ID for the vnet
 
    .PARAMETER ipaddressprefix
        The address prefix to use for static IP assignment
 
    .PARAMETER gateway
        The gateway to use when using static IP
 
    .PARAMETER dnsservers
        The dnsservers to use when using static IP
 
    .PARAMETER vippoolstart
        The starting ip address to use for the vip pool.
        The vip pool addresses will be used by the k8s API server and k8s services'
 
    .PARAMETER vippoolend
        The ending ip address to use for the vip pool.
        The vip pool addresses will be used by the k8s API server and k8s services
 
    .PARAMETER k8snodeippoolstart
        The starting ip address to use for VM's in the cluster.
 
    .PARAMETER k8snodeippoolend
        The ending ip address to use for VM's in the cluster.
 
    .OUTPUTS
        VirtualNetwork object
 
    .EXAMPLE
        New-VirtualNetwork -name External -vswitchname External -vippoolstart 172.16.0.0 -vippoolend 172.16.0.240
         
    .EXAMPLE
        New-VirtualNetwork -name "defaultswitch" -vswitchname "Default Switch" -ipaddressprefix 172.16.0.0/24 -gateway 172.16.0.1 -dnsservers 4.4.4.4, 8.8.8.8 -vippoolstart 172.16.0.0 -vippoolend 172.16.0.240
    #>


    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $name,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $vswitchName,

        [Parameter(Mandatory=$false)]
        [String] $MacPoolName = $global:cloudMacPool,

        [Parameter(Mandatory=$false)]
        [int] $vlanID = $global:defaultVlanID,

        [Parameter(Mandatory=$false)]
        [String] $ipaddressprefix,

        [Parameter(Mandatory=$false)]
        [String] $gateway,

        [Parameter(Mandatory=$false)]
        [String[]] $dnsservers,

        [Parameter(Mandatory=$false)]
        [String] $vippoolstart,

        [Parameter(Mandatory=$false)]
        [String] $vippoolend,

        [Parameter(Mandatory=$false)]
        [String] $k8snodeippoolstart,

        [Parameter(Mandatory=$false)]
        [String] $k8snodeippoolend
    )

    Test-ValidNetworkName -Name $name | Out-Null

    if ($dnsservers)
    {
        foreach ($dns in $dnsservers)
        {
            try
            {
                Test-ValidEndpoint -endpoint $dns
            }
            catch
            {
                throw [CustomException]::new(($([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_ip_list , $dnsservers))), $true)
            }
        }
    }

    if ($ipaddressprefix -or $gateway -or $dnsservers -or $k8snodeippoolstart -or $k8snodeippoolend)
    {
        if ((-not $ipaddressprefix) -or (-not $gateway) -or (-not $dnsservers) -or (-not $k8snodeippoolstart) -or (-not $k8snodeippoolend))
        {
            throw [CustomException]::new(($($GenericLocMessage.comm_ip_param_missing)), $true)
        }
    }

    if ($ipaddressprefix)
    {
        Test-ValidCIDR -CIDR $ipaddressprefix
    }

    if ((-not [string]::IsNullOrWhiteSpace($vippoolstart)) -or (-not [string]::IsNullOrWhiteSpace($vippoolend)))
    {
        Test-ValidPool -PoolStart $vippoolstart -PoolEnd $vippoolend -CIDR $ipaddressprefix
    }

    if ($k8snodeippoolstart -and $k8snodeippoolend)
    {
        Test-ValidPool -PoolStart $k8snodeippoolstart -PoolEnd $k8snodeippoolend -CIDR $ipaddressprefix
    }
    
    return [VirtualNetwork]::new($name, $vswitchname, $ipaddressprefix, $gateway, $dnsservers, $MacPoolName, $vlanID, $vippoolstart, $vippoolend, $k8snodeippoolstart, $k8snodeippoolend)
}


#region configuration
function Save-ConfigurationDirectory
{
    <#
    .DESCRIPTION
        Saves the workingDir of configuration in registry.
        Handles multinode as well.
     
    .PARAMETER WorkingDir
        WorkingDir to be persisted
     
    .PARAMETER moduleName
        Name of the module
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName,
        [Parameter(Mandatory=$true)]
        [String] $WorkingDir
    )

    $configDir = [io.Path]::Combine($WorkingDir, "." + $moduleName)
    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_save_config_dir , $configDir)) -moduleName $moduleName

    if (Test-MultiNodeDeployment)
    {
        # *. If Multinode, replicate this across all nodes
        Get-ClusterNode -ErrorAction Stop | ForEach-Object {
            Invoke-Command -ComputerName $_.Name -ScriptBlock  {
                $regPath = $args[0]
                $regKey = $args[1]
                $regValue = $args[2]

                if (!(Test-Path ($regPath)))
                {
                    New-Item -Path $regPath | Out-Null
                }
                Set-ItemProperty -Path $regPath -Name $regKey  -Value $regValue  -Force | Out-Null
            } -ArgumentList @($global:configurationKeys[$moduleName], $script:psConfigKeyName, $configDir)
        }
    }
    else 
    {
        # *. If Standalone, store it locally
        if (!(Test-Path ($global:configurationKeys[$moduleName])))
        {
            New-Item -Path $global:configurationKeys[$moduleName] | Out-Null
        }

        Set-ItemProperty -Path $global:configurationKeys[$moduleName] -Name $script:psConfigKeyName  -Value $configDir -Force | Out-Null
    }
}

function Set-SecurePermissionFolder
{
    <#
    .DESCRIPTION
        Initialize folder with appropriate permissions
     
    .PARAMETER Path
        path of config folder.
    #>

    param (
        [parameter(Mandatory=$true)]
        [string]$Path

    )
    
    $acl = Get-Acl $Path
    $acl.SetAccessRuleProtection($true,$false)
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("BUILTIN\Administrators","FullControl","ContainerInherit,ObjectInherit", "None", "Allow")
    $acl.SetAccessRule($accessRule)
    $acl | Set-Acl $Path
}

function Set-SecurePermissionFile
{
    <#
    .DESCRIPTION
        Initialize file with appropriate permissions
     
    .PARAMETER Path
        path of file.
    #>

    param (
        [parameter(Mandatory=$true)]
        [string]$Path

    )
    
    # ACL the yaml so that it is only readable by administrator
    $acl = Get-Acl $Path
    $acl.SetAccessRuleProtection($true,$false)
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("BUILTIN\Administrators","FullControl","Allow")
    $acl.SetAccessRule($accessRule)
    $acl | Set-Acl $Path
}


function Reset-ConfigurationDirectory
{
    <#
    .DESCRIPTION
        Cleanup workingDir info in registry that has been saved
     
    .PARAMETER moduleName
        Name of the module
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    Write-Status $($GenericLocMessage.comm_reset_config_dir) -moduleName $moduleName
    if (Test-MultiNodeDeployment)
    {
        # *. If Multinode, remove this across all nodes
        Get-ClusterNode -ErrorAction Stop | ForEach-Object {
            Invoke-Command -ComputerName $_.Name -ScriptBlock  {
                $regPath = $args[0]
                Remove-Item -Path $regPath -Force -ErrorAction SilentlyContinue | Out-Null
            } -ArgumentList @($global:configurationKeys[$moduleName])
        }
    }
    else 
    {
        # *. If Standalone, remove it locally
        Remove-Item -Path $global:configurationKeys[$moduleName] -Force -ErrorAction SilentlyContinue | Out-Null
    }
}
function Get-ConfigurationDirectory
{
    <#
    .DESCRIPTION
        Gets the Working Directory of configuration from the registry
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    # 1. If the psconfig path is available, try to use it.
    $regVal = Get-ItemPropertyValue -Path $global:configurationKeys[$moduleName] -Name $script:psConfigKeyName  -ErrorAction SilentlyContinue
    if ($regVal)
    {
        return $regVal
    }
    # 2. If not, use the default path
    return [io.Path]::Combine($global:psConfigDirectoryRoot, "." + $moduleName)
}

function Get-ConfigurationFile
{
    <#
    .DESCRIPTION
        Get the configuration file to be used for persisting configurations
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    $configFile = [io.Path]::Combine((Get-ConfigurationDirectory -moduleName $moduleName), $script:psConfigJson)
    # Write-Status "Configuration file for $moduleName => [$configFile]"
    return $configFile
}

function Test-Configuration
{
    <#
    .DESCRIPTION
        Tests if a configuration exists
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    # Write-Status "Testing Configuration for $moduleName"
    return Test-Path -Path (Get-ConfigurationFile -moduleName $moduleName)
}

function Reset-Configuration
{
    <#
    .DESCRIPTION
        Resets the configuration
        Resets also the configuration info that persisted in registry.
        Does a double cleanup so the one in working dir as well as user directory is removed
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    Write-Status $($GenericLocMessage.comm_reset_config)  -moduleName $moduleName
    # 1. Remove the shared configuration
    if (Test-Configuration -moduleName $moduleName)
    {
        Remove-Item -Path (Get-ConfigurationDirectory -moduleName $moduleName) -Recurse -Force -ErrorAction SilentlyContinue
    }
    Reset-ConfigurationDirectory -moduleName $moduleName

    # 2. Remove the local configuration, if any
    if (Test-Configuration -moduleName $moduleName)
    {
        Remove-Item -Path (Get-ConfigurationDirectory -moduleName $moduleName) -Recurse -Force -ErrorAction SilentlyContinue
    }
    $global:config[$moduleName] = @{}
}

function Save-Configuration
{
    <#
    .DESCRIPTION
        saves a configuration to persisted storage
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    $configFile = Get-ConfigurationFile -moduleName $moduleName
    $configDir = [IO.Path]::GetDirectoryName($configFile)
    if (!(Test-Path $configDir))
    {
        New-Item -ItemType Directory -Force -Path $configDir | Out-Null
    }

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_save_config , $moduleName, $configFile)) -moduleName $moduleName

    ConvertTo-Json -InputObject $global:config[$moduleName] | Out-File -FilePath $configFile
}

function Import-Configuration
{
    <#
    .DESCRIPTION
        Loads a configuration from persisted storage
     
    .PARAMETER moduleName
        Name of the module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )
    # Write-Status "Importing Configuration for $moduleName"

    $tmp = ConvertFrom-Json -InputObject (Get-Content (Get-ConfigurationFile -moduleName $moduleName) -Raw)
    $global:config[$moduleName] = @{}
    $tmp.psobject.Properties | ForEach-Object  { $global:config[$moduleName][$_.Name] = $_.Value}
}

function Set-ConfigurationValue
{
    <#
    .DESCRIPTION
        Persists a configuration value to the registry
  
    .PARAMETER name
        Name of the configuration value
 
    .PARAMETER moduleName
        Name of the module
 
    .PARAMETER value
        Value to be persisted
    #>

    param (
        [String] $name,
        [Parameter(Mandatory=$true)]
        [String] $module,
        [Object] $value
    )

    $global:config[$module][$name] = $value
    Save-Configuration -moduleName $module
}

function Get-ConfigurationValue
{
    <#
    .DESCRIPTION
        Retrieves a configuration value from the registry
  
    .PARAMETER type
        The expected type of the value being retrieved
 
    .PARAMETER module
        module of the module
     
    .PARAMETER name
        Name of the configuration value
    #>

    param (
        [Type] $type = [System.String],
        [Parameter(Mandatory=$true)]
        [String] $module,
        [String] $name
    )

    $value = $null
    if  (Test-Configuration -moduleName $module)
    {
        Import-Configuration -moduleName $module
        $value = $global:config[$module][$name]
    }

    switch($type.Name)
    {
        "Boolean"            { if (!$value) {$value = 0} return [System.Convert]::ToBoolean($value) }
        "UInt32"             { if (!$value) {$value = 0} return [System.Convert]::ToUInt32($value) }
        "VmSize"             { if (!$value) {$value = 0} return [Enum]::Parse([VmSize], $value, $true) }
        "DeploymentType"     { if (!$value) {$value = 0} return [Enum]::Parse([DeploymentType], $value, $true) }
        "InstallState"       { if (!$value) {$value = 0} return [Enum]::Parse([InstallState], $value, $true) }
        "LoadBalancerType"   { if (!$value) {$value = 0} return [Enum]::Parse([LoadBalancerType], $value, $true) }
        Default              { if (!$value) {$value = ""} return $value }
    }
}

#endregion

function ConvertTo-SystemVersion
{
    <#
    .DESCRIPTION
        Converts a string representation of a version to a System.Version object.
 
    .PARAMETER Version
        The version string to be converted.
    #>


    param (
        [String] $Version
    )

    $converted = $null
    if ([System.Version]::TryParse($Version, [ref] $converted))
    {
        return $converted
    }

    throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_version_str_err , $Version))
}

function Compare-Versions
{
    <#
    .DESCRIPTION
        Compares two string versions and returns an indication of their relative values. The comparison
        is performed by compariing the major version numbers for equality (per SemVer specification).
 
        The return value is a signed integer that indicates the relative values of the two objects:
 
        Less than zero - Version has a major version lower than the ComparisonVersion.
        Zero - The two major versions are the same (i.e. they are compatible).
        Greater than zero - Version has a newer major version than the ComparisonVersion.
 
    .PARAMETER Version
        The current version number.
 
    .PARAMETER ComparisonVersion
        A version number to be compared with the CurrentVersion.
 
    .OUTPUTS
        System.Int32
    #>


    param (
        [String] $Version,
        [String] $ComparisonVersion
    )

    $current = ConvertTo-SystemVersion -Version $Version
    $comparison = ConvertTo-SystemVersion -Version $ComparisonVersion

    if ($current.Major -eq $comparison.Major)
    {
        return 0
    }

    if ($current.Major -lt $comparison.Major)
    {
        return -1
    }

    return 1
}

function Test-IsProductInstalled()
{
    <#
    .DESCRIPTION
        Tests if the desired product/module is installed (or installing). Note that we consider some
        failed states (e.g. UninstallFailed) to represent that the product is still installed, albeit
        in a unknown/failed state.
 
    .PARAMETER moduleName
        The module name to test for installation state
 
    .PARAMETER activity
        Activity name to use when writing progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName,

        [Parameter()]
        [String]$activity = $MyInvocation.MyCommand.Name
    )

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $($GenericLocMessage.comm_verify_prod_installation_state)

    $currentState = Get-InstallState -module $moduleName
    if (-not $currentState)
    {
        return $false
    }

    switch($currentState)
    {
        $([InstallState]::NotInstalled) { return $false }
        $([InstallState]::InstallFailed) { return $false }
    }

    return $true
}

function Get-InstallState
{
    <#
    .DESCRIPTION
        Returns the installation state for a product/module. May return $null.
  
    .PARAMETER moduleName
        The module name to query
    #>


    param (
        [String] $moduleName
    )

    $state = Get-ConfigurationValue -module $moduleName -name "InstallState" -type ([Type][InstallState])
    if (-not $state)
    {
        $state = [InstallState]::NotInstalled
    }

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_installation_state , $state))
    return $state
}

function Test-ValidEndpoint
{
    <#
    .DESCRIPTION
        Validates that an endpoint is a valid ip address
         
        This function exists to validate that a endpoint is a valid ip address. If the enpoint is not a valid ip address, it throws.
  
    .PARAMETER endpoint
        A string representing an IP address
    #>

    param (
        [String] $endpoint
    )

    if (-not [ipaddress]::TryParse($endpoint,[ref]$null))
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_invalid_ip , $endpoint))
    }
}

function Test-ValidK8sObjectName
{
    <#
    .DESCRIPTION
        Validates the format of a name that will be used as the name of a kubernetes object
         
        This function exists so that multiple other functions can re-use the same validation rather
        than us repeating it all over the script (ValidatePattern does not allow variables to be
        used for the regex string as it must be a constant). We throw to provide a more specific
        error message to the caller.
  
    .PARAMETER Name
        The name of an k8s object
    .PARAMETER Type
        Currently we are only using k8s names for clusters and networks
    #>

    param (
        [String] $Name,
        [ValidateSet("cluster","network","nodepool")]
        [String] $Type
    )

    if (-not ($Name -cmatch $clusterNameRegex))
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_inavlid_name , $Name, $Type, $clusterNameRegex))
    }

    return $true
}

function Test-ValidClusterName
{
    <#
    .DESCRIPTION
        Validates the format of a cluster name.
         
        This function exists so that multiple other functions can re-use the same validation rather
        than us repeating it all over the script (ValidatePattern does not allow variables to be
        used for the regex string as it must be a constant). We throw to provide a more specific
        error message to the caller.
  
    .PARAMETER Name
        A cluster name
    #>

    param (
        [String] $Name
    )
    return Test-ValidK8sObjectName -Name $Name -Type "cluster"
}

function Test-ValidNetworkName
{
    <#
    .DESCRIPTION
        Validates the format of a network name.
         
        This function exists so that multiple other functions can re-use the same validation rather
        than us repeating it all over the script (ValidatePattern does not allow variables to be
        used for the regex string as it must be a constant). We throw to provide a more specific
        error message to the caller.
  
    .PARAMETER Name
        A network name
    #>

    param (
        [String] $Name
    )
    return Test-ValidK8sObjectName -Name $Name -Type "network"
}

function Test-ValidNodePoolName
{
    <#
    .DESCRIPTION
        Validates the format of a node pool name.
 
        This function exists so that multiple other functions can re-use the same validation rather
        than us repeating it all over the script (ValidatePattern does not allow variables to be
        used for the regex string as it must be a constant). We throw to provide a more specific
        error message to the caller.
 
    .PARAMETER Name
        A node pool name
    #>

    param (
        [String] $Name
    )
    return Test-ValidK8sObjectName -Name $Name -Type "nodepool"
}

function Test-ForUpdates
{
    <#
    .DESCRIPTION
        Check if a module is up to date and provide the option to update it.
 
    .PARAMETER moduleName
        The name of the module.
 
    .PARAMETER repositoryName
        Powershell repository name.
 
    .PARAMETER repositoryUser
        Powershell repository username.
 
    .PARAMETER repositoryPass
        Powershell repository password.
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
    param (
        [String] $moduleName,
        [String] $repositoryName,
        [String] $repositoryUser,
        [String] $repositoryPass
    )

    if ($global:config[$moduleName]["skipUpdates"])
    {
        return
    }

    Write-Status $($GenericLocMessage.comm_module_updates) -moduleName $moduleName

    $proxyParameters = @{}
    $proxyConfig = Get-ProxyConfiguration -moduleName $moduleName

    if ($proxyConfig.HTTPS)
    {
        $proxyParameters.Add("Proxy", $proxyConfig.HTTPS)
    }
    elseif ($proxyConfig.HTTP)
    {
        $proxyParameters.Add("Proxy", $proxyConfig.HTTP)
    }

    if ($proxyConfig.Credential)
    {
        $proxyParameters.Add("ProxyCredential", $proxyConfig.Credential)
    }

    $current = Get-InstalledModule -Name "PowershellGet" -ErrorAction SilentlyContinue
    if (($null -eq $current) -or ($current.version -lt 1.6.0))
    {
        Write-SubStatus $($GenericLocMessage.comm_psget_update) -moduleName $moduleName

        $parameters = @{
            Name = "PowershellGet"
            Force = $true
            Confirm = $false
            SkipPublisherCheck = $true
        }
        $parameters += $proxyParameters
        Install-Module @parameters

        Write-SubStatus $($GenericLocMessage.comm_psget_update_done) -moduleName $moduleName
        exit 0
    }

    $patToken = $repositoryPass | ConvertTo-SecureString -AsPlainText -Force
    $repositoryCreds = New-Object System.Management.Automation.PSCredential($repositoryUser, $patToken)

    $current = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue

    # TODO: Skip alpha/preview updates for now
    if ($current.Repository -ieq $global:repositoryNamePreview)
    {
        Write-SubStatus $($GenericLocMessage.comm_current_version_pre_release) -moduleName $moduleName
        return
    }

    $parameters = @{
        Name = $moduleName
        Repository = $repositoryName
        Credential = $repositoryCreds
        ErrorAction = "SilentlyContinue"
    }
    $parameters += $proxyParameters

    $latest = Find-Module @parameters
    if (($null -eq $current) -or ($null -eq $latest))
    {
        Write-SubStatus $($GenericLocMessage.comm_update_unable) -moduleName $moduleName
        return
    }

    Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_installed_version , $current.version)) -moduleName $moduleName

    Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_latest_version , $latest.version)) -moduleName $moduleName

    if ([System.Version]$current.version -ge [System.Version]$latest.version)
    {
        Write-SubStatus $($GenericLocMessage.comm_already_uptodate) -moduleName $moduleName
        return
    }

    Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_new_version , $moduleName)) -moduleName $moduleName

    $title    = 'Recommended update'
    $question = "Do you want to update to the latest version of the module/binaries? If you choose 'Yes' then we will perform a full cleanup of your deployment before applying the updates.`n"
    $choices  = '&Yes', '&No'

    $decision = $Host.UI.PromptForChoice($title, $question, $choices, 0)
    if ($decision -eq 0)
    {
        Write-Status $($GenericLocMessage.comm_installing_updates) -moduleName $moduleName

        $parameters = @{
            Name = $moduleName
            Credential = $repositoryCreds
            Force = $true
        }
        $parameters += $proxyParameters
        Update-Module @parameters

        $current = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue
        Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_installed_version_now , $current.version)+"`n") -moduleName $moduleName

        if ([System.Version]$current.version -ge [System.Version]$latest.version)
        {
            Write-SubStatus $($GenericLocMessage.comm_remove_older_version+"`n") -moduleName $moduleName
            Get-InstalledModule -Name $moduleName -AllVersions | Where-Object {$_.Version -ne $current.Version} | Uninstall-Module -Force -Confirm:$false -ErrorAction:SilentlyContinue

            Write-SubStatus $($GenericLocMessage.comm_update_successful) -moduleName $moduleName
            exit 0
        }

        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_update_failed , $current.version, $latest.version))
    }
}

function Initialize-Environment
{
    <#
    .DESCRIPTION
        Executes steps to prepare the environment for operations. This includes checking
        to ensure that the module is up to date and that a configuration is present.
 
    .PARAMETER moduleName
        The name of the module.
 
    .PARAMETER repositoryName
        Powershell repository name.
 
    .PARAMETER repositoryUser
        Powershell repository username.
 
    .PARAMETER repositoryPass
        Powershell repository password.
 
    .PARAMETER checkForUpdates
        Should the script check for updates before proceeding to prepare the environment.
 
    .PARAMETER createIfNotPresent
        Should the script create a new deployment configuration if one is not present.
    #>


    param (
        [Parameter()]
        [String] $moduleName,
        [Parameter()]
        [String] $repositoryName = $global:repositoryName,
        [Parameter()]
        [String] $repositoryUser = $global:repositoryUser,
        [Parameter()]
        [String] $repositoryPass = $global:repositoryPass,
        [Parameter()]
        [Switch]$checkForUpdates,
        [Parameter()]
        [Switch]$createIfNotPresent
    )

    Initialize-ProxyEnvironment -moduleName $moduleName

    if ($checkForUpdates.IsPresent)
    {
        Test-ForUpdates -moduleName $moduleName -repositoryName $repositoryName -repositoryUser $repositoryUser -repositoryPass $repositoryPass
    }
}

function Get-FirewallRules
{
    <#
    .DESCRIPTION
        Obtains the firewall rules needed for agent communication
    #>


    $firewallRules =
    @(
        ("wssdagent GRPC server port", "TCP", $global:config[$global:MocModule]["nodeAgentPort"]),
        ("wssdagent GRPC authentication port", "TCP", $global:config[$global:MocModule]["nodeAgentAuthorizerPort"]),
        ("wssdcloudagent GRPC server port", "TCP", $global:config[$global:MocModule]["cloudAgentPort"]),
        ("wssdcloudagent GRPC authentication port", "TCP", $global:config[$global:MocModule]["cloudAgentAuthorizerPort"])
    )
    return $firewallRules
}
#endregion

#region General helper functions

function Write-StatusWithProgress
{
    <#
    .DESCRIPTION
        Outputs status to progress and to console
  
    .PARAMETER status
        The status message
 
    .PARAMETER activity
        The progress activity
 
    .PARAMETER percentage
        The progress percentage. 100% will output progress completion
 
    .PARAMETER moduleName
        The module name. Will become a prefix for console output
    #>


    [CmdletBinding()]
    param (
        [String] $status = "",
        [String] $activity = "Status",
        [Int] $percentage = -1,
        [Switch] $completed,
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    # Propagate verbose preference across modules from the caller, if not explicitly specified
    if (-not $PSBoundParameters.ContainsKey('Verbose'))
    {
        $script:VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    $message = $status
    if ($moduleName)
    {
        $eventmessage = $("$activity - $message")
        $message = $("$moduleName - $message")
        Write-ModuleEventLog -moduleName $moduleName -entryType Information -eventId 1 -message $eventmessage
    }

    Write-Progress -Activity $activity -Status $message -PercentComplete $percentage -Completed:$completed.IsPresent
    Write-Status -msg $status -moduleName $moduleName
}

function Write-Status
{
    <#
    .DESCRIPTION
        Outputs status to the console with a prefix for readability
  
    .PARAMETER msg
        The message/object to output
 
    .PARAMETER moduleName
        The module name. Will be used as a prefix
    #>


    [CmdletBinding()]
    param (
        [Object]$msg,
        [Parameter(Mandatory=$true)]
        [String]$moduleName
    )

    # Propagate verbose preference across modules from the caller, if not explicitly specified
    if (-not $PSBoundParameters.ContainsKey('Verbose'))
    {
        $script:VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }

    if ($msg)
    {
        $time = Get-Date -DisplayHint Time
        Write-Verbose "[$time] [$moduleName] $msg`n"
    }
}

function Write-SubStatus
{
    <#
    .DESCRIPTION
        Outputs sub-status to the console with a indent for readability
  
    .PARAMETER msg
        The message/object to output
 
    .PARAMETER moduleName
        The module name. Will be used as a prefix
 
    .PARAMETER indentChar
        Char to use as the indent/bulletpoint of the status message
    #>


    [CmdletBinding()]
    param (
        [Object]$msg,
        [Parameter(Mandatory=$true)]
        [String]$moduleName,
        [String]$indentChar = "`t`t"
    )

    # Propagate verbose preference across modules from the caller, if not explicitly specified
    if (-not $PSBoundParameters.ContainsKey('Verbose'))
    {
        $script:VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
    }


    Write-Status -msg $($indentChar + $msg) -moduleName $moduleName
}


function GetDefaultLinuxNodepoolName
{
    <#
    .DESCRIPTION
        Gets the Default Linux Nodepool name.
  
    .PARAMETER clusterName
        Cluster Name.
    #>

    param (
        [String]$clusterName
    )

    # GA build named the default nodepools as $Name-default-linux-nodepool
    # fallback if $clusterName-linux is not found.
    $linuxNodepool = Invoke-Kubectl -ignoreError -arguments $("get akshcinodepools/$clusterName-default-linux-nodepool -o json") | ConvertFrom-Json
    if ($null -eq $linuxNodepool)
    {
        return "$clusterName-linux"
    }

    return "$clusterName-default-linux-nodepool"
}


function GetDefaultWindowsNodepoolName
{
    <#
    .DESCRIPTION
        Gets the Default Windows Nodepool Name
 
    .PARAMETER clusterName
        Cluster Name.
    #>


    param (
        [String]$clusterName
    )

    # GA build named the default nodepools as $Name-default-windows-nodepool
    # fallback if $clusterName-windows is not found.
    $windowsNodepool = Invoke-Kubectl -ignoreError -arguments $("get akshcinodepools/$clusterName-default-windows-nodepool -o json") | ConvertFrom-Json
    if ($null -eq $windowsNodepool)
    {
        return "$clusterName-windows"
    }

    return "$clusterName-default-windows-nodepool"
}

function Test-Process
{
    <#
    .DESCRIPTION
        Test if a process is running.
  
    .PARAMETER processName
        The process name to test.
 
    .PARAMETER nodeName
        The node to test on.
    #>


    param (
        [String]$processName,
        [String]$nodeName
    )

    Invoke-Command -ComputerName $nodeName -ScriptBlock  {
        $processName = $args[0]
        $GenericLocMessage = $args[1]
        $process = Get-Process -Name $processName -ErrorAction SilentlyContinue
        if(!$process)
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_process_not_running , $processName, $env:computername))
        }
    } -ArgumentList $processName, $GenericLocMessage
}

function Test-LocalFilePath
{
    <#
    .DESCRIPTION
        Returns true if the path appears to be local. False otherwise.
  
    .PARAMETER path
        Path to be tested.
    #>


    param(
        [String]$path
    )

    $path = [System.Environment]::ExpandEnvironmentVariables($path)
    $clusterStorage = Join-Path -Path $([System.Environment]::ExpandEnvironmentVariables("%systemdrive%")) -ChildPath 'clusterstorage'

    if($path.StartsWith('\\') -or $path.ToLower().StartsWith($clusterStorage.ToLower()))
    {
        return $false
    }

    return $true
}

function Stop-AllProcesses
{
    <#
    .DESCRIPTION
        Kill all instance of the given process name.
    #>


    param (
        [string]$processName,
        [string]$computerName = "localhost"
    )
    Invoke-Command -ComputerName $computerName {
        $processName = $args[0]
        Get-Process -Name $processName -ErrorAction SilentlyContinue | ForEach-Object {
            $_ | Stop-Process -Force
        }
    } -ArgumentList $processName
}

function Copy-FileLocal
{
    <#
    .DESCRIPTION
        Copies a file locally.
 
    .PARAMETER source
        File source.
 
    .PARAMETER destination
        File destination.
    #>


    param (
        [String]$source,
        [String]$destination
    )
    Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_copy , $source, $destination)) -moduleName $global:CommonModule
    Copy-Item -Path $source -Destination $destination -Recurse
}

function Copy-FileToRemoteNode
{
    <#
    .DESCRIPTION
        Copies a file to a remote node.
 
    .PARAMETER source
        File source.
 
    .PARAMETER destination
        File destination.
 
    .PARAMETER remoteNode
        The remote node to copy to.
    #>


    param (
        [String]$source,
        [String]$destination,
        [String]$remoteNode
    )

    $remotePath = "\\$remoteNode\" + ($destination).Replace(":", "$")
    Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_copy , $source, $remotePath)) -moduleName $global:CommonModule
    $remoteDir = [IO.Path]::GetDirectoryName($remotePath)
    if (!(Test-Path $remoteDir))
    {
        New-Item -ItemType Directory -Force -Path $remoteDir | Out-Null
    }
    while($true) {
        try {
            Copy-Item -Path $source -Destination $remotePath
            return
        } catch {
            Write-Warning $_.Exception.Message -ErrorAction Continue
            Start-Sleep -Seconds 5
            $process = [io.path]::GetFileNameWithoutExtension($destination)
            Stop-AllProcesses -computerName $remoteNode -processName $process
        }
    }
}

function Test-ForWindowsFeatures
{
    <#
    .DESCRIPTION
        Installs any missing required OS features.
 
    .PARAMETER features
        The features to check for.
 
    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [String[]]$features,
        [String]$nodeName
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_req_os_feature , $nodeName)) -moduleName $global:MocModule

    $nodeEditionName = Invoke-Command -ComputerName $nodeName -ScriptBlock {
        return (get-itemproperty "hklm:\software\microsoft\windows nt\currentversion").editionid
    }

    if (-not ($nodeEditionName -match 'server'))
    {
        # Comment out this check
        # throw $($GenericLocMessage.comm_support_server_editions)
    }

    $remoteRebootRequired = Invoke-Command -ComputerName $nodeName -ScriptBlock {
        $rebootRequired = $false
        $GenericLocMessage = $args[1]
        foreach($feature in $args[0])
        {
            write-verbose $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_checking_status_of_feature, $feature))

            $wf = Get-WindowsFeature -Name "$feature" 
            if ($null -eq $wf)
            {
                throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_feature_not_found , $feature))

            }

            if ($wf.InstallState -ine "Installed")
            {
                write-verbose $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_installing_missing_feature, $feature))

                $result = Install-WindowsFeature -Name "$feature" -WarningAction SilentlyContinue
                if ($result.RestartNeeded)
                {
                    $rebootRequired = $true
                }
            }
        }

        return $rebootRequired
    } -ArgumentList $features, $GenericLocMessage

    if ($remoteRebootRequired)
    {
        Write-Status $($GenericLocMessage.comm_reboot_req) -moduleName $global:MocModule
        Read-Host $("Press enter when you are ready to reboot $nodeName ...")
        Restart-Computer -ComputerName $nodeName -Force
    }
}

function Enable-Remoting
{
    <#
    .DESCRIPTION
        Enables powershell remoting on the local machine.
    #>


    Write-Status $($GenericLocMessage.comm_ps_remote) -moduleName $global:MocModule

    Enable-PSRemoting -Force -Confirm:$false
    winrm quickconfig -q -force
}

function Get-HostRoutingInfo
{
    <#
    .DESCRIPTION
        Obtains the host routing information.
 
    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [String]$nodeName
    )
    return Invoke-Command -ComputerName $nodeName -ScriptBlock {
        $GenericLocMessage = $args[0]
        $oldProgressPreference = $global:progressPreference
        $global:progressPreference = 'silentlyContinue'
        
        $computerName = "www.msftconnecttest.com"
        $routingInfo = Test-NetConnection -DiagnoseRouting -ComputerName $computerName -ErrorAction SilentlyContinue
        $global:progressPreference = $oldProgressPreference

        if (!$routingInfo -or !$routingInfo.RouteDiagnosticsSucceeded)
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_conn_test_failed , $computerName))
        }

        return $routingInfo 
    } -ArgumentList $GenericLocMessage
}

function Get-HostAdapterName
{
    <#
    .DESCRIPTION
        Obtains the name of the best host network adapter. Uses routing to determine the host interface name to return.
 
    .PARAMETER nodeName
        The node to execute on.
    #>


    param (
        [String]$nodeName
    )

    $routingInfo = Get-HostRoutingInfo -nodeName $nodeName
    return $routingInfo.OutgoingInterfaceAlias
}

function Get-HostIp
{
    <#
    .DESCRIPTION
        Obtains the hosts IP address. Uses routing to determine the best host IPv4 address to use.
 
    .PARAMETER nodeName
        The node to execute on.
    #>

    param (
        [String]$nodeName
    )

    $routingInfo = Get-HostRoutingInfo -nodeName $nodeName
    return $routingInfo.SelectedSourceAddress.IPv4Address
}

function Test-Binary
{
    <#
    .DESCRIPTION
        A basic sanity test to make sure that this system is ready to deploy kubernetes.
 
    .PARAMETER nodeName
        The node to execute on.
 
    .PARAMETER binaryName
        Binary to check.
    #>


    param (
        [String]$nodeName,
        [String]$binaryName
    )

    Invoke-Command -ComputerName $nodeName -ScriptBlock {
        $GenericLocMessage = $args[1]
        if ( !(Get-Command $args[0] -ErrorAction SilentlyContinue )) {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_missing_binary , $args[0]))
        }
    } -ArgumentList $binaryName, $GenericLocMessage
}

function Get-CloudFqdn
{
    <#
    .DESCRIPTION
        Determines the right FQDN to use for cloudagent based on the type of deployment and script args
    #>


    return $global:config[$global:MocModule]["cloudFqdn"]
}

function Get-SshPublicKey
{
    <#
    .DESCRIPTION
        Get the SSH Public Key that is configured to be used by the deployment
    #>


    return $global:config[$global:MocModule]["sshPublicKey"]
}

function Get-SshPrivateKey
{
    <#
    .DESCRIPTION
        Get the SSH Private Key that is configured to be used by the deployment
    #>


    return $global:config[$global:MocModule]["sshPrivateKey"]
}

function ConvertTo-ArgString {
    <#
    .DESCRIPTION
        Takes a dictionary of parameters and converts them to a string representation.
 
        Convert-ParametersToString is designed for powershell Cmdlet arguments.
        However this one is for binaries. It doesn't add "-" in front parameter names
        and joins the value by comma if its an array.
 
    .PARAMETER argDictionary
        Dictionary of arguments (e.g. obtained from $PSBoundParameters).
 
    .PARAMETER boolFlags
        List of boolean flags to be passed
 
    .PARAMETER separator
        Separates flag and value; default: " " (single space)
    #>

    param (
        [System.Collections.IDictionary] $argDictionary,
        [string[]] $boolFlags,
        [string] $separator = " "
    )

    [string[]] $argsList = @()

    if ($argDictionary)
    {
        foreach ($key in $argDictionary.Keys) {
            $argsList += ('{0}{1}"{2}"' -f $key, $separator, $($argDictionary[$key] -join ","))
        }
    }

    if ($boolFlags)
    {
        $argsList += $boolFlags
    }

    return $argsList -join " "
}



#region invoke methods
function Invoke-CommandLine
{
    <#
    .DESCRIPTION
        Executes a command and optionally ignores errors.
 
    .PARAMETER command
        Comamnd to execute.
 
    .PARAMETER arguments
        Arguments to pass to the command.
 
    .PARAMETER ignoreError
        Optionally, ignore errors from the command (don't throw).
 
    .PARAMETER showOutput
        Optionally, show live output from the executing command.
 
    .PARAMETER showOutputAsProgress
        Optionally, show output from the executing command as progress bar updates.
 
    .PARAMETER progressActivity
        The activity name to display when showOutputAsProgress was requested.
 
    .PARAMETER moduleName
        The calling module name to show in output logging.
    #>


    param (
        [String]$command,
        [String]$arguments,
        [Switch]$ignoreError,
        [Switch]$showOutput,
        [Switch]$showOutputAsProgress,
        [String]$progressActivity,
        [Parameter(Mandatory=$true)]
        [String]$moduleName
    )

    try {
        if ($showOutputAsProgress.IsPresent)
        {
            $result = (& $command $arguments.Split(" ") | ForEach-Object { $status = $_ -replace "`t"," - "; Write-StatusWithProgress -activity $progressActivity -moduleName $moduleName -Status $status }) 2>&1
        }
        elseif ($showOutput.IsPresent)
        {
            $result = (& $command $arguments.Split(" ") | Out-Default) 2>&1
        }
        else
        {
            $result = (& $command $arguments.Split(" ") 2>&1)
        }
    }
    catch {
        if ($ignoreError.IsPresent)
        {
            return
        }
        throw
    }
    
    $out = $result | Where-Object {$_.gettype().Name -ine "ErrorRecord"}  # On a non-zero exit code, this may contain the error
    #$outString = ($out | Out-String).ToLowerInvariant()

    if ($LASTEXITCODE)
    {
        $err = $result | Where-Object {$_.gettype().Name -eq "ErrorRecord"}
        $errMessage = "$command $arguments $GenericLocMessage.generic_non_zero $LASTEXITCODE [$err]"
        if ($ignoreError.IsPresent)
        {
            $ignoreMessage = "[IGNORED ERROR] $errMessage"
            Write-Status -msg $ignoreMessage -moduleName $moduleName
            Write-ModuleEventLog -moduleName $moduleName -entryType Warning -eventId 2 -message $errMessage
            return
        }
        throw $errMessage
    }
    return $out
}

function Invoke-Kubectl
{
    <#
    .DESCRIPTION
        Executes a kubectl command.
 
    .PARAMETER kubeconfig
        The kubeconfig file to use. Defaults to the management kubeconfig.
 
    .PARAMETER arguments
        Arguments to pass to the command.
 
    .PARAMETER ignoreError
        Optionally, ignore errors from the command (don't throw).
 
    .PARAMETER showOutput
        Optionally, show live output from the executing command.
    #>


    param (
        [string] $kubeconfig = $global:config[$global:KvaModule]["kubeconfig"],
        [string] $arguments,
        [switch] $ignoreError,
        [switch] $showOutput
    )

    return Invoke-CommandLine -command $global:kubeCtlFullPath -arguments $("--kubeconfig=""$kubeconfig"" $arguments") -showOutput:$showOutput.IsPresent -ignoreError:$ignoreError.IsPresent -moduleName $global:KvaModule
}

#end region

function Compress-Directory
{

    <#
    .DESCRIPTION
        Util for zipping folders
 
    .PARAMETER ZipFilename
        output zip file name
 
    .PARAMETER SourceDir
        directory to compress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$ZipFilename,

        [Parameter(Mandatory=$true)]
        [String]$SourceDir
    )

    if (Test-Path $ZipFilename) 
    {

        $title    = 'ZipFile already exists'
        $question = "Do you want to overwrite it?`n"
        $choices  = '&Yes', '&No'

        $decision = $Host.UI.PromptForChoice($title, $question, $choices, 0)
        if ($decision -eq 0) 
        {
            Remove-Item -Path $ZipFilename -Force
        }
        else 
        {
            throw $([System.IO.IOException] $GenericLocMessage.generic_file_exists)
        }

   }

   Add-Type -Assembly System.IO.Compression.FileSystem
   $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
   [System.IO.Compression.ZipFile]::CreateFromDirectory($SourceDir,$ZipFilename, $compressionLevel, $false)

}


#endregion

#region Resource limit helper functions

function Convert-ParametersToString
{
    <#
    .DESCRIPTION
        Takes a dictionary of parameters and converts them to a string representation.
 
    .PARAMETER argDictionary
        Dictionary of arguments (e.g. obtained from $PSBoundParameters).
 
    .PARAMETER stripAsJob
        Optionally remove 'AsJob' if it's included in the argDictionary.
    #>


    param (
        [System.Collections.IDictionary] $argDictionary,
        [Switch] $stripAsJob
    )

    $strArgs = ""
    foreach ($key in $argDictionary.Keys)
    {
        if (($key -ieq "AsJob") -and ($stripAsJob.IsPresent))
        {
            continue
        }

        $seperator = " "
        $val = $argDictionary[$key]
        if (($val -eq $true) -or ($val -eq $false))
        {
            $seperator = ":$"
        }
        $strArgs += " -$key$seperator$val"
    }

    return $strArgs
}

#endregion

#region prechecks

function Test-HCIRegistration
{
    <#
    .DESCRIPTION
       Check the SKU of node and if HCI substrate then check the registration status of the node
    #>


    $osResult = Get-CimInstance -Namespace root/CimV2 -ClassName Win32_OperatingSystem -Property OperatingSystemSKU
    # PRODUCT_AZURESTACKHCI_SERVER_CORE = 406
    # Check if the substrate is HCI
    if ($osResult.OperatingSystemSKU -eq 406)
    {
        $hciStatus = Get-AzureStackHCI
        #Check if the HCI node is registered else throw error
        $regStatus = ($hciStatus).RegistrationStatus
        if ($regStatus -ine "Registered")
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_unregistered_node , $env:computername, $regStatus))
        }

        if ($hciStatus.ConnectionStatus -ne "Connected")
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_outofpolicy , $hciStatus.AzureResourceName, $hciStatus.ConnectionStatus))
        }

        # $hciSubStatus = Get-AzureStackHCISubscriptionStatus | Where-Object { $_.SubscriptionName -eq "Azure Stack HCI" }
        # if ($hciSubStatus.Status -ne "Subscribed")
        # {
        # throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_hcistatusnotsubscribed , $hciSubStatus.Status))
        # }
    }
}

function Test-ClusterHealth
{
   <#
    .DESCRIPTION
       Check if cluster node and cluster network are up
   #>

   
   #Check the ClusterNode
   Get-ClusterNode -ErrorAction Stop | ForEach-Object {
       if ($_.State -ine "Up") 
       {
          throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_cluster_node_state , $_, $($_.State)))
       }
   }
   Get-ClusterNetwork -ErrorAction Stop | ForEach-Object {
       if ($_.State -ine "Up") 
       {
          throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_cluster_network_state , $_, $($_.State)))
       }
   }
}

#endregion

#region Asynchronous job helpers

function Get-BackgroundJob
{
    <#
    .DESCRIPTION
        Returns a background job by name (supports wildcards and may return multiple jobs)
  
    .PARAMETER name
        Name of the job(s).
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $name
    )
 
    return Get-Job -Name $name -ErrorAction SilentlyContinue
}

function New-BackgroundJob
{
    <#
    .DESCRIPTION
        Creates a new background job.
  
    .PARAMETER name
        Name of the job.
 
    .PARAMETER cmdletName
        Cmdlet to execute as a job
 
    .PARAMETER argDictionary
        Argument dictionary to pass to the job cmdlet.
 
    .PARAMETER scheduledJob
        Optionally, use a scheduled job instead of a regular job. This allows the job to execute fully in the background
        and be accessible cross-session. Note that less progress information is available while this type of job is
        running.
 
    .PARAMETER allowDuplicateJobs
        Optionally, allow a new job to be created even if a job with the same name already exists and is still executing.
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $name,

        [Parameter(Mandatory=$true)]
        [String] $cmdletName,

        [Parameter(Mandatory=$true)]
        [System.Collections.IDictionary] $argDictionary,

        [Parameter()]
        [Switch] $scheduledJob,

        [Parameter()]
        [Switch] $allowDuplicateJobs
    )

    if (-not $allowDuplicateJobs.IsPresent)
    {
        $jobs = Get-BackgroundJob -name $name
        foreach ($job in $jobs)
        {
            if (($job.State -ieq "Completed") -or ($job.State -ieq "Failed"))
            {
                continue
            }
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_job_exists , $name))
        }
    }

    $strArgs = Convert-ParametersToString($argDictionary) -stripAsJob

    if ($scheduledJob.IsPresent)
    {
        if (-not $strArgs)
        {
            # Always pass at least one arg
            $strArgs = '-Verbose:$false'
        }
        $options = New-ScheduledJobOption -RunElevated -HideInTaskScheduler
        return Register-ScheduledJob -Name $name -ScheduledJobOption $options -RunNow -ScriptBlock {
            param($p1,$p2)
            #$VerbosePreference = "continue"
            Invoke-Expression $("$p1 $p2")
        } -ArgumentList $cmdletName,$strArgs
    }
    else
    {
        return Start-Job -Name $name -ScriptBlock { Invoke-Expression $("$using:cmdletName $using:strArgs") }
    }
}

#endregion

function Get-FailoverCluster
{
    <#
    .DESCRIPTION
        Safe wrapper around checking for the presence of a failover cluster.
    #>


    # Check if failover cluster powershell module was installed
    # and only run Get-Cluster in that case
    if (Get-Command "Get-Cluster" -errorAction SilentlyContinue)
    {
        return Get-Cluster -ErrorAction SilentlyContinue
    }

    return $null
}

function Get-KubernetesGalleryImageName 
{
    <#
    .DESCRIPTION
        Returns the appropriate gallery image name based on kubernetes versions
 
    .PARAMETER k8sVersion
        Kubernetes version
 
    .PARAMETER imageType
        Image Type
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$k8sVersion,
        
        [Parameter(Mandatory=$true)]
        [ValidateSet("Windows", "Linux")]
        [String]$imageType
    )

    $tmpVersion = ($k8sVersion.TrimStart("v").Replace('.', '-'))

    switch ($imageType)
    {
        "Windows" {  return "Windows_k8s"   }
        "Linux"   {  return "Linux_k8s_"   + $tmpVersion   }
    }
}

function Test-MultiNodeDeployment
{
    <#
    .DESCRIPTION
        Returns true if the script believes this is a multi-node deployment. False otherwise.
    #>

    $failoverCluster = Get-FailoverCluster
    return ($nil -ne $failoverCluster)
}

function Get-Ipv4MaskFromPrefix
{
    <#
    .DESCRIPTION
        Transforms an IP prefix length to an IPv4 net mask.
 
    .PARAMETER PrefixLength
        Length of the prefix
    #>

    param (
        [Parameter(Mandatory=$true)]
        [int] $PrefixLength
    )

    Add-Type -Language CSharp -ReferencedAssemblies "System.Numerics.dll" @"
using System;
using System.Numerics;
 
namespace AKSHCI
{
    public static class Ipv4MaskCompute
    {
        public static byte[] GetIpv4MaskFromPrefix(int prefixLength)
        {
            BigInteger fullMask = new BigInteger(0xFFFFFFFF);
            BigInteger mask = ((fullMask << (32 - prefixLength)) & fullMask);
            return mask.ToByteArray();
        }
    }
}
"@
;
 
    if ($PrefixLength -lt 0)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_invalid_prefix_length , $PrefixLength))
    }
    if ($PrefixLength -eq 0)
    {
        return "0.0.0.0"
    }
    if ($PrefixLength -gt 32)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_invalid_prefix_length , $PrefixLength))
    }

    $maskArray = [AKSHCI.Ipv4MaskCompute]::GetIpv4MaskFromPrefix($PrefixLength)
    return "$($maskArray[3]).$($maskArray[2]).$($maskArray[1]).$($maskArray[0])"
}

function Get-ClusterNetworkPrefixForIp
{
    <#
    .DESCRIPTION
        Returns the cluster network prefix length associated with the given IP, or $null if not found.
 
    .PARAMETER IpAddress
        Find the cluster network associated with this IP address
    #>

    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress] $IpAddress
    )
 
    $v4Networks = (Get-ClusterNetwork -ErrorAction SilentlyContinue | Where-Object { $_.Ipv4Addresses.Count -gt 0 })

    foreach($v4Network in $v4Networks) {
        for($i = 0; $i -lt $v4Network.Ipv4Addresses.Count; $i++) {
            [System.Net.IPAddress]$ipv4 = $null
            $clusIpv4 = $v4Network.Ipv4Addresses[$i]
            if (-Not [System.Net.IPAddress]::TryParse($clusIpv4, [ref] $ipv4))
            {
                Write-Warning $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_ignore_failover_ip , $clusIpv4))
                continue
            }
            $lastIp = [AKSHCI.IPUtilities]::GetLastIpInCidr($ipv4, $v4Network.Ipv4PrefixLengths[$i])
            if([AKSHCI.IPUtilities]::CompareIpAddresses($ipAddress, $ipv4) -ge 0 -AND [AKSHCI.IPUtilities]::CompareIpAddresses($ipAddress, $lastIp) -le 0)
            {
                return $v4Network.Ipv4PrefixLengths[$i]
            }
        }
    }

    throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_cluster_network_not_found , $IpAddress))
}

function Get-FailoverClusterResourceState
{
    <#
    .DESCRIPTION
        Gets the status information of the resource
 
    .PARAMETER clusterResource
        Cluster Resource
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $resourceName
    )
   
    $extendedStatus = ""
    $tmp = Get-ClusterResource -Name $resourceName -ErrorAction Ignore
    if ($tmp)
    {
        $extendedStatus = "CurrentState: $($tmp.State), LastOperationStatusCode: $($tmp.LastOperationStatusCode), StatusInformation: $($tmp.StatusInformation)."
    }

    return $extendedStatus
}

function Get-FailoverClusterGroupState
{
    <#
    .DESCRIPTION
        Gets the status information of the group
 
    .PARAMETER clusterGroupName
        Cluster group
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $clusterGroupName
    )
   
    $extendedStatus = ""
    $tmp = Get-ClusterGroup -Name $clusterGroupName -ErrorAction Ignore
    if ($tmp)
    {
        $extendedStatus = "CurrentState: $($tmp.State), LastOperationStatusCode: $($tmp.LastOperationStatusCode), StatusInformation: $($tmp.StatusInformation)."
    }

    return $extendedStatus
}

function Start-FailoverClusterResource
{
    <#
    .DESCRIPTION
        Starts a failover cluster resource
 
    .PARAMETER resourceName
        Name of the resource
     
    .PARAMETER waitTimeMinutes
        waitTime in minutes (Example: -waitTimeMinutes 5)
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $resourceName,
        [int] $waitTimeMinutes = 5
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_starting_cluster_resource , $resourceName)) -moduleName $global:MocModule

    # 1. Start the cluster resource
    try {
        Start-ClusterResource -Name $resourceName -ErrorAction Stop | Out-Null
    } catch [Exception] {
        $extendedError = Get-FailoverClusterResourceState -resourceName $resourceName
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_failed_to_start_cluster_resource, $resourceName, $extendedError, $($_.Exception.Message.ToString())))
    }

    # 2. Wait for up to 5 minutes for the transition to happen
    $clusterResource = (Get-ClusterResource -Name $resourceName -ErrorAction Stop)
    $waitStart = [DateTime]::get_Now()
    while ($clusterResource.State -eq [Microsoft.FailoverClusters.PowerShell.ClusterResourceState]::Pending)
    {
        $extendedState = Get-FailoverClusterResourceState -resourceName $resourceName
        Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_waiting_for_resource, $resourceName, $extendedState)) -moduleName $global:MocModule

        if (([DateTime]::get_Now() - $waitStart) -ge [Timespan]::FromMinutes($waitTimeMinutes))
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_timed_out_waiting_for_resource, $resourceName, $extendedState))
        }
        Start-Sleep 5
        $clusterResource = (Get-ClusterResource -Name $resourceName -ErrorAction Stop)
    }    
    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_started_cluster_resource , $resourceName)) -moduleName $global:MocModule
}

function Start-FailoverClusterGroup
{
    <#
    .DESCRIPTION
        Start and wait for cluster group
 
    .PARAMETER clusterGroupName
        Name of the cluster group (Example: ca-2f87825b-a4af-473f-8a33-8e3bdd5f9b61)
 
    .PARAMETER waitTimeMinutes
        waitTime in minutes (Example: -waitTimeMinutes 5)
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $clusterGroupName,
        [int] $waitTimeMinutes = 5
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_starting_cluster_group , $clusterGroupName)) -moduleName $global:MocModule

    # 1. Start the cluster resource
    try {
        Start-ClusterGroup -Name $clusterGroupName -ErrorAction Stop | Out-Null
    } catch [Exception] {
        $extendedError = Get-FailoverClusterGroupState -clusterGroupName $clusterGroupName
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_failed_to_start_cluster_group, $clusterGroupName, $extendedError, $($_.Exception.Message.ToString())))
    }

    # 2. Wait for up to 5 minutes for the transition to happen - ie ip resource to become ready
    $clusterGroup = (Get-ClusterGroup -Name $clusterGroupName -ErrorAction Stop)
    $waitStart = [DateTime]::get_Now()
    while ($clusterGroup.State -eq [Microsoft.FailoverClusters.PowerShell.ClusterGroupState]::Pending)
    {
        $extendedState = Get-FailoverClusterGroupState -clusterGroupName $clusterGroupName
        Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_waiting_for_group, $clusterGroupName, $extendedState)) -moduleName $global:MocModule

        if (([DateTime]::get_Now() - $waitStart) -ge [Timespan]::FromMinutes($waitTimeMinutes))
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_timed_out_waiting_for_group_in_failover_cluster, $extendedState))
        }
        Start-Sleep 5
        $clusterGroup = (Get-ClusterGroup -Name $clusterGroupName -ErrorAction Stop)
    }
    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_started_cluster_group , $clusterGroupName)) -moduleName $global:MocModule
}

function Add-FailoverClusterGenericRole
{
    <#
    .DESCRIPTION
        Creates a generic service role in failover cluster (similar to Add-ClusterGenericServiceRole), but allows for fine tuning network configuration.
 
    .PARAMETER staticIpCidr
        Static IP and network prefix, using the CIDR format (Example: 192.168.1.2/16)
     
    .PARAMETER serviceDisplayName
        Display name of the service (Example: "WSSD cloud agent service")
 
    .PARAMETER clusterGroupName
        Name of the cluster group (Example: ca-2f87825b-a4af-473f-8a33-8e3bdd5f9b61)
 
    .PARAMETER serviceName
        Name of the service binary (Example: wssdcloudagent)
     
    .PARAMETER serviceParameters
        Service start parameters (Example: --fqdn ca-2f87825b-a4af-473f-8a33-8e3bdd5f9b61.contoso.com)
    #>

    param (
        [string] $staticIpCidr,
        [Parameter(Mandatory=$true)]
        [string] $serviceDisplayName,
        [Parameter(Mandatory=$true)]
        [string] $clusterGroupName,
        [Parameter(Mandatory=$true)]
        [string] $serviceName,
        [Parameter(Mandatory=$true)]
        [string] $serviceParameters
    )

    # 0 - Prerequisite
    Add-ClusterGroup -Name $clusterGroupName -GroupType GenericService -ErrorAction Stop | Out-Null
    $dnsName = Add-ClusterResource -Name "$clusterGroupName" -ResourceType "Network Name" -Group $clusterGroupName -ErrorAction Stop
    $dnsName | Set-ClusterParameter -Multiple @{"Name"="$clusterGroupName";"DnsName"="$clusterGroupName"} -ErrorAction Stop
    
    # 1. Create and Start the resources in order - IP resource, Service and then start the Resource Group
    if ([string]::IsNullOrWhiteSpace($staticIpCidr))
    {
        # 1.a DHCP Case
        $networkList = Get-ClusterNetwork -ErrorAction SilentlyContinue | Where-Object { $_.Role -eq "ClusterAndClient" }

        foreach ($network in $networkList)
        {
            $IPResourceName = "IPv4 Address on $($network.Address)"
            $IPAddress = Add-ClusterResource -Name $IPResourceName -ResourceType "IP Address" -Group $clusterGroupName -ErrorAction Stop
            $IPAddress | Set-ClusterParameter -Multiple @{"Network"=$network;"EnableDhcp"=1} -ErrorAction Stop

            Add-ClusterResourceDependency -Resource "$clusterGroupName" -Provider $IPResourceName -ErrorAction Stop | Out-Null

            try 
            {
                Start-FailoverClusterResource -resourceName $IPResourceName -waitTimeMinutes 5
            }
            catch 
            {
                $errorMessage = Write-ModuleEventException -message "Start-FailoverClusterResource failed on resource $IPResourceName" -exception $_ -moduleName $global:MocModule
                Remove-ClusterGroup -Name $clusterGroupName -RemoveResources -Force -ErrorAction Ignore
                throw $([System.Exception]::new([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_failover_cluster_networks_error, $errorMessage), $_.Exception))
            }
        }
    }
    else
    {
        # 1.b Static IP Case
        $staticIpCidrArray = $staticIpCidr.Split("/") #Split a string of format x.x.x.x/pp to an array of x.x.x.x and pp
        if ($staticIpCidrArray.Length -eq 1)
        {
            $prefixLength = Get-ClusterNetworkPrefixForIp -IpAddress $staticIpCidrArray[0]
        }
        else
        {
            $prefixLength = $staticIpCidrArray[1]
        }

        $subnetMask = Get-Ipv4MaskFromPrefix -PrefixLength $prefixLength

        $IPResourceName = "IPv4 Address $($staticIpCidrArray[0])"
        $IPAddress = Add-ClusterResource -Name $IPResourceName -ResourceType "IP Address" -Group $clusterGroupName -ErrorAction Stop
        $IPAddress | Set-ClusterParameter -Multiple @{"Address"=$staticIpCidrArray[0];"SubnetMask"=$subnetMask;"EnableDhcp"=0} -ErrorAction Stop
        Add-ClusterResourceDependency -Resource "$clusterGroupName" -Provider $IPResourceName -ErrorAction Stop | Out-Null

        try 
        {
            Start-FailoverClusterResource -resourceName $IPResourceName -waitTimeMinutes 5
        }
        catch 
        {
            $errorMessage = Write-ModuleEventException -message "Start-FailoverClusterResource failed." -exception $_ -moduleName $global:MocModule
            Remove-ClusterGroup -Name $clusterGroupName -RemoveResources -Force -ErrorAction Ignore            
            throw $([System.Exception]::new([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_ip_address, $($staticIpCidrArray[0]), $errorMessage), $_.Exception))
        }
    }

    #
    $ServiceConfig = Add-ClusterResource -Name $serviceDisplayName -ResourceType "Generic Service" -Group $clusterGroupName -ErrorAction Stop
    Add-ClusterResourceDependency -Resource $serviceDisplayName -Provider "$clusterGroupName" -ErrorAction Stop | Out-Null
    $ServiceConfig | Set-ClusterParameter -Multiple @{"ServiceName"=$serviceName;"StartupParameters"=$serviceParameters;"UseNetworkName"=1} -ErrorAction Stop

    # 3. Start the service .
    try 
    {
        Start-FailoverClusterResource -resourceName $serviceDisplayName -waitTimeMinutes 5
    }
    catch 
    {
        $errorMessage = Write-ModuleEventException -message "Start-FailoverClusterResource failed " -exception $_ -moduleName $global:MocModule
        Remove-ClusterGroup -Name $clusterGroupName -RemoveResources -Force -ErrorAction Ignore
        throw $([System.Exception]::new([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_issue_while_registering_resource_name, $errorMessage), $_.Exception))
    }

    # 5. Start the cluster group and wait at most 5 minutes for it to come online
    try {
        Start-FailoverClusterGroup -clusterGroupName $clusterGroupName -waitTimeMinutes 5
    } catch [Exception] {
        Remove-ClusterGroup -Name $clusterGroupName -RemoveResources -Force -ErrorAction Ignore
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_cloudagent_service_failed_to_start, $clusterGroupName, $($_.Exception.Message.ToString())))
    }
}

function Get-CapiClusters
{
    <#
    .DESCRIPTION
        Returns all AksHciCluster resources
    #>


    Write-Status $($GenericLocMessage.comm_retrieve_clusters) -moduleName $global:KvaModule

    $capiClusters = Invoke-Kubectl -arguments $("get akshciclusters -o json") | ConvertFrom-Json
    if ($null -eq $capiClusters)
    {
        throw [CustomException]::new(($($GenericLocMessage.comm_no_cluster_info)), $true)
    }

    $clusters = @()
    foreach($capiCluster in $capiClusters.items)
    {
        $clusters += Get-CapiCluster -Name $capiCluster.metadata.name
    }

    return $clusters
}

function Get-CapiCluster
{
    <#
    .DESCRIPTION
        Returns the requested CapiCluster resource
 
    .PARAMETER Name
        Name of the cluster
    #>


    param (
        [Parameter()]
        [String]$Name
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_retreive_cluster_config , $Name)) -moduleName $global:KvaModule

    try
    {
        $capiCluster = Invoke-Kubectl -arguments $("get akshciclusters/$Name -o json") | ConvertFrom-Json
    }
    catch
    {
        # Filter Not Found exception
        if (($_.Exception.Message -like "*not found*"))
        {
            throw [CustomException]::new(($([System.Exception]::new([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_no_cluster_found , $Name), $_.Exception))), $true)
        }
        throw $_
    }

    $nodePools = Invoke-Kubectl -ignoreError -arguments $("get akshcinodepools -l msft.microsoft/capicluster-name=$Name -o json") | ConvertFrom-Json

    Write-SubStatus $($GenericLocMessage.comm_cluster_info) -moduleName $global:KvaModule

    $cniConfig = $capiCluster.spec.clusterConfiguration.cniConfiguration.configuration
    $primaryNetworkPlugin = [NetworkPlugin]::Default

    if (-Not [string]::IsNullOrWhiteSpace($cniConfig))
    {
        if ($cniConfig.Contains(':'))
        {
            $primaryNetworkPlugin = $cniConfig.Substring(0, $cniConfig.IndexOf(':')) # only keep the cni name, remove the version
        }
        else
        {
            $primaryNetworkPlugin = $cniConfig
        }
    }

    # For ease, calculate default linux and windows nodepool replica counts + VmSizes and append them as a member of the returned object
    $linuxWorkerReplicas = 0
    $linuxWorkerVmSize = [VmSize]::Default
    $windowsWorkerReplicas = 0
    $windowsWorkerVmSize = [VmSize]::Default

    if ($null -ne $nodePools)
    {
        $linuxNodePools = ($nodePools.items | Where-Object { $_.spec.infrastructureProfile.osProfile.osType -eq "Linux" })
        foreach ($np in $linuxNodePools)
        {
            if ("replicas" -in $np.spec.PSObject.Properties.Name)
            {
                $linuxWorkerReplicas += $np.spec.replicas
            }
        }

        $windowsNodePools = ($nodePools.items | Where-Object { $_.spec.infrastructureProfile.osProfile.osType -eq "Windows" })
        foreach ($np in $windowsNodePools)
        {
            if ("replicas" -in $np.spec.PSObject.Properties.Name)
            {
                $windowsWorkerReplicas += $np.spec.replicas
            }
        }
    }

    if ($null -ne $nodePools)
    {
        $capiCluster | Add-Member -NotePropertyName nodepools -NotePropertyValue $nodePools
    }
    $capiCluster | Add-Member -NotePropertyName linuxWorkerReplicas -NotePropertyValue $linuxWorkerReplicas
    $capiCluster | Add-Member -NotePropertyName linuxWorkerVmSize -NotePropertyValue $linuxWorkerVmSize
    $capiCluster | Add-Member -NotePropertyName windowsWorkerReplicas -NotePropertyValue $windowsWorkerReplicas
    $capiCluster | Add-Member -NotePropertyName windowsWorkerVmSize -NotePropertyValue $windowsWorkerVmSize
    $capiCluster | Add-Member -NotePropertyName primaryNetworkPlugin -NotePropertyValue $primaryNetworkPlugin

    return $capiCluster
}

#region Logging and Monitoring

#region Cleanup Functions

function Install-Binaries
{
    <#
    .DESCRIPTION
        Copies AksHci binaries to a node
 
    .PARAMETER nodeName
        The node to execute on.
    .PARAMETER module
        The module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$nodeName,
        [Parameter(Mandatory=$true)]
        [String]$module,
        [Parameter(Mandatory=$true)]
        [Hashtable] $binariesMap
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_install_binaries , $module, $nodeName)) -moduleName $module

    Invoke-Command -ComputerName $nodeName -ScriptBlock {
        $path = $args[0]

        New-Item -ItemType Directory -Force -Path $path
        
        $envPath = [Environment]::GetEnvironmentVariable("PATH")
        if($envPath -notlike $("*$path*"))
        {
            [Environment]::SetEnvironmentVariable("PATH", "$envPath;$path")
            [Environment]::SetEnvironmentVariable("PATH", "$envPath;$path", "Machine")
        }
    } -ArgumentList $global:installDirectory | out-null

    $binariesMap.Keys | foreach-object {
        Copy-FileToRemoteNode -source $([io.Path]::Combine($global:config[$module]["installationPackageDir"], $_)) -remoteNode $nodeName -destination $binariesMap[$_]
    }
}

function Uninstall-Binaries
{
    <#
    .DESCRIPTION
        Copies AksHci binaries to a node
 
    .PARAMETER nodeName
        The node to execute on.
    .PARAMETER module
        The module
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$nodeName,
        [Parameter(Mandatory=$true)]
        [String]$module,
        [Parameter(Mandatory=$true)]
        [Hashtable] $binariesMap
    )

    Write-Status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_uninstall_binaries , $module, $nodeName)) -moduleName $module

    Invoke-Command -ComputerName $nodeName -ScriptBlock {
        $binaries = $args[0]

        $binaries.Keys | Foreach-Object {
            Remove-Item -Path $binaries[$_] -force -ErrorAction SilentlyContinue
        }
    } -ArgumentList $binariesMap | out-null
}

#endregion

#region Catalog helpers

function Get-Catalog
{
    <#
    .DESCRIPTION
        Get the Catalog for AksHci. This would include a set of product release versions
         
    .PARAMETER moduleName
        Module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$moduleName
    )

    $cacheFile = $global:config[$moduleName]["manifestCache"]
    if ((Test-Path $cacheFile)) {
        return (Get-Content $cacheFile | ConvertFrom-Json)
    }

    $provider = Get-DownloadProvider -module $moduleName

    $downloadParams = @{
        Name = $global:config[$moduleName]["catalog"]
        Audience = $global:config[$moduleName]["ring"]
        Provider = $provider
    }

    if ($global:config[$moduleName]["useStagingShare"])
    {
        $downloadParams.Add("Endpoint", $global:config[$moduleName]["stagingShare"])
    }

    $catalog = Get-DownloadSdkCatalog @downloadParams

    $cacheFile = $global:config[$moduleName]["manifestCache"]
    $catalogJson = $catalog | ConvertTo-Json -depth 100
    Set-Content -path $cacheFile -value $catalogJson -encoding UTF8
    return $catalog
}

function Get-LatestCatalog
{
    <#
    .DESCRIPTION
        Get the latest catalog for AksHci by clearing the cache and redownloading the latest
 
    .PARAMETER moduleName
        Module name
    #>

    
    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    # Clean the catalog cache, so we download the latest
    Clear-CatalogCache -moduleName $moduleName

    return Get-Catalog -moduleName $moduleName
}

function Clear-CatalogCache
{
    <#
    .DESCRIPTION
        Removes any cached copy of the catalog
         
    .PARAMETER moduleName
        Module name
    #>


    $cacheFile = $global:config[$moduleName]["manifestCache"]
    if ((Test-Path $cacheFile)) {
        Remove-Item $cacheFile -Force
    }
    # Sometimes this path wouldnt exist. Try to initialize it here
    $dirPath = [io.Path]::GetDirectoryName($cacheFile)
    if (!(Test-Path $dirPath)) {
        New-Item -ItemType Directory -force -Path $dirPath
    }
}

function Get-ProductRelease
{
    <#
    .DESCRIPTION
        Gets the Product Release manifest for the specified Version
 
    .PARAMETER version
        The requested release version
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $catalog = Get-Catalog -moduleName $moduleName
    foreach($productRelease in $catalog.ProductStreamRefs[0].ProductReleases)
    {
        if ($productRelease.Version -ieq $version)
        {
            return $productRelease
        }
    }

    throw [CustomException]::new(($([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_release_not_found , $version))), $true)
}

function Get-ProductReleasesUptoVersion
{
    <#
    .DESCRIPTION
        Get all of the Product Release Manifests up to the specified Version
 
    .PARAMETER version
        Requested version
    #>

    param (
        [String]$version,

        [Parameter(Mandatory=$true)]
        [String]$moduleName
    )

    # Assumption here is that the ordering of values in catalog release stream is latest at top.
    $releaseList = @()
    $catalog = Get-Catalog -moduleName $moduleName
    foreach($productRelease in $catalog.ProductStreamRefs[0].ProductReleases)
    {
        $releaseList += $productRelease
        if ($productRelease.Version -ieq $version)
        {
            break
        }
    }

    if ($releaseList.Count -eq 0) {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_not_found , $version))
    }

    return $releaseList
}

function Get-LatestRelease
{
    <#
    .DESCRIPTION
        Get the latest release of AksHci by refreshing the catalog and returning the latest release
        from the updated catalog
 
    .PARAMETER moduleName
        Module name
    #>

    
    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $catalog = Get-LatestCatalog -moduleName $moduleName
    if (-not $catalog)
    {
        throw [CustomException]::new(($($GenericLocMessage.comm_no_catalog_retreive)), $true)
    }

    return $catalog.ProductStreamRefs[0].ProductReleases[0]
}

function Get-ReleaseDownloadParameters
{
    <#
    .DESCRIPTION
 
    .PARAMETER name
        Release name to be downloaded
 
    .PARAMETER version
        Release version to be downloaded
 
    .PARAMETER destination
        The destination for the download
 
    .PARAMETER parts
        How many download parts to use (concurrency)
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $name,

        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter()]
        [String] $destination,

        [Parameter(Mandatory=$false)]
        [Int] $parts = 1,

        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $provider = Get-DownloadProvider -module $moduleName
    if(0 -eq $parts)
    {
        $parts = 1
    }
    $downloadParams = @{
        Provider = $provider
        Name = $name
        Version = $version
        Destination = $destination
        Parts = $parts
    }

    if ($global:config[$moduleName]["useStagingShare"] -or ($global:config[$moduleName]["offlineDownload"] -and $global:config[$moduleName]["downloadCompleted"]))
    {
        $downloadParams.Add("Endpoint", $global:config[$moduleName]["stagingShare"])
        $downloadParams.Add("CatalogName", $global:config[$moduleName]["catalog"])
        $downloadParams.Add("Audience", $global:config[$moduleName]["ring"])
    }

    return $downloadParams
}

function Get-DownloadProvider
{
    <#
    .DESCRIPTION
        Returns an appropriate download provider based on the current module configuration
         
    .PARAMETER moduleName
        Module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String]$moduleName
    )

    if ($global:config[$moduleName]["useStagingShare"])
    {
        $endpoint = $($global:config[$moduleName]["stagingShare"])
        if ($endpoint.StartsWith("http"))
        {
            return "http"
        }
        elseif ($endpoint.StartsWith("//") -or $endpoint.StartsWith("\\") -or $endpoint.Contains(":"))
        {
            return "local"
        }
        else
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_unsupported_endpoint , $endpoint))
        }
    }

    return "sfs"
}

#endregion

#region File download helpers

function Test-AuthenticodeBinaries
{
    <#
    .DESCRIPTION
        Validates binary integrity via authenticode
 
    .PARAMETER workingDir
        Location of the binaries to be tested
 
    .PARAMETER binaries
        The list of binaries to be tested
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $workingDir,

        [Parameter(Mandatory=$true)]
        [string[]] $binaries
    )

    Write-Status $($GenericLocMessage.comm_verify_binaries) -moduleName $global:DownloadModule

    $workingDir = $workingDir -replace "\/", "\"
    foreach ($binary in $binaries)
    {
        $name = $("$workingDir/$binary") -replace "\/", "\"
        Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_check_binary_sign , $name)) -moduleName $global:DownloadModule

        $auth = Get-AuthenticodeSignature -FilePath $name
        if (($global:expectedAuthResponse.status -ne $auth.status) -or ($global:expectedAuthResponse.SignatureType -ne $auth.SignatureType))
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_authenticode_failed , "Binary "+$name, $($global:expectedAuthResponse.status), $($global:expectedAuthResponse.SignatureType), $($auth.status), $($auth.SignatureType)))
        }

        Write-SubStatus $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_verify_sign , $name)) -moduleName $global:DownloadModule
    }
}

#endregion

#region EventLog

function New-ModuleEventLog
{
    <#
    .DESCRIPTION
        Tests if the desired product/module is installed (or installing). Note that we consider some
        failed states (e.g. UninstallFailed) to represent that the product is still installed, albeit
        in a unknown/failed state.
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )
            
    New-EventLog -LogName "AKSHCI" -Source $moduleName -ErrorAction Ignore
}

function Write-ModuleEventException
{
    <#
    .DESCRIPTION
        Write Exception message with stacktrace to event log
 
    .PARAMETER moduleName
        The module name to test for installation state
 
    .PARAMETER exception
        The message that has to be logged
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName,

        [Parameter(Mandatory=$true)]
        [String] $message,

        [Parameter(Mandatory=$true)]
        [Object] $exception
    )

    $errorMessage = $message
    $errorMessage += "`r`nException [" + $exception.Exception.Message + "]"
    $errorMessage += "`r`nStacktrace [" + $($exception.ScriptStackTrace) + "]"

    if ($exception.Exception.InnerException)
    {
        $errorMessage += "`r`nInnerException[" +  $exception.Exception.InnerException.Message + "]"
    }

    Write-ModuleEventLog -moduleName $moduleName -message $errorMessage -eventId 2 -entryType Warning
    Write-Warning $errorMessage

    return $errorMessage
}

function Write-ModuleEventLog
{
    <#
    .DESCRIPTION
        Tests if the desired product/module is installed (or installing). Note that we consider some
        failed states (e.g. UninstallFailed) to represent that the product is still installed, albeit
        in a unknown/failed state.
 
    .PARAMETER moduleName
        The module name to test for installation state
 
    .PARAMETER message
        The message that has to be logged
 
    .PARAMETER entryType
        Activity name to use when writing progress
         
    .PARAMETER eventId
        Activity name to use when writing progress
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName,

        [Parameter(Mandatory=$true)]
        [String] $message,

        [Parameter(Mandatory=$true)]
        [System.Diagnostics.EventLogEntryType] $entryType,
    
        [Parameter(Mandatory=$true)]
        [int] $eventId
    )
    $start = 0
    $end = 32760
    $tmpMessage = $message

    # Try to trim down the message if its too big
    if ($message.Length -gt $end)
    {
        $tmpMessage = $message.Substring($start, $end)
    }

    Write-EventLog -LogName "AKSHCI" -Source $moduleName -EventID $eventId -EntryType $entryType -Message $tmpMessage
}

#endregion

#region Validation functions

function Test-ValidCIDR
{
    <#
    .DESCRIPTION
        This function validates that CIDR is valid by checking:
        1. That the CIDR notation is correct (IP/prefix)
        2. That the IP is valid.
        3. That the prefix length is between 1 and 30
 
    .PARAMETER CIDR
        The CIDR in the form IP/prefixlength. E.g. 10.0.0.0/24
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $CIDR
    )

    $x = $CIDR.Split('/')
    if ($x.Length -ne 2) {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_cidr , $CIDR))
    }
    $ip = $x[0]
    $prefix = [int]::Parse($x[1])

    Test-ValidEndpoint -endpoint $ip

    # The minimum prefix length is /30 (which leaves 2 usable ip addresses. 1 for mgmt cluster VM, 1 for mgmt k8s VIP)
    if (($prefix -lt 1) -or ($prefix -gt 30)) {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_prefix_len , $prefix))
    }
}

function Test-ValidPool
{
    <#
    .DESCRIPTION
        This function validates that the pool start/end are valid by checking:
        1. That the pool start/end are valid ip addresses
        2. That the pool end comes after or is equal (1 IP) to the pool start.
        3. If CIDR is also given, it validates the range is within the CIDR.
 
    .PARAMETER PoolStart
        The starting ip address of the pool
 
    .PARAMETER PoolEnd
        The ending ip address of the pool
 
    .PARAMETER CIDR
        The CIDR from where the pool should come from.
        Note that if CIDR is given, it is expected to have already been validated.
    #>

    param (
        [Parameter(Mandatory=$true)]
        [string] $PoolStart,
        [Parameter(Mandatory=$true)]
        [string] $PoolEnd,
        [string] $CIDR
    )

    Test-ValidEndpoint -endpoint $PoolStart
    Test-ValidEndpoint -endpoint $PoolEnd

    $valid = [AKSHCI.IPUtilities]::ValidateRange($PoolStart, $PoolEnd)
    if (-not $valid) {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_ip_pool_range , $PoolStart, $PoolEnd))
    }
    if ($CIDR) {
        $valid = [AKSHCI.IPUtilities]::ValidateRangeInCIDR($PoolStart, $PoolEnd, $CIDR)
        if (-not $valid) {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_ip_pool_not_in_cidr , $PoolStart, $PoolEnd, $CIDR))
        }    
    } 
}

function Test-VipPoolAgainstVnicAddressPrefix
{
    <#
    .DESCRIPTION
        This functions validates that the pool belongs to the subnet by:
        1. Finding the adapters attached to the switch (`$switchName`)
        2. Filtering by internet facing adapters
        3. Checking the subnets of the adapters against the pool
 
    .PARAMETER switchName
        The name of the switch
     
    .PARAMETER multiNode
        Whether Multi Node Configuration
 
    .PARAMETER PoolStart
        The starting ip address of the pool
 
    .PARAMETER PoolEnd
        The ending ip address of the pool
     
    .PARAMETER vlanID
        Vlan ID
 
    .PARAMETER nodeName
        The node to execute on.
    #>

    [cmdletbinding()]
    param(
        [parameter(Mandatory)]
        [String] $switchName,

        [Switch] $multiNode,

        [parameter(Mandatory)]
        [String] $PoolStart,

        [parameter(Mandatory)]
        [String] $PoolEnd,

        [Parameter(Mandatory=$false)]
        [int] $vlanID = $global:defaultVlanID,

        [parameter(Mandatory)]
        [String] $nodeName
    )

    $isVlanSetup = $($vlanID -ne $global:defaultVlanID)

    $inputArgs = @(
        $nodeName,
        $switchName,
        $multiNode.IsPresent,
        $isVlanSetup,
        $vlanID
    )

    $subnetRanges = Invoke-Command -ComputerName $nodeName {
        $hostName = $args[0]
        $switchName = $args[1]
        $isMultiNode = $args[2]
        $isVlanSetup = $args[3]
        $vlanID = $args[4]

        # validate if switch exists
        Write-Verbose $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_checking_for_virtual_switch_with_name, $switchName, $hostName))
        $switch = $null
        # For Multi Node configuration the vSwitch needs to be external
        if ($isMultiNode)
        {
            $switch = Get-VMSwitch -Name $switchName -SwitchType External -ErrorAction SilentlyContinue
            if ($null -eq $switch)
            {
                throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMsg.comm_external_switch_missing, $switchName, $hostName))
            }
        }
        else
        {
            $switch = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue
            if ($null -eq $switch)
            {
                throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMsg.comm_switch_missing, $switchName, $hostName))
            }
        }

        [string[]] $subnetRanges = @()
        [hashtable] $adapterDeviceIdToMac = @{}

        # Populate Adapter device id to MAC Address map
        (Get-VMNetworkAdapter -ManagementOS -SwitchName "$switchName" -ErrorAction SilentlyContinue) | ForEach-Object {
            $adapterDeviceIdToMac.Add($_.DeviceId, $_.MacAddress)
        }

        if ($adapterDeviceIdToMac.Count -eq 0)
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMsg.comm_adapter_not_connected, $switchName, $hostName))
        }

        $adapters = (Get-NetAdapter | Where-Object { $adapterDeviceIdToMac.Keys -contains $_.InterfaceGuid })

        # Get internet facing adapters; only for multi node
        if ($isMultiNode)
        {
            $adapters = $adapters | Where-Object {
                $null -ne (Get-NetRoute "0.0.0.0/0" -InterfaceIndex $_.ifIndex -ErrorAction SilentlyContinue) # Route 0.0.0.0/0 is always pointing to the internet
            }

            if ($adapters.Count -eq 0)
            {
                throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMsg.comm_adapter_not_found, $hostName))
            }
        }

        foreach ($adapter in $adapters)
        {
            if ($isVlanSetup)
            {
                # If on a different vlan; skip validation
                # TODO validate in case different vlan
                if (($null -eq $adapter.VlanID) -or ($adapter.VlanID -ne $vlanID))
                {
                    continue
                }
            }
            $adapterInstanceID = $adapter.InterfaceGuid
            $mac = $adapterDeviceIdToMac[$adapterInstanceID]
            $macAddressBytes = [System.Net.NetworkInformation.PhysicalAddress]::Parse($mac).GetAddressBytes()
            $mac = (($macAddressBytes|ForEach-Object ToString X2) -join ':')
            # docs : https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-networkadapterconfiguration
            $adapterConfig = (Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object IPEnabled | Where-Object MACAddress -eq $mac  | Select-Object IPSubnet, IpAddress, DHCPEnabled)

            # For static IP configuration skip validation
            # TODO
            if ($adapterConfig.DHCPEnabled -eq $false)
            {
                continue
            }

            for($i = 0; $i -lt $adapterConfig.IpAddress.Count; $i++) {
                [IPAddress] $ip = $adapterConfig.IpAddress[$i]
                if ($ip.AddressFamily -ine "InterNetwork")  # Skip if not IPv4
                {
                    continue
                }

                [IPAddress] $netmask = $adapterConfig.IPSubnet[$i]
                # convert mask to cidr
                $octets = $netmask.IPAddressToString.Split('.')
                $cidr=0
                foreach($octet in $octets)
                { 
                    while(0 -ne $octet)
                    { 
                        $octet = ($octet -shl 1) -band [byte]::MaxValue; 
                        $cidr++ 
                    } 
                }

                $ipAddressBytes = $ip.GetAddressBytes()
                $netmaskAddressbytes = $netmask.GetAddressBytes()
                $startIP = (((0..3) | ForEach-Object { $ipAddressBytes[$_] -band $netmaskAddressbytes[$_] }) -join '.')
                $subnetRange = $startIP+'/'+$cidr

                $subnetRanges += $subnetRange
            }
        }

        return $subnetRanges
    } -ArgumentList $inputArgs 

    foreach ($subnetRange in $subnetRanges)
    {
        if([AKSHCI.IPUtilities]::ValidateRangeInCIDR($PoolStart, $PoolEnd, $subnetRange)) # if any one range is valid, pass, return early
        {
            return
        }
    }

    if ($subnetRanges.Count -ne 0)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMsg.comm_poolstart_poolend_outside_subnet_range, $PoolStart, $PoolEnd, $switchName))        
    }
}


#endregion

#region Proxy server functions

function Set-ProxyConfiguration
{
    <#
    .DESCRIPTION
        Sets the proxy server configuration for a module
 
    .PARAMETER proxySettings
        Proxy server settings
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter()]
        [ProxySettings] $proxySettings,

        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $http = ""
    $https = ""
    $noProxy = ""
    $certFile = ""
    $user = ""
    $pass = ""

    if ($proxySettings)
    {
        $http = $proxySettings.HTTP
        $https = $proxySettings.HTTPS
        $noProxy = $proxySettings.NoProxy
        $certFile = $proxySettings.CertFile

        Test-ProxyConfiguration -http $http -https $https -noProxy $noProxy -certFile $certFile

        if ($proxySettings.Credential.Username)
        {
            $user = $proxySettings.Credential.UserName
        }
        
        if ($proxySettings.Credential.Password)
        {
            $pass = $proxySettings.Credential.Password | ConvertFrom-SecureString -Key $global:credentialKey
        }
    }

    Set-ConfigurationValue -name "proxyServerUsername" -value $user -module $moduleName
    Set-ConfigurationValue -name "proxyServerPassword" -value $pass -module $moduleName
    Set-ConfigurationValue -name "proxyServerHTTP" -value $http -module $moduleName
    Set-ConfigurationValue -name "proxyServerHTTPS" -value $https -module $moduleName
    Set-ConfigurationValue -name "proxyServerNoProxy" -value $noProxy -module $moduleName
    Set-ConfigurationValue -name "proxyServerCertFile" -value $certFile -module $moduleName

    Initialize-ProxyEnvironment -moduleName $moduleName
}

function Initialize-ProxyEnvironment
{
    <#
    .DESCRIPTION
        Applies proxy settings to the current process environment
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $proxySettings = Get-ProxyConfiguration -moduleName $moduleName
    if ($proxySettings.HTTP -or $proxySettings.HTTPS)
    {
        Set-DownloadSdkProxy -Http "$($proxySettings.HTTP)" -Https "$($proxySettings.HTTPS)" -NoProxy "$($proxySettings.NoProxy)"
    }
}

function Get-ProxyConfiguration
{
    <#
    .DESCRIPTION
        Returns a custom PSObject containing the complete HTTP, HTTPS, NoProxy, and CertFile setting strings
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $proxyHTTP = ""
    $proxyHTTPS = ""
    $proxyCertName = ""
    $proxyCertContentB64 = ""

    if ($global:config[$moduleName]["proxyServerHTTP"])
    {
        $proxyHTTP = Get-ProxyWithCredentials -proxyServer $($global:config[$moduleName]["proxyServerHTTP"]) -proxyUsername $($global:config[$moduleName]["proxyServerUsername"]) -proxyPass $($global:config[$moduleName]["ProxyServerPassword"])
    }

    if ($global:config[$moduleName]["proxyServerHTTPS"])
    {
        $proxyHTTPS = Get-ProxyWithCredentials -proxyServer $($global:config[$moduleName]["proxyServerHTTPS"]) -proxyUsername $($global:config[$moduleName]["proxyServerUsername"]) -proxyPass $($global:config[$moduleName]["ProxyServerPassword"])
    }

    if (($global:config[$moduleName]["proxyServerCertFile"]) -and (Test-Path $global:config[$moduleName]["proxyServerCertFile"]))
    {
        $content = Get-Content -Encoding Byte -Path $global:config[$moduleName]["proxyServerCertFile"]
        if ($content)
        {
            $proxyCertName = "proxy-cert.crt"
            $proxyCertContentB64 = [Convert]::ToBase64String($content)
        }
    }

    $proxyConfig = [ordered]@{
        'HTTP' = $proxyHTTP;
        'HTTPS' = $proxyHTTPS;
        'NoProxy' = $global:config[$moduleName]["proxyServerNoProxy"];
        'CertPath' = $global:config[$moduleName]["proxyServerCertFile"];
        'CertName' = $proxyCertName; 
        'CertContent' = $proxyCertContentB64
    }

    if ($($global:config[$moduleName]["proxyServerUsername"]) -and $($global:config[$moduleName]["ProxyServerPassword"]))
    {
        $securePass = $($global:config[$moduleName]["ProxyServerPassword"]) | ConvertTo-SecureString -Key $global:credentialKey
        $credential = New-Object System.Management.Automation.PSCredential -ArgumentList $($global:config[$moduleName]["proxyServerUsername"]), $securePass

        $proxyConfig.Add("Credential", $credential)
    }

    $result = @()
    $result += New-Object -TypeName PsObject -Property $proxyConfig

    return $result
}

function Get-ProxyWithCredentials
{
    <#
    .DESCRIPTION
        Returns a complete proxy string with credentials in the URI format (e.g. http://user:pass@server.com:8080)
 
    .PARAMETER proxyServer
        Proxy server string URI
 
    .PARAMETER proxyUsername
        Proxy server username
 
    .PARAMETER proxyPass
        Proxy server password (this is a secure string representation, not plaintext)
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $proxyServer,

        [Parameter()]
        [String] $proxyUsername,

        [Parameter()]
        [String] $proxyPass
    )

    $uri = Test-ValidProxyServer -proxyServer $proxyServer
    $proxyString = $($uri.Scheme + "://")

    if ($proxyUsername -and $proxyPass)
    {
        $securePass = $proxyPass | ConvertTo-SecureString -Key $global:credentialKey
        $credential = New-Object System.Management.Automation.PSCredential -ArgumentList $proxyUsername, $securePass
        $proxyString += $($credential.UserName + ":" + $credential.GetNetworkCredential().Password + "@")
    }

    $proxyString += $uri.Authority

    if ($uri.IsDefaultPort)
    {
        $proxyString += (":" + $uri.Port)
    }

    return $proxyString
}

function Test-ProxyConfiguration
{
    <#
    .DESCRIPTION
        Validates the provided proxy server configuration. On failure, would throw.
 
    .PARAMETER http
        HTTP proxy server configuration
 
    .PARAMETER https
        HTTPS proxy server configuration
 
    .PARAMETER noProxy
        Proxy server exemption/bypass list
 
    .PARAMETER certFile
        Path to a CA certificate file used to establish trust with a HTTPS proxy server
    #>


    param (
        [Parameter()]
        [String] $http,

        [Parameter()]
        [String] $https,

        [Parameter()]
        [String] $noProxy,

        [Parameter()]
        [String] $certFile
    )

    if ($http)
    {
        Test-ValidProxyServer -proxyServer $http | Out-Null
    }

    if ($https)
    {
        Test-ValidProxyServer -proxyServer $https | Out-Null
    }

    $http_proxy_env = [System.Environment]::GetEnvironmentVariable("HTTP_PROXY", "Machine")
    $https_proxy_env = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY", "Machine")
    $no_proxy_env = [System.Environment]::GetEnvironmentVariable("NO_PROXY", "Machine")

    if ($http_proxy_env -or $https_proxy_env) {
        if (-not $no_proxy_env) {
            throw [CustomException]::new(($($GenericLocMessage.comm_missing_NoProxy_configuration )), $true)
        }
    }

    if (($no_proxy_env -and $no_proxy_env.contains("*")) -or ($noProxy -and $noProxy.contains("*"))) {
        throw [CustomException]::new(($($GenericLocMessage.comm_NoProxy_list_invalid_wildcard )), $true)
    }

    if ($certFile)
    {
        if (-not (Test-Path $certFile))
        {
            throw [CustomException]::new(($([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_proxy_cert_not_found , $certFile))), $true)
        }
    }
}

function Test-ValidProxyServer
{
    <#
    .DESCRIPTION
        Validates the provided proxy server string
  
    .PARAMETER proxyServer
        Proxy server string in absolute URI format (e.g. http://proxy.com:3128)
    #>


    param (
        [String] $proxyServer
    )

    $uri = $null
    $result = [System.URI]::TryCreate($proxyServer, [System.UriKind]::Absolute, [ref]$uri)
    if (-not $result)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_proxy_invalid_uri , $proxyServer))
    }

    switch($uri.Scheme)
    {
        $([System.URI]::UriSchemeHttp) {
            break
        }
        $([System.URI]::UriSchemeHttps) {
            break
        }
        Default {            
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_proxy_invalid_uri , $proxyServer))
        }
    }

    return $uri
}

function Test-ValidAutoScalerProfileConfig
{
    <#
    .DESCRIPTION
        Validates the keys provided in a AutoScalerProfile config hashtable.
 
    .PARAMETER AutoScalerProfileConfig
        An AutoScalerProfile config
    #>

    param (
        [hashtable] $AutoScalerProfileConfig
    )

    if ($AutoScalerProfileConfig.Count -lt 1)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $KvaLocMessage.kva_empty_autoscalerprofile_config))
    }

    foreach($k in $AutoScalerProfileConfig.Keys)
    {
        if (-not $global:autoScalerProfileConfigToKvaYamlKeys.ContainsKey($k))
        {
            throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $KvaLocMessage.kva_invalid_autoscalerprofile_config_key, $k))
        }
    }
    return $true
}

function New-VipPoolSettings
{
    <#
    .DESCRIPTION
        A wrapper around [New-VipPoolSettings]::new that Validates parameters before returning a New VipPoolSettings object
 
    .PARAMETER name
        The name of the vip pool
 
    .PARAMETER vipPoolStart
        The starting ip address to use for the vip pool.
 
    .PARAMETER vippoolend
        The ending ip address to use for the vip pool.
 
    .OUTPUTS
        VipPoolSettings object
 
    .EXAMPLE
         
    #>


    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string] $name,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $vipPoolStart,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String] $vipPoolEnd

    )

    Test-ValidPool -PoolStart $vipPoolStart -PoolEnd $vipPoolEnd 
    
    return [VipPoolSettings]::new($name, $vipPoolStart, $vipPoolEnd)
}

#endregion

#

function Get-ImageReleaseManifest
{
    <#
    .DESCRIPTION
        Discovers the requested image and returns the release manifest for it
 
    .PARAMETER imageVersion
        Image release version to target
 
    .PARAMETER operatingSystem
        Image operating system to target
 
    .PARAMETER k8sVersion
        Kubernetes version to target
 
    .PARAMETER moduleName
        The module name
    #>


    param(
        [parameter(Mandatory=$true)]
        [String] $imageVersion,

        [parameter(Mandatory=$true)]
        [String] $operatingSystem,

        [Parameter(Mandatory=$true)]
        [String] $k8sVersion,

        [Parameter(Mandatory=$true)]
        [String] $moduleName
    )

    $k8sVersion = $k8sVersion.TrimStart("v")

    $productRelease = Get-ProductRelease -version $imageVersion -moduleName $moduleName
    foreach($releaseStream in $productRelease.ProductStreamRefs)
    {
        foreach($subProductRelease in $releaseStream.ProductReleases)
        {
            $vhdInfo = Get-ImageReleaseVhdInfo -release $subProductRelease
            if (-not $vhdInfo)
            {
                continue
            }

            if ($vhdInfo.CustomData.BaseOSImage.OperatingSystem -ine $operatingSystem)
            {
                continue
            }

            foreach($pkg in $vhdInfo.CustomData.K8SPackages)
            {
                if ($pkg.Version -ieq $k8sVersion)
                {
                    return $subProductRelease
                }
            }
        }
    }

    throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_image_not_found, $imageVersion, $operatingSystem, $k8sVersion))
}

function Get-ImageReleaseVhdInfo
{
    <#
    .DESCRIPTION
        Discovers and returns vhd information for the specified image release
 
    .PARAMETER imageRelease
        Image release manifest
    #>


    param(
        [parameter(Mandatory=$True)]
        [PSCustomObject] $release
    )

    foreach ($fileRelease in $release.ProductFiles)
    {
        if ($fileRelease.CustomData.Type -ieq "vhd")
        {
            return $fileRelease
        }
    }

    return $null
}

function Get-ImageRelease
{
    <#
    .DESCRIPTION
        Download the specified image release.
 
    .PARAMETER imageRelease
        Image release manifest
 
    .PARAMETER imageDir
        Directory for local image store.
 
    .PARAMETER moduleName
        The module name
    #>


    param(
        [parameter(Mandatory=$True)]
        [PSCustomObject] $imageRelease,

        [parameter(Mandatory=$True)]
        [String] $imageDir,

        [parameter(Mandatory=$True)]
        [String] $moduleName
    )

    $downloadpath = "$imageDir\$([System.IO.Path]::GetRandomFileName().Split('.')[0])"
    New-Item -ItemType Directory -Force -Confirm:$false -Path $downloadpath | Out-Null

    try
    {
        $vhdInfo = Get-ImageReleaseVhdInfo -release $imageRelease
        $k8sVersion = $vhdInfo.CustomData.K8SPackages[0].Version
        $imageGalleryName = Get-KubernetesGalleryImageName -imagetype $vhdInfo.CustomData.BaseOSImage.OperatingSystem -k8sVersion $k8sVersion

        $imageVersionCurrent = $imageRelease.Version
        $imageVersionManifestPath = "$ImageDir\$imageGalleryName.json"
        $destinationpath = $("$ImageDir\$imageGalleryName.vhdx" -replace "\/", "\")

        if (Test-Path $imageVersionManifestPath)
        {
            $OldImageData = get-content $imageVersionManifestPath | ConvertFrom-Json
            $OldImageVersion = $OldImageData.Version
            Write-SubStatus -moduleName $moduleName "Existing image $imageVersionManifestPath has version $OldImageVersion . Requested version is $imageVersionCurrent"

            if ($imageVersionCurrent -ieq $OldImageVersion)
            {
                if (Test-Path -Path $destinationpath)
                {
                    Write-SubStatus -moduleName $moduleName $($GenericLocMessage.comm_existing_image_upto_date)
                    return $destinationpath
                }

                Write-SubStatus -moduleName $moduleName $($GenericLocMessage.comm_existing_image_not_present)
            }
        }

        Write-Status -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_download_image_with_version, $($imageRelease.ProductStream), $imageVersionCurrent, $downloadPath))
        $downloadParams = Get-ReleaseDownloadParameters -name $imageRelease.ProductStream -version $imageVersionCurrent -destination $downloadPath -parts 10 -moduleName $moduleName
        $releaseInfo = Get-DownloadSdkRelease @downloadParams

        if ($global:config[$moduleName]["useStagingShare"] -or ($global:config[$moduleName]["offlineDownload"] -and $global:config[$moduleName]["downloadCompleted"]))
        {
            $imageFile = $releaseInfo.Files[0] -replace "\/", "\"
        }
        else
        {
            $imageFile = Expand-SfsImage -files $releaseInfo.Files -destination $downloadPath -workingDirectory $downloadPath -moduleName $moduleName
        }

        $imageJson = $imageRelease | ConvertTo-Json -depth 100
        Set-Content -path $imageversionManifestPath -value $imageJson -encoding UTF8 -Confirm:$false

        if (test-path $destinationpath)
        {
            Remove-Item $destinationpath -Force -Confirm:$false
        }

        Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_moving_image, $imageFile, $destinationpath))
        Move-Item -Path $imageFile -Destination $destinationpath -Confirm:$false
    }
    finally
    {
        Remove-Item -Force $downloadpath -Recurse -Confirm:$false
    }

    return $destinationpath
}

function Expand-SfsImage
{
    <#
    .DESCRIPTION
        Expand and verify a SFS image download using authenticode.
        NOTE: This is temporary until cross-platform signing is available in Download SDK.
 
    .PARAMETER files
        The downloaded image files (expected to be a image file zip and a companion cab).
 
    .PARAMETER destination
        Destination for the expanded image file.
 
    .PARAMETER workingDirectory
        Working directory to use for zip and cab file expansion.
 
    .PARAMETER moduleName
        The module name
    #>


    param(
        [Parameter(Mandatory=$True)]
        [String[]] $files,

        [Parameter(Mandatory=$True)]
        [String] $destination,

        [Parameter(Mandatory=$True)]
        [String] $workingDirectory,

        [parameter(Mandatory=$True)]
        [String] $moduleName
    )

    Write-Status -moduleName $moduleName $($GenericLocMessage.comm_verifying_image_companion_file_download)

    [String[]]$cabfile = $files | Where-Object { $_ -match "\.cab$"}
    if (($null -eq $cabfile) -or ($cabfile.count -ne 1))
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_wrong_cab_file_count, $cabfile.count))
    }

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_verify_authenticode_signature, $cabFile))
    $auth = Get-AuthenticodeSignature -filepath $cabfile
    if (($global:expectedAuthResponse.status -ne $auth.status) -or ($global:expectedAuthResponse.SignatureType -ne $auth.SignatureType))
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_authenticode_failed, "KVA image companion file", $global:expectedAuthResponse.status, $global:expectedAuthResponse.SignatureType, $auth.status, $auth.SignatureType))
    }

    Write-Status -moduleName $moduleName $($GenericLocMessage.comm_expanding_image_companion_file)
    $expandDir = "$WorkingDirectory\expand_" + [System.IO.Path]::GetRandomFileName().Split('.')[0]
    New-Item -ItemType Directory -Force -Confirm:$false -Path $expandDir | Out-Null
    $expandoutput = expand.exe $cabfile $expandDir
    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_expand_output, $expandoutput))
    $manifest = Get-ChildItem $expandDir | select-object -first 1
    $manifestcontents = get-content $manifest.fullname | convertfrom-json

    $packageAlgo = $manifestcontents.PackageVerification.VerificationDescriptor.Algorithm
    $packageHash = $manifestcontents.PackageVerification.VerificationDescriptor.FileHash
    $packageName = $manifestcontents.PackageVerification.VerificationDescriptor.Filename

    Write-Status -moduleName $moduleName $($GenericLocMessage.comm_verifying_image_file_download)
    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_verification_request, $packageName, $packageAlgo, $packageHash))

    [string[]]$imagezip = $files | Where-Object { $_ -match "$packageName$"}
    if ($null -eq $imagezip)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_unable_to_locate_image_file, $packageName))
    }

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_calculating_hash_for_archive, $packageAlgo, $imagezip[0]))
    $hash = Get-Base64Hash -file $imagezip[0] -algorithm $packageAlgo -moduleName $moduleName
    if ($packageHash -ne $hash)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_unexpected_hash, $moduleName, $packageHash, $($imagezip[0]), $hash))
    }

    Write-Status -moduleName $moduleName $($GenericLocMessage.comm_expanding_image_file_archive)
    $contentsDir = "$expandDir\contents"
    New-Item -ItemType Directory -Force -Confirm:$false -Path $contentsdir | Out-Null
    Expand-Archive -path $imagezip[0] -destinationpath $contentsDir -Confirm:$false | Out-Null
    Remove-Item $imagezip[0] -Confirm:$false

    [System.IO.FileInfo[]]$workimage = Get-ChildItem -r $contentsDir | Where-Object { (-not $_.psiscontainer) -and ($_.name -match "\.vhdx$")}
    if ($workimage.count -ne 1)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_invalid_file_count_expansion, $($workimage.count)))
    }
    $content0 = $manifestcontents.ContentVerification[0].VerificationDescriptor

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_calculating_hash_for_file, $packageAlgo, $workimage[0].fullname))

    $hash = Get-Base64Hash -file $workimage[0].fullname -algorithm $content0.algorithm -moduleName $moduleName
    if ($content0.FileHash -ne $hash)
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_unexpected_hash, $moduleName, $($content0.FileHash), $($workimage[0].fullname), $hash))
    }

    $image = $("$destination\" + $workimage[0].name)

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_moving_image_file, $image))
    Move-item -Path $workimage[0].fullname -Destination $image -Confirm:$false

    return $image
}
function Get-Base64Hash
{
    <#
    .DESCRIPTION
        Obtain the base64 byte hash of the specified file. Used to verify SFS binaries
 
    .PARAMETER file
        File to generate the base64 hash for
 
    .PARAMETER algorithm
        Hashing algorithm to use
 
    .PARAMETER moduleName
        The module name
    #>

    param (
        [Parameter(Mandatory=$True)]
        [string] $file,

        [Parameter(Mandatory=$True)]
        [string] $algorithm,

        [parameter(Mandatory=$True)]
        [String] $moduleName
    )

    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_generating_base64_hash, $file, $algorithm))
    $diskhash_hex = Get-FileHash -algo $algorithm -path $file

    [byte[]] $diskhash_bin = for ([int]$i = 0; $i -lt $diskhash_hex.hash.length; $i += 2) {
        [byte]::parse($diskhash_hex.hash.substring($i, 2), [System.Globalization.NumberStyles]::HexNumber)
    }

    return [convert]::ToBase64String($diskhash_bin)
}

function Get-ReleaseContent
{
    <#
    .DESCRIPTION
        Download all required files and packages for the specified release
 
    .PARAMETER version
        Release version
 
    .PARAMETER destination
        Destination directory for the content
 
    .PARAMETER activity
        Activity name to use when writing progress
 
    .PARAMETER moduleName
        The module name
    #>


    param (
        [Parameter(Mandatory=$true)]
        [String] $version,

        [Parameter(Mandatory=$true)]
        [string] $destination,

        [Parameter()]
        [String] $activity = $MyInvocation.MyCommand.Name,

        [Parameter(Mandatory=$true)]
        [string] $moduleName
    )

    $akshci = $false

    $destination = $destination -replace "\/", "\"

    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_discovering_release_content, $moduleName))
    $productRelease = Get-ProductRelease -version $version -moduleName $moduleName

    # find requested release
    foreach($releaseStream in $productRelease.ProductStreamRefs)
    {
        foreach($subProductRelease in $releaseStream.ProductReleases)
        {   
            if ($subProductRelease.ProductName -ieq "client-cred-plugin") 
            {
                continue
            }
            
            $vhdInfo = Get-ImageReleaseVhdInfo -release $subProductRelease
                
            if (-not $vhdInfo)
            {
                $akshci = $true
                $name = $subProductRelease.ProductName.Substring(0, 3)
                $versionManifestPath = [io.Path]::Combine($destination, $("$name-release.json"))
    
                Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_downloading_release_content_to, $name, $destination))
    
                $downloadParams = Get-ReleaseDownloadParameters -name $subProductRelease.ProductStream -version $subProductRelease.Version -destination $destination -parts 3 -moduleName $moduleName
                $releaseInfo = Get-DownloadSdkRelease @downloadParams
    
                if (-not ($global:config[$moduleName]["useStagingShare"]) -or ($global:config[$moduleName]["offlineDownload"] -and -not $global:config[$moduleName]["downloadCompleted"]))
                {
                    if ($releaseInfo.Files.Count -ne 1)
                    {
                        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_wrong_release_files_count, $moduleName, $releaseInfo.Files.Count))
                    }
    
                    $packagename = $releaseInfo.Files[0] -replace "\/", "\"
    
                    # Temporary until cross-platform signing is available
                    $auth = Get-AuthenticodeSignature -filepath $packagename
                    if (($global:expectedAuthResponse.status -ne $auth.status) -or ($auth.SignatureType -ne $global:expectedAuthResponse.SignatureType))
                    {
                        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_authenticode_failed, $global:expectedAuthResponse.status, $global:expectedAuthResponse.SignatureType, $auth.status, $auth.SignatureType))
                    }
    
                    Write-StatusWithProgress -activity $activity -moduleName $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_expanding_package, $moduleName, $packagename, $destination))
                    $expandoutput = expand.exe -r $packagename $destination -f:*
                    Write-SubStatus -moduleName $moduleName $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.generic_expand_result, $expandoutput))
                }
    
                $versionJson = $subProductRelease | ConvertTo-Json -depth 100
                set-content -path $versionManifestPath -value $versionJson -encoding UTF8
            }
            else 
            {
                if ($vhdInfo.CustomData.BaseOSImage.OperatingSystem -ieq "Linux") 
                {
                    $k8sVersion = $subProductRelease.ProductFiles.CustomData.K8sPackages.Version
                } 
                else 
                {
                    $k8sVersion = $productRelease.CustomData.ManagementNodeImageK8sVersion
                }
    
                Write-StatusWithProgress -activity $activity -module $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_obtain_download_information_for_image, $subProductRelease.ProductName))
                $imageRelease = Get-ImageReleaseManifest -imageVersion $version -operatingSystem $vhdInfo.CustomData.BaseOSImage.OperatingSystem -k8sVersion $k8sVersion -moduleName $moduleName

                Write-Output $subProductRelease.ProductName

                if ($global:config[$moduleName]["offlineDownload"] -and $global:config[$moduleName]["downloadCompleted"])
                {
                    Write-StatusWithProgress -activity $activity -module $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_using_offline_download_image, $subProductRelease.ProductName))
                }
                else 
                {
                    Write-StatusWithProgress -activity $activity -module $moduleName -status $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_downloading_image, $subProductRelease.ProductName))
                }
                Get-ImageRelease -imageRelease $imageRelease -imageDir $destination -moduleName $moduleName
            }
        }
    }

    if(-not $akshci) 
    {
        throw $([System.String]::Format([System.Globalization.CultureInfo]::InvariantCulture, $GenericLocMessage.comm_no_release_content, $version))
    }

    return
}

#endregion
#region Multi-Admin common functions
function Get-UserSSHPublicKey {
    <#
    .DESCRIPTION
        Get the SSH Public Key that is set at the user scope to be used by the deployment
    #>


    $version = Get-AksHciVersion
    if ([version]$version -lt [version]$global:multiAdminRelease){
        return Get-SshPublicKey
    }
    
    $workingDir = $global:config[$global:MocModule]["workingDir"]
    if(!(Test-Path -Path "$env:USERPROFILE\.ssh")){
        New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh" | Out-Null
    }
    Copy-Item -Path "$workingDir\.ssh\akshci_rsa.pub" -Destination $($env:USERPROFILE + "\.ssh")
    Copy-Item -Path "$workingDir\.ssh\akshci_rsa" -Destination $($env:USERPROFILE + "\.ssh")
    return "$env:USERPROFILE\.ssh\akshci_rsa.pub"
}

function Get-UserSSHPrivateKey {
    <#
    .DESCRIPTION
        Get the SSH Private Key that is set at the user scope to be used by the deployment
    #>


    $version = Get-AksHciVersion
    if ([version]$version -lt [version]$global:multiAdminRelease){
        return Get-SshPrivateKey
    }

    $workingDir = $global:config[$global:MocModule]["workingDir"]
    if(!(Test-Path -Path "$env:USERPROFILE\.ssh")){
        New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh" | Out-Null
    }
    Copy-Item -Path "$workingDir\.ssh\akshci_rsa.pub" -Destination $($env:USERPROFILE + "\.ssh")
    Copy-Item -Path "$workingDir\.ssh\akshci_rsa" -Destination $($env:USERPROFILE + "\.ssh")
    return "$env:USERPROFILE\.ssh\akshci_rsa"

}

#endregion

# SIG # Begin signature block
# MIInngYJKoZIhvcNAQcCoIInjzCCJ4sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCVPnE2iWy9HuHa
# AqzHdezUVqc5J/9RJrGV2A4FVVetdqCCDYEwggX/MIID56ADAgECAhMzAAACzI61
# lqa90clOAAAAAALMMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAxWhcNMjMwNTExMjA0NjAxWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCiTbHs68bADvNud97NzcdP0zh0mRr4VpDv68KobjQFybVAuVgiINf9aG2zQtWK
# No6+2X2Ix65KGcBXuZyEi0oBUAAGnIe5O5q/Y0Ij0WwDyMWaVad2Te4r1Eic3HWH
# UfiiNjF0ETHKg3qa7DCyUqwsR9q5SaXuHlYCwM+m59Nl3jKnYnKLLfzhl13wImV9
# DF8N76ANkRyK6BYoc9I6hHF2MCTQYWbQ4fXgzKhgzj4zeabWgfu+ZJCiFLkogvc0
# RVb0x3DtyxMbl/3e45Eu+sn/x6EVwbJZVvtQYcmdGF1yAYht+JnNmWwAxL8MgHMz
# xEcoY1Q1JtstiY3+u3ulGMvhAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUiLhHjTKWzIqVIp+sM2rOHH11rfQw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDcwNTI5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAeA8D
# sOAHS53MTIHYu8bbXrO6yQtRD6JfyMWeXaLu3Nc8PDnFc1efYq/F3MGx/aiwNbcs
# J2MU7BKNWTP5JQVBA2GNIeR3mScXqnOsv1XqXPvZeISDVWLaBQzceItdIwgo6B13
# vxlkkSYMvB0Dr3Yw7/W9U4Wk5K/RDOnIGvmKqKi3AwyxlV1mpefy729FKaWT7edB
# d3I4+hldMY8sdfDPjWRtJzjMjXZs41OUOwtHccPazjjC7KndzvZHx/0VWL8n0NT/
# 404vftnXKifMZkS4p2sB3oK+6kCcsyWsgS/3eYGw1Fe4MOnin1RhgrW1rHPODJTG
# AUOmW4wc3Q6KKr2zve7sMDZe9tfylonPwhk971rX8qGw6LkrGFv31IJeJSe/aUbG
# dUDPkbrABbVvPElgoj5eP3REqx5jdfkQw7tOdWkhn0jDUh2uQen9Atj3RkJyHuR0
# GUsJVMWFJdkIO/gFwzoOGlHNsmxvpANV86/1qgb1oZXdrURpzJp53MsDaBY/pxOc
# J0Cvg6uWs3kQWgKk5aBzvsX95BzdItHTpVMtVPW4q41XEvbFmUP1n6oL5rdNdrTM
# j/HXMRk1KCksax1Vxo3qv+13cCsZAaQNaIAvt5LvkshZkDZIP//0Hnq7NnWeYR3z
# 4oFiw9N2n3bb9baQWuWPswG0Dq9YT9kb+Cs4qIIwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZczCCGW8CAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAsyOtZamvdHJTgAAAAACzDAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgfUYYB/7w
# rkI8v0oew00L7nD2MzfYwAR9r8I+VKVoxYUwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQBKEpPxJFR7/L+i8qfjZnfTXC3hpnH8U6rWxIZK5YR/
# DnZQPXzVo1a1O24gUDdug5WPSt1J1sez4sD3dbCK1dJUEmTEZs7+8Ct6f5yEYzi2
# FD/03LLT1hmrEhoG1YM3ewJpxAbK+bcMcUpiO9zYhbm1u3LSpQqAbPFYR8vA3wDl
# clLmdOzzyuDIwdpoy31Xp4RX3jUOQ5R188nlfderIcJ97em/9V802DenL2m/cBnw
# iK0NF9Tk71USZP9RzZwj2hJ7d8rIWnW1CewfAfb7zZrvFPc49f+ZSaIBrAJ8IUH7
# T56LytcH+Wm5E0WFliDo4C4E/0paOxpoSn3WX/lAOWMvoYIW/TCCFvkGCisGAQQB
# gjcDAwExghbpMIIW5QYJKoZIhvcNAQcCoIIW1jCCFtICAQMxDzANBglghkgBZQME
# AgEFADCCAVEGCyqGSIb3DQEJEAEEoIIBQASCATwwggE4AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIL5BT7a2J83nLPRxr4W31w1G68oA5XuKdj80MuRM
# BVC5AgZjbQj13TgYEzIwMjIxMTE5MDEzOTQ0LjcyNFowBIACAfSggdCkgc0wgcox
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1p
# Y3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1Mg
# RVNOOjNFN0EtRTM1OS1BMjVEMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFt
# cCBTZXJ2aWNloIIRVDCCBwwwggT0oAMCAQICEzMAAAHJ+tWOJSB0Al4AAQAAAckw
# DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcN
# MjIxMTA0MTkwMTM4WhcNMjQwMjAyMTkwMTM4WjCByjELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046M0U3QS1FMzU5LUEy
# NUQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0G
# CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDWcuLljm/Pwr5ajGGTuoZb+8LGLl65
# MzTVOIRsU4byDtIUHRUyNiCjpOJHOA5D4I3nc4E4qXIwdbNEvjG9pLTdmUiB60gg
# tiIBKiCwS2WPMSVEc7t8MYMVZx3P6UI1iYmjO1sbc8yufFuVQcdSSvgLsQEdvZjT
# sZ3kYkGA/z7kBk2xOWwcZzMezjmaY/utSBwyf/9zxD8ZhKp1Pg5cQunneH30SfIX
# jNyx3ZkWPF2PWU/xAbBllLgXzYkEZ7akKtJqTIWNPHMUpQ7BxB6vAFH9hpCXLua0
# Ktrg81zIRCb6f8sNx79VWJBrw4zacFkcrDoLIyoTMUknLkeLPPxnrGuqosq2Ly+I
# lRDQW2qRNdJHf//Dw8ArIGW8hhMUX8vLcmHdxtV46BKa5s5XC/ycx6FxBvYC3FxT
# +V3IRSrLz+2EQchY1pvMdfHk70Phu1Lqgl2AuYfGtMG0axxVCrHTPn99QiQsTu1v
# B+irzhwX9REsTLDernspXZTiA6FzfnpdgRVB0lejpUVYFANhvNqdDbnNjbVQKSPz
# bULIP3SCqs7etA+VxCjp6vBbYMXZ+yaABtWrNCzPpGSZp/Pit7XuSbup7T0+7AfD
# l7fHlkgYShWV82cm/r7znW7ApfoClkXE/N5Cjtb/kG1pOaRkSHBjkB0I+A+/Rpog
# RCfaoXsy8XAJywIDAQABo4IBNjCCATIwHQYDVR0OBBYEFAVvnWdGwjyhvng6FMV5
# UXtELjLLMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRY
# MFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01p
# Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEF
# BQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w
# a2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAo
# MSkuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI
# hvcNAQELBQADggIBADaoupxm30eKQgdyPsCWceGOi7FKM54FpMT4QrxpdxUub1wD
# wPb9ljY5Sli852G4MRX2ESVWbOimIm6T/EFiHp1YlNGGZLuFWOsa2rNIVbQt9+xH
# KyPGSm6rKEeIEPExcwZnoZ3NR+pU/Zl3Y74n8FhAmCz00djP8IzhdpE/5PZUzckT
# WZI7Wotr6Z8HjbtCIuP8kLtNRiCHhFj6gswVW5Alm9diX+MhMV9SmkmgBqQGvRVz
# avWQ/kOIlo29lYn9y5hqJZDiT3GnDrAbPeqrvEBaeUbOxrDAWGO3CrkQf+zfssJ9
# 6HK4LDxlEn1be2BIV6kBUzuxQT4+vdS76I+8FXhOxMM0UvQJUg9f7Vc4nphEZgna
# QcamgZz/myADYgpByX3tkNgkiqLGDAo1+3I3vQ7QBNulNWGxs3TUVWWLQf6+BwaH
# LOTqOkDLAc8NJD/GgR4ZTj7o8VNcxE798zMZxRx/RkepkybRSGgfy062TXyToHvk
# oldO1jdkzulN+6tK/ZCu/nPMIGLLKy04/D8gkj6T2ilOBq2sLf0vr38rDK0PTHu3
# SOZNe2Utloa+hKWN3LKvpANFWSqwJotRJKwCJZ5q/mqDrhTeYuZ56SjQT1MnnLO0
# 3+NyLOUfHReyA643qy5vcI9XsAAwyIqil1BiqI9e70jG+pdPsIT9IwLalw3JMIIH
# cTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCB
# iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp
# TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEw
# OTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIh
# C3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNx
# WuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFc
# UTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAc
# nVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUo
# veO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyzi
# YrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9
# fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdH
# GO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7X
# KHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiE
# R9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/
# eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3
# FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAd
# BgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEE
# AYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMI
# MBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMB
# Af8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1Ud
# HwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By
# b2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQRO
# MEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2Vy
# dHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4IC
# AQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pk
# bHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gng
# ugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3
# lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHC
# gRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6
# MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEU
# BHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvsh
# VGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+
# fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrp
# NPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHI
# qzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAsswggI0AgEBMIH4
# oYHQpIHNMIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUw
# IwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1U
# aGFsZXMgVFNTIEVTTjozRTdBLUUzNTktQTI1RDElMCMGA1UEAxMcTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAfemLy/4eAZuNVCzg
# bfp1HFYG3Q6ggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN
# BgkqhkiG9w0BAQUFAAIFAOciExcwIhgPMjAyMjExMTgyMjIwMDdaGA8yMDIyMTEx
# OTIyMjAwN1owdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5yITFwIBADAHAgEAAgIL
# 2jAHAgEAAgIRszAKAgUA5yNklwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEE
# AYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GB
# AAKJZKAlDNyJJnC2vrZtHK4AjUb+KmIxPQNMG9nYQjjCzCj5rssbE1E4YoX3IujR
# TCtepbp72n+AGOZ4qYGMJzq/bE4jJLwm+H8d6Yw/y+Uuuby6KxeiUfePbsuzH1pB
# bJUx2ILLOxvclIXl2GZjEa+IuKKxRp8lWA23V5m7tRCEMYIEDTCCBAkCAQEwgZMw
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHJ+tWOJSB0Al4AAQAA
# AckwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRAB
# BDAvBgkqhkiG9w0BCQQxIgQg2Vmy7lSm5vyV5w47ja8s9E+bdpmUBuuwaz/K7E5O
# RC8wgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCCBdc5/Ut1RSxAneCnYf2AN
# IyGJAP/NfeFdfOHZOXb9gTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwAhMzAAAByfrVjiUgdAJeAAEAAAHJMCIEINbkejWzmnu/dqdBwfr6AHc3
# Zwy+H3vPcgGgYOsZy2krMA0GCSqGSIb3DQEBCwUABIICAF3e75i6ppn/zo8VTFZB
# GtAB56YpxVAlr7/Y52IlbWlN7iRIcVUXQuYMDx9n9ZLPOtAVyMa+UNB5I2vH659u
# Vs5RLwPP0xN4HHmINQ3qvx9aB8auFRVrFC+w08rUE1mipGEEb6sSEumJ5zFZ+1Fx
# i1B/Wqt6A89RRBOJGBfq0XHqqq8qj5cYlG7yE2+CDAD8RpWi1JGjb23CkVs3QS4F
# zPkFIHdcpGvoSY46gc37jA5Z33cFb/lReNlpBO1IiyDocmlSeoOqTZgxKD0LSC3m
# tJFhaSl4nkJI7CBdqtQnZDPh0MkgUsQoC4asi8osT7m0kjcRmqy7nKRR98Z43gZV
# ClMupltj27mg3d6ArSaxvWclJtsibytR7fhUZGgQff/fK4qQeqxfz40xSKJBT4kw
# TIxPTBSDi+9GEE95TTiwLvkJcuDRMKF0hqnpPK0cdexPD+tDstH5Db+JiX4FzB6R
# sXsUHaMJLBw62ik2EpVxj549PaYqVewQSUbhjLAYOg9zXzKLj76QWmIRE8k+l5T9
# 0742vjc6crwD4kCgNXmbZ5ecyEshpMflHpJkyrS95Ifc9fbmoPvUmqVR2NZDlhA+
# 4iBCcPPF3vzRfHpx+y+Sk9wKJ4DNsW5pDJJDd7iNnLMU+rl9BLDbIL5uJGn81AXW
# dRVRgKYVBZVXKmyLeeZJC52P
# SIG # End signature block