Isard.ps1

# https://gitlab.com/isard/isardvdi/-/tree/main/api/src/api/schemas

# /api/v3/user
# /api/v3/user/config


function Connect-Isard {

  param(
    [Parameter(Position = 0, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [string] $OS = "ubuntu",
    [switch] $New
  )

  if (-Not(Get-Command "remote-viewer.exe" -ErrorAction SilentlyContinue)) {
    Install-Scoop virt-viewer -Bucket "extras"
  }

  if ($New) {
    New-Isard $Name -os $OS
    Start-Sleep -Seconds 1
  }

  $vm = Get-Isard -Name $Name -Raw
  if ($Null -eq $vm) {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Red "No existeix cap màquina amb aquest nom"
    return
  }

  # TODO comprovar màquina arrencada

  if ($vm.state -ne "Started") {
    Start-Isard $Name -Wait
  }


  Write-IsardLog $Name
  Write-Host "Obtenint el fitxer de configuració ..." -NoNewline
  $data = Invoke-IsardRequest -Endpoint "/api/v3/desktop/$($vm.id)/viewer/file-spice"
  Write-Host -ForegroundColor Green " Fet."

  $data = $data | ConvertFrom-Json
  $file = Get-IsardPath -Name "isard-spice.vv"
  $content = $data.content
  $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False

  [System.IO.File]::WriteAllLines($file, $content, $Utf8NoBomEncoding)

  remote-viewer.exe $file
}

function Get-Isard {
  
  param(
    [Parameter(Position = 0, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [switch] $Raw
  )

  $vms = Invoke-IsardRequest -Endpoint "/api/v3/user/desktops"
  $vms = $vms | ConvertFrom-Json

 
  if ($Name -ne "") {
 
    $vms = $vms | Where-Object { $_.name -eq $Name } | Select-Object -First 1

    if (($Null -eq $vms) -And -Not($Raw)) {
      Write-IsardLog $Name
      Write-Host -ForegroundColor Red "No existeix cap màquina amb aquest nom"
      return
    }
  }

  if ($Raw) {
    return $vms
  }
    
  $vms | Sort-Object name | Format-Table -AutoSize -Property @{
    Label      = "Nom"
    Expression = { $_.name }
  }, @{
    Label      = "Estat"
    Expression = { $_.state }
  },
  @{
    Label      = "IP"
    Expression = { $_.ip }
  }
}

function New-Isard {

  param(
    [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [string] $os = "ubuntu"
  )

  $oss = @("ubuntu","windows")
  if ($oss -notcontains $os) {
    Write-Host -ForegroundColor Red "Error" -NoNewline
    Write-Host ": Els unics sistemes operatius són 'ubuntu' i 'windows'"
    return
  }

  $endpoint = "/api/v3/persistent_desktop"
  
  $vm = Get-Isard -Name $Name -Raw
  if ($vm) {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Red "Ja existeix una màquina amb aquest nom"
    return
  }

  $template = $isard_templates[$os]
  $template.name = $Name

  Write-IsardLog $Name
  Write-Host "Creant la màquina ..." -NoNewline
  $data = Invoke-IsardRequest -Endpoint $endpoint -Data $template
  Write-Host -ForegroundColor Yellow $data.id -NoNewline
  Write-Host -ForegroundColor Green " Fet"
  
}

function Remove-Isard {
  param(
    [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [switch] $Now
  )

  $vm = Get-Isard -Name $Name -Raw
  if ($Null -eq $vm) {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Red "No existeix cap màquina amb aquest nom"
    return
  }

  $endpoint = "/api/v3/desktop/${vm.id}"

  Write-IsardLog $Name
  Write-Host "Eliminant la màquina ..." -NoNewline
  # Method not allowed
  Invoke-IsardRequest -Method "Delete" -Endpoint $endpoint

}

function Start-Isard {

  param(
    [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [switch] $Wait
  )

  #https://gitlab.com/isard/isardvdi/-/blob/main/api/src/api/views/DesktopsPersistentView.py

  $vm = Get-Isard -Name $Name -Raw
  if ($Null -eq $vm) {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Red "No existeix cap màquina amb aquest nom"
    return
  }

  if ($vm.state -eq "Started") {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Yellow "La màquina ja està engegada"
    return
  }

  if ($vm.state -eq "Shutting-down") {
    Stop-Isard $Name -Wait
  }
  
  if ($vm.state -eq "WaitingIP") {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Yellow "La màquina està esperant la IP" -NoNewLine
    Write-Host " ..." -NoNewLine
  }
  else {

    $endpoint = "/api/v3/desktop/start/$($vm.id)"

    Write-IsardLog $Name
    Write-Host "Engegant la màquina ..." -NoNewline
    $data = Invoke-IsardRequest -Endpoint $endpoint
    # TODO check ok {"id": "ba4d3752-7dd5-49e2-8fd9-b237eaeea4f5"}
  }

  if ($Wait) {
    $state = ""
    while ($state -ne "Started") {
      Start-Sleep -Seconds 1
      Write-Host "." -NoNewline
      $vm = Get-Isard -Name $Name -Raw
      $state = $vm.state
    }
  }

  Write-Host " " -NoNewline
  Write-Host -ForegroundColor Green "Fet"
}

function Stop-Isard {

  param(
    [Parameter(Position = 0, Mandatory = $true, HelpMessage = "Nom de la màquina virtual")]
    [string] $Name,
    [switch] $Wait
  )

  #https://gitlab.com/isard/isardvdi/-/blob/main/api/src/api/views/DesktopsPersistentView.py

  $vm = Get-Isard -Name $Name -Raw
  if ($Null -eq $vm) {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Red "No existeix cap màquina amb aquest nom"
    return
  }

  if ($vm.state -eq "Stopped") {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Yellow "La màquina ja està aturada"
    return
  }

  if ($vm.state -eq "Shutting-down") {
    Write-IsardLog $Name
    Write-Host -ForegroundColor Yellow "La màquina s'està aturant ..." -NoNewline
  }
  else {

    $endpoint = "/api/v3/desktop/stop/$($vm.id)"
  
    Write-IsardLog $Name
    Write-Host "Aturant la màquina ..." -NoNewline
    $data = Invoke-IsardRequest -Endpoint $endpoint
    # TODO check ok {"id": "ba4d3752-7dd5-49e2-8fd9-b237eaeea4f5"}
  }

  if ($Wait) {
    $state = ""
    while ($state -ne "Stopped") {
      Start-Sleep -Seconds 1
      Write-Host "." -NoNewline
      $vm = Get-Isard -Name $Name -Raw
      $state = $vm.state
    }
  }

  Write-Host " " -NoNewline
  Write-Host -ForegroundColor Green "Fet"
}

##### Private

function Get-IsardPath {
  param(
    [string] $Name
  )

  $path = Get-BoxPath -Path "isard"
  if ($Name) {
    $path = Join-Path $path $Name
  }
  return $path
}

# https://gitlab.com/isard/isardvdi/-/blob/main/api/src/api/views/AuthenticationView.py
function Get-IsardToken {
  param (
    [switch] $Update
  )

  $tokenFile = Get-IsardPath -Name token
  
  if ($Update) {
    Remove-Item -Path $tokenFile
  }

  if (Test-Path $tokenFile) {
    return (Get-Content $tokenFile)
  }


  $login = @{ 
    "category" = @{"name" = "Provençana"; "id" = "db79b78a-5408-4ff9-9853-a1172b7eadb2" };
    "username" = $Null 
  }
  
  $loginPath = Get-IsardPath "login.json"
  if (Test-Path $loginPath -PathType Leaf) {
    $login = (Get-Content $loginPath) | ConvertFrom-Json
  }


  Write-Host -ForegroundColor Green "Inciar Sessió"
  $categoryId = $login.category.id
  if (!($username = Read-Host "Nom d'usuari [$($login.username)]")) { $username = $login.username }
  $login.username = $username
  $password = Read-Host "Contrasenya" -AsSecureString
  $password = (New-Object PSCredential 0, $password).GetNetworkCredential().Password

  $endpoint = "/authentication/login"


  $response = Invoke-IsardRequest2 -Endpoint $endpoint -Form $True -Data @{
    provider    = "p:form"
    category_id = "p:$categoryId"
    username    = $username
    password    = $password 
  }

  $token = $response.Content
  $token | Out-File $tokenFile

  $login | ConvertTo-Json | Out-File $loginPath

  return $token
}

function Invoke-IsardRequest {
  param(
    [string] $Method = "Get",
    [string] $Endpoint,
    [hashtable] $Data,
    [bool] $Form = $False
  )
  try {

    $token = Get-IsardToken
    try {
      $response = Invoke-IsardRequest2 -Method $Method -Endpoint $Endpoint -Data $Data -Form $Form -Token $token
      return $response.Content
    }
    catch {
      $status = [int]$_.Exception.Response.StatusCode
      if ($status -ne 401) {
        throw $_.Exception
      }

      $token = Get-IsardToken -Update
      $response = Invoke-IsardRequest2 -Method $Method -Endpoint $Endpoint -Data $Data -Form $Form -Token $token
      return $response
    }
  }
  catch {

    $response = $_.Exception.Response
    #StatusDescription

    Write-IsardLog "HTTP"
    Write-Host -ForegroundColor Yellow "($([int]$response.StatusCode) $($response.StatusCode))" -NoNewline
    Write-Host ": " -NoNewline
    Write-Host $_.ErrorDetails.Message

    throw $_.Exception
  }

}

function Invoke-IsardRequest2 {
  param(
    [string] $Method,
    [string] $Endpoint,
    [hashtable] $Data,
    [bool] $Form,
    [string] $Token
  )


  $uri = "https://elmeuescriptori.gestioeducativa.gencat.cat${Endpoint}"
  $headers = @()
  $contentType = $Null
  $body = $Null

  if ($Null -ne $Token) {
    $headers = @{Authorization = "Bearer $token" }
  }

  if ($Null -ne $Data) {

    $method = "Post"

    if (-Not($Form)) {
      $contentType = "application/json"
      $body = $Data | ConvertTo-Json -Depth 10
    }
    else {
      $boundary = [System.Guid]::NewGuid().ToString();     
      $contentType = "multipart/form-data; boundary=`"$boundary`""

      $LF = "`r`n"

      $query = $Null
      $body = @()
      foreach ($item in $Data.GetEnumerator()) {
        $body += "--$boundary"
        $body += "Content-Disposition: form-data; name=`"$($item.Name)`"$LF"
    
        $value = $item.Value
        if (-Not($value.StartsWith("p:"))) {
          $body += $value
        }
        else {
          $value = $value.Substring(2)
          $body += $value
          if ($Null -eq $query) {
            $query += "?"
          }
          else {
            $query += "&"
          }
          $query += "$($item.Name)=$value"
        }
      }
      $body += "--$boundary--$LF" 
      $body = $body -join $LF

      if ($Null -ne $query) {
        $uri += $query
      }
    }
  }

  $ProgressPreference = "SilentlyContinue"
  $response = Invoke-WebRequest -UseBasicParsing -Method $Method -Uri $uri -Header $headers -ContentType $contentType -Body $body
  $ProgressPreference = "Continue"

  # session https://superuser.com/questions/1275923/using-invoke-webrequest-in-powershell-with-cookies

  $setCookie = $response.Headers['Set-Cookie']

  return $response
}

function Write-IsardLog {
  param(
    [string] $Name
  )

  Write-Host -ForegroundColor Blue -NoNewline $Name 
  Write-Host ": " -NoNewline
}


$isard_templates = @{
  
  ubuntu = @{
    
    "template_id"      = "23f44236-d6df-43eb-9f00-e113d1e23997" # Ubuntu Mate Desktop 24.04 LTS v1.1
    "name"             = $Null
    "description"      = ""
    "guest_properties" = @{
      "credentials" = @{
        "username" = "box"
        "password" = "password"
      }
      "fullscreen"  = $False
      "viewers"     = @{
        "browser_rdp" = @{
          "options" = "null"
        }
        "browser_vnc" = @{
          "options" = "null"
        }
        "file_rdpgw"  = @{
          "options" = "null"
        }
        "file_spice"  = @{
          "options" = "null"
        }
      }
    }
    "hardware"         = @{
      "boot_order"  = @("disk")
      "disk_bus"    = "default"
      "disks"       = @(
        @{
          "bus"        = "virtio"
          "storage_id" = "62d93be1-cfb1-4397-b6eb-bf42f003d0ae"
        }
      )
      "floppies"    = @()
      "interfaces"  = @("default", "wireguard")
      "isos"        = @()
      "memory"      = 16
      "vcpus"       = 8
      "videos"      = @("default")
      "reservables" = @{
        "vgpus" = @("None")
      }
    }
    "image"            = @{
      "id"   = "os_ubuntu.png"
      "type" = "stock"
    }
  }

  windows = @{
    
    "template_id"      = "fd44d7c5-0a05-4a6f-a7df-a1a87a85da90" # Windows 11 T3_Isardvdi_v2.0
    "name"             = $Null
    "description"      = ""
    "guest_properties" = @{
      "credentials" = @{
        "username" = "box"
        "password" = "password"
      }
      "fullscreen"  = $False
      "viewers"     = @{
        "browser_rdp" = @{
          "options" = "null"
        }
        "browser_vnc" = @{
          "options" = "null"
        }
        "file_rdpgw"  = @{
          "options" = "null"
        }
        "file_spice"  = @{
          "options" = "null"
        }
      }
    }
    "hardware"         = @{
      "boot_order"  = @("disk")
      "disk_bus"    = "sata"
      "disks"       = @(
        @{
          "bus"        = "sata"
          "storage_id" = "d2d17c23-eec8-40c5-8c88-51719f792834"
        }
      )
      "floppies"    = @()
      "interfaces"  = @("default", "wireguard")
      "isos"        = @()
      "memory"      = 16
      "vcpus"       = 8
      "videos"      = @("default")
      "reservables" = @{
        "vgpus" = @("None")
      }
    }
    "image"            = @{
      "id"   = "os_windows.png"
      "type" = "stock"
    }
  }
}