mega-has-Install.psm1


function Import-HASInstaller {
  [CmdletBinding(DefaultParametersetName = "default")]
  param(
    [Parameter(Mandatory)]
    [String]$token, 
    [Parameter(ParameterSetName = 'byBundle')]
    [ValidatePattern("^([^v:]+)[v:]([0-9\.\+]+)$")]
    [String]$bundle, 
    [Parameter(ParameterSetName = 'byHopexVersion')]
    [String]$hopexVersion,
    [Parameter()]
    [switch]$includePrerelease,
    [Parameter()]
    [String]$outFile = 'has.setup.exe',
    [Parameter(HelpMessage = "HOPEX store address (eg. https://store.mega.com)")]
    [String]$server = 'https://store.mega.com'
  )
    
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  Write-Output "Downloading HAS installer..."
  $server = $server.TrimEnd('/')  
  $url = "$server/releases/latest?kind=installer&api_key=$Token"
  if ($bundle) {
    $url = $url + "&bundle=$Bundle"
  }
  if ($hopexVersion) {
    $url = $url + "&hopex_version=$hopexVersion"
  }
  if ($IncludePrerelease.IsPresent) {
    $url = $url + "&includePrerelease=1"
  }
  Write-Output " Downloading setup from '$url' ..."
  Invoke-WebRequest $url -OutFile $outFile

  Write-Output "HAS installer donwloaded in $outFile"
}

function Import-HASModule {
  [CmdletBinding(DefaultParametersetName = "default")]
  param(
    [Parameter(Mandatory)]
    [String]$token, 
    [Parameter(Mandatory)]
    [String]$module, 
    [Parameter(HelpMessage='Module version (Can be a partial number like 15.0 in this case the latest version in this range will be taken)')]
    [String]$version = 'latest',
    [Parameter()]
    [switch]$includePrerelease,
    [Parameter(Mandatory)]
    [String]$outFile ,
    [Parameter(HelpMessage = "HOPEX store address (eg. https://store.mega.com)")]
    [String]$server = 'https://store.mega.com'
  )
    
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

  Write-Output "Downloading HAS module $module..."
  $server = $server.TrimEnd('/')
  $url = "$server/api/modules/package/$module/$version" + "?api_key=$token"

  if ($IncludePrerelease.IsPresent) {
    $url = $url + "&includePrerelease=1"
  }
  Write-Output " Downloading module from '$url' ..."
  Invoke-WebRequest $url -OutFile $outFile

  Write-Output "HAS module donwloaded in $outFile"
}

function Add-HASCertificate {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$siteName,
    [Parameter(Mandatory)]
    [string]$domain,
    [string]$contact = 'contact@mega.com'
  )

  # Update SSL certificate
  # https://www.virtualtothecore.com/new-year-new-lets-encrypt-automation-goodbye-acmesharp-hello-posh-acme/
  Write-Output "Generate certificate"
  Import-Module Posh-Acme -force
  Set-PaServer LE_PROD
  $pfxPass = "Hopex"
  New-PACertificate $domain -AcceptTOS -Contact $contact -pfxPass $pfxPass -Plugin WebSelfHost -PluginArgs @{}

  $cert = Get-PACertificate -MainDomain $domain

  Write-Output "Create IIS https binding"
  New-WebBinding -name $siteName -Protocol https -Port 443 -ErrorAction SilentlyContinue
  $cert | Set-IISCertificate -SiteName $siteName
  Remove-WebBinding -Name $siteName -Protocol http  -ErrorAction SilentlyContinue

  ## Add certificate renewal script
  $T = New-JobTrigger -Weekly -At "3:00 AM" -DaysOfWeek Monday -RandomDelay 0:30:00
  Register-ScheduledJob -Name "RenewCertificateWeekly" -Trigger $T -ErrorAction SilentlyContinue -ScriptBlock {
    Import-Module Posh-Acme
    Import-Module Posh-Acme.deploy
    Set-PAOrder $domain 
    if ($cert = Submit-Renewal) {
      $cert | Set-IISCertificate -SiteName $siteName -RemoveOldCert
    }
  }

  $T = New-JobTrigger -AtStartup 
  Register-ScheduledJob -Name "RenewCertificateOnStart" -Trigger $T -ErrorAction SilentlyContinue -ScriptBlock {
    Import-Module Posh-Acme
    Import-Module Posh-Acme.deploy
    $cert = submit-renewal $domain -force
    $cert | Set-IISCertificate -SiteName $siteName -RemoveOldCert
  }
}

function Install-HASInstanceManager {
  [CmdletBinding(DefaultParametersetName = "default")]
  param(
    [Parameter(Mandatory, HelpMessage = "HOPEX store token")]
    [String]$token, 
    [Parameter()]
    [ValidateRange(5000, 60000)]
    [int]$port = 30100,
    [Parameter(Mandatory)]
    [string]$password,
    [Parameter(ParameterSetName = "fromCmdLine")]
    [ValidatePattern("^([^v:]+)[v:]([0-9\.\+]+)$")]
    [String]$bundle, 
    [Parameter()]
    [String]$installer = 'has.setup.exe',
    [Parameter()]
    [ValidateSet("Development", "Training", "Staging", "Production")]
    [String]$defaultMode = 'Production',
    [Parameter()]
    [ValidateRange(5000, 60000)]
    [int]$start,
    [Parameter(HelpMessage = "HOPEX store address (eg. https://store.mega.com)")]
    [String]$server = 'https://store.mega.com',
    [Parameter(ParameterSetName = "fromCmdLine")]
    [String]$provisionFile,
    [Parameter(HelpMessage = "Max waiting time in seconds")]
    [int]$wait,
    [Parameter(ValueFromPipeline, ParameterSetName = "fromPipeline")] 
    [PSCustomObject] $provision
  )

  Write-Output "Installing HAS with $installer..."

  if ($provision) {
    $provisionFile = '.provision'
    $provision | Save-HASProvisionFile -outFile $provisionFile
    $bundle = $provision.bundle.bundle + ":" + $provision.bundle.version
    if (!$start) {
      $start = 5000
    }
  }
  
  Write-Output " Preparing settings file..."
  $settings = [PSCustomObject]@{
    AgentPort         = $port
    ApiKey            = $password
    HopexStoreAddress = $server
    HopexStoreToken   = $token
    RunningMode       = $mode
    Bundle            = $bundle
    mode              = $defaultMode
  }

  if ($start) {
    $settings | Add-Member -NotePropertyName "InstancePort" -NotePropertyValue $start
    $settings | Add-Member -NotePropertyName "TemplateFile" -NotePropertyValue $provisionFile
  }

  $settings | ConvertTo-Json | Set-Content -Path settings.json -Force

  Try {
    Write-Output "Running setup..."
    Start-Process -FilePath $installer -ArgumentList "install settings.json" -Wait
    If (Test-Path ".\has.setup.log") {
      If (Select-String -Path ".\has.setup.log" -pattern "Installation completed successfully") {
        Write-Output "Installation completed successfully"
      }
      Else {
        Write-Output "Installation failed"
        Throw "Error installing HAS setup"
      }
    }
    Else {
      Write-Output "WARNING: installing HAS setup (installation log is missing)"
    }
  }
  Catch {
    Throw $_
  }
  Finally {
    Remove-Item settings.json
  }

  if ($wait) {
    Start-Sleep -Seconds 10
    Wait-ForHASReady -wait $wait -port $start
  }
}

function Wait-ForHASReady {
  [CmdletBinding()]
  param(
    [Parameter()]
    [string]$HASAddress = "http://localhost:5000",
    [Parameter(HelpMessage = "Max waiting time in seconds")]
    [int]$wait = 2700
  )

  Try {
    $HASStatusCode = ""
    $Timer = [Diagnostics.Stopwatch]::StartNew()
    Write-Output "Waiting for HAS to start for $($wait/60) minutes..."
  
    While (($HASStatusCode -ne "200")) {
      Try { 
        $HASStatusCode = (Invoke-WebRequest -URI "$HASAddress/admin/cluster/health" -UseBasicParsing -ErrorAction SilentlyContinue).StatusCode 
      }
      Catch {}

      if ($Timer.Elapsed.TotalSeconds -ge $wait) {
        break
      }
      Start-Sleep -Seconds 10
    }
  
    If ($Timer.Elapsed.TotalSeconds -ge $wait) {
      Throw "Error timeout during HAS start up ($($wait/60) min)"
    }
    Else {
      Write-Output "HAS successfully started"
    }
  }
  Catch {
    Throw $_
  } 
  Finally {
    $Timer.Reset()
    $Timer.Stop()
  }
}

function New-HASUser {
  [CmdletBinding()]
  param(
    [Parameter()]
    [string]$HASAddress = "http://localhost:5000",
    [Parameter(Mandatory)]
    [string]
    $adminPassword,
    [Parameter(Mandatory)]
    [string]
    $userName,
    [Parameter(Mandatory)]
    [string]
    $userPassword,
    [Parameter(Mandatory)]
    [string]
    $login,
    [Parameter(Mandatory)]
    [string]
    $profile,
    [Parameter(Mandatory)]
    [string]
    $repository,
    [Parameter(Mandatory)]
    [string]
    $environment,
    [Parameter()]
    [string]
    $description
  )

  $settings = @{
    name         = $name
    login        = $login
    password     = $userPassword
    profile      = $profile
    repository   = $repository
    environment  = $environment
    description  = $description
  }

  $cred = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("admin:$adminPassword"))
  $header = @{
    authorization="Basic $cred"
  }

  Write-Output "Creating a new user $name..." 
  Invoke-RestMethod -URI "$HASAddress/uas/admin/api/users" -Method Post -Body $settings -Headers $headers
}



function Copy-HASWebConfig {
  [CmdletBinding()]
  param(
    [Parameter()]
    [string]$iisFolder = 'C:\inetpub\wwwroot\has\'
  )

  $webConfig = (Get-ChildItem -Recurse -Path 'C:\ProgramData\MEGA\Hopex Application Server\.binaries\' -Include web.config).fullname | Select -First 1
  $sourceIISFolder = Split-Path $webConfig   
  Write-Host "Copy $sourceIISFolder\..\iis to iis folder $iisFolder"
  copy-item "$sourceIISFolder\..\iis\*" $iisFolder -force
}

# -----------------------------------------------------------
# Provision file
# -----------------------------------------------------------
function New-HASProvision {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory)]
    [string]
    $bundle,
    [Parameter(Mandatory)]
    [string]
    $publicAddress,
    [Parameter()]
    [String]
    $name,   
    [Parameter()]
    [ValidateSet("Development", "Staging", "Training", "Production")]
    [String]$mode, 
    [Parameter(Mandatory)]
    [string]
    $database,
    [Parameter()]
    [string]
    $adminPassword,
    [Parameter()]
    [Switch]
    $noSsl
  )
  
  $pattern = '([^v:]+)([v:])([0-9\.\+]+)'
  $match = [regex]::match($bundle, $pattern)
  
  return [PSCustomObject]@{
    bundle        = @{
      bundle      = $match.Groups[1].Value
      versionName = $bundle -replace $pattern, '$1 V$3'
      version     = $match.Groups[3].Value
    }
    adminPassword = $(if ($adminPassword) { $adminPassword } else { $null }) 
    configuration = @{
      name                     = $(if ($name) { $name } else { $null }) 
      databaseConnectionString = $database
      mode                     = $(if ($mode) { $mode } else { $null }) 
      publicAddress            = $publicAddress
      noSsl                    = $(if ($noSsl) { $true } else { $false }) 
    } 
  }; 
}
  
function Add-HASProvisionModule {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $inputProvision, [Parameter(Mandatory)] [string] $id, [Parameter()] [string] $version
  )
  
  Process {
    $modules = $inputProvision.modules;
    if ($null -eq $modules) {
      $modules = @();
      $inputProvision | Add-Member -NotePropertyName modules -NotePropertyValue $modules 
    }
  
    $inputProvision.modules += [PSCustomObject]@{ 
      id      = $id 
      version = $(if ($version) { $version } else { $null }) 
    };
  
    return [PSCustomObject]$inputProvision
  }
}
  
function Add-HASProvisionMegasiteEntry {
  [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $inputProvision, [Parameter(Mandatory)] [string] $section, [Parameter(Mandatory)] [string] $name, [Parameter(Mandatory)] [string] $value
  )
  
  Process {
   if($inputProvision.PSObject.Properties.Name -contains "stages")
    {
      $stage = $inputProvision.stages[0];
    }
    else {
      $stage = [PSCustomObject] @{};
      $stages = @($stage)
      $inputProvision | Add-Member -NotePropertyName stages -NotePropertyValue $stages 
    }

    if($stage.PSObject.Properties.Name -contains "megaSite")
    {
      $megaSite = [PSCustomObject]$stage.megaSite
    }
    else {
      $megaSite = [PSCustomObject]@{}
      $stage | Add-Member -NotePropertyName "megaSite" -NotePropertyValue $megaSite
    }
  
    $megasite | Add-Member -NotePropertyName "[$section].$name" -NotePropertyValue $value    
  
    return [PSCustomObject]$inputProvision
  }
}

function Add-HASModuleSettingsEntry {
  [CmdletBinding()] 
  param ( 
    [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $inputProvision, 
    [Parameter()] [string] $module = "Globals", 
    [Parameter(Mandatory)] [string] $name, 
    [Parameter(Mandatory)] [string] $key, 
    [Parameter(Mandatory)] [string] $value
  )
  
  Process {
   if($inputProvision.PSObject.Properties.Name -contains "stages")
    {
      $stage = $inputProvision.stages[0];
    }
    else {
      $stage = [PSCustomObject] @{};
      $stages = @($stage)
      $inputProvision | Add-Member -NotePropertyName stages -NotePropertyValue $stages 
    }

    $settingsName = $module + "." + $name
    if($stage.PSObject.Properties.Name -contains $settingsName)
    {
      $moduleSettings = [PSCustomObject]$stage.settings
    }
    else 
    {
      $moduleSettings = [PSCustomObject]@{}
      $stage | Add-Member -NotePropertyName $settingsName -NotePropertyValue $moduleSettings
    }
  
    $moduleSettings | Add-Member -NotePropertyName "$key" -NotePropertyValue $value    
  
    return [PSCustomObject]$inputProvision
  }
}

function Add-HASProvisionApiKey {
  [CmdletBinding()] 
  param ( 
    [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $inputProvision, 
    [Parameter(Mandatory)] [string] $name, 
    [Parameter(Mandatory)] [string] $password, 
    [Parameter()] [string] $environmentId, 
    [Parameter()] [string] $profileId,   
    [Parameter()] [string] $repositoryId,   
    [Parameter()] [string] $description, 
    [Parameter()] [string] $login
  )
  
  Process {
    
    if($inputProvision.PSObject.Properties.Name -contains "stages")
    {
      $stage = $inputProvision.stages[0];
    }
    else {
      $stage = [PSCustomObject] @{};
      $stages = @($stage)
      $inputProvision | Add-Member -NotePropertyName stages -NotePropertyValue $stages 
    }

    if($stage.PSObject.Properties.Name -contains "apikeys")
    {
      $apikeys = [System.Collections.ArrayList]$stage.apikeys
    }
    else {
      $apikeys = [System.Collections.ArrayList]@()
      $stage | Add-Member -NotePropertyName "apikeys" -NotePropertyValue $apikeys
    }

  
    $apikeys.Add(  [PSCustomObject] @{
      Name = $name
      Password = $password
      Login = $(if ($login) { $login } else { $null })
      EnvironmentId = $(if ($environmentId) { $environmentId } else { $null })
      Description = $(if ($description) { $description } else { $null })
      ProfileId = $(if ($profileId) { $profileId } else { $null })
      RepositoryId = $(if ($repositoryId) { $repositoryId } else { $null })
    })
  
    return [PSCustomObject]$inputProvision
  }
}

function Add-HASProvisionUser {
  [CmdletBinding()] 
  param ( 
    [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $inputProvision, 
    [Parameter(Mandatory)] [string] $name, 
    [Parameter(Mandatory)] [string] $password, 
    [Parameter()] [string] $environmentId, 
    [Parameter()] [string] $profileId,   
    [Parameter()] [string] $repositoryId,   
    [Parameter()] [string] $description, 
    [Parameter()] [string] $login
  )
  
  Process {
   if($inputProvision.PSObject.Properties.Name -contains "stages")
    {
      $stage = $inputProvision.stages[0];
    }
    else {
      $stage = [PSCustomObject] @{};
      $stages = @($stage)
      $inputProvision | Add-Member -NotePropertyName stages -NotePropertyValue $stages 
    }

    if($stage.PSObject.Properties.Name -contains "users")
    {
      $users = [System.Collections.ArrayList]$stage.users
    }
    else {
      $users = [System.Collections.ArrayList]@()
      $stage | Add-Member -NotePropertyName "users" -NotePropertyValue $users
    }
  
    $users.Add(  [PSCustomObject] @{
      Name = $name
      Password = $password
      Login = $(if ($login) { $login } else { $null })
      EnvironmentId = $(if ($environmentId) { $environmentId } else { $null })
      Description = $(if ($description) { $description } else { $null })
      ProfileId = $(if ($profileId) { $profileId } else { $null })
      RepositoryId = $(if ($repositoryId) { $repositoryId } else { $null })
    })
  
    return [PSCustomObject]$inputProvision
  }
}

function Save-HASProvisionFile {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory, ValueFromPipeline)] 
    [PSCustomObject] $inputProvision, 
    [Parameter()]
    [string] $outFile = 'provision.json'
  )
  Write-Output $inputProvision | ConvertTo-Json -Depth 10 -Compress | Set-Content -Path $outFile -Force 
}