Public/metal.ps1

function Get-MetalHost {
    <#
    .SYNOPSIS
        Gets metal hosts from the Cloud Server.
     
    .DESCRIPTION
        Retrieves a list of metal hosts. Automatically handles token refresh.
     
    .PARAMETER Name
        Optional. Filter by metal host name.
     
    .PARAMETER ID
        Optional. Filter by metal host ID
     
    .EXAMPLE
        # Get all hosts
        Get-MetalHost
         
    .EXAMPLE
        # Get a host by ID
        Get-MetalHost -ID 5
         
    .EXAMPLE
        # Get a host by name
        Get-MetalHost -Name si-storage-1
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$Name,
        
        [Parameter(Mandatory = $false)]
        [int]$ID
    )
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/host"
    
    if ($Name) {
        $hosturi = "$uri/${Name}"
        $response = Invoke-CloudApiRequest -Uri $hosturi -Method Get
       
        return $response
    }
    else {
        # Use the helper function which handles token refresh automatically
        $response = Invoke-CloudApiRequest -Uri $uri -Method Get
        
        # Extract hosts from the response
        $hosts = $response.hosts
        
        # Filter by ID if specified - use PSBoundParameters to check if parameter was provided
        if ($PSBoundParameters.ContainsKey('ID')) {
            $hosts = $hosts | Where-Object { ($_.cloud_id -eq $ID) -and ($null -ne $_.cloud_id) }
        }
        
        return $hosts
    }
}

function Get-MetalLicense {
    <#
    .SYNOPSIS
        Gets metal license from the Cloud Server.
     
    .DESCRIPTION
        Retrieves the metal license. Automatically handles token refresh.
     
    .EXAMPLE
        # Get the current license status
        Get-MetalLicense#
 
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$Name,
        
        [Parameter(Mandatory = $false)]
        [int]$ID
    )
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/license"
    $producturi = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal"
    # Use the helper function which handles token refresh automatically
    $response = Invoke-CloudApiRequest -Uri $uri -Method Get
    $productresponse = Invoke-CloudApiRequest -Uri $producturi -Method Get
    $product = ($productresponse).product_name
    
    # Extract hosts from the response
    $license = $response
    if ($product -eq "HyperCloud") {
        write-host " "
        write-host "This is HyperCloud. License not required." -ForegroundColor Cyan
        write-host " "
    }
    else {
        return $license
    }
    
    if ($licensereq -eq $true) {
        return $license
    }
    
}

function Get-MetalCapacity {
    <#
    .SYNOPSIS
        Gets metal capacity from the Cloud Server.
     
    .DESCRIPTION
        Retrieves the metal Capacity. Automatically handles token refresh.
     
    .PARAMETER Graph
        Optional. Display capacity with visual percentage graphs
     
    .PARAMETER B
        Display memory and storage in Bytes
     
    .PARAMETER KB
        Display memory and storage in Kilobytes
     
    .PARAMETER MB
        Display memory and storage in Megabytes
     
    .PARAMETER GB
        Display memory and storage in Gigabytes
     
    .PARAMETER TB
        Display memory and storage in Terabytes
     
    .PARAMETER EB
        Display memory and storage in Exabytes
     
    .EXAMPLE
        # Get the capacity of the hosts
        Get-MetalCapacity
         
    .EXAMPLE
        # Get the capacity of the hosts and display what is used graphically
        Get-MetalCapacity -Graph
         
    .EXAMPLE
        # Get the capacity of the hosts in GB and display what is used graphically
        Get-MetalCapacity -Graph -GB
         
    .EXAMPLE
        # Get the capacity of the hosts in TB
        Get-MetalCapacity -TB
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [switch]$Graph,
        
        [Parameter(Mandatory = $false)]
        [switch]$B,
        
        [Parameter(Mandatory = $false)]
        [switch]$KB,
        
        [Parameter(Mandatory = $false)]
        [switch]$MB,
        
        [Parameter(Mandatory = $false)]
        [switch]$GB,
        
        [Parameter(Mandatory = $false)]
        [switch]$TB,
        
        [Parameter(Mandatory = $false)]
        [switch]$EB
    )
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/capacity"
    
    # Use the helper function which handles token refresh automatically
    $response = Invoke-CloudApiRequest -Uri $uri -Method Get
    
    # Extract capacity from the response
    $metal = $response.capacity
    
    # Determine target unit and conversion factor
    $targetUnit = "native"
    $conversionFactors = @{
        'B'  = @{ fromKB = 1024; fromMB = 1048576; label = 'B' }
        'KB' = @{ fromKB = 1; fromMB = 1024; label = 'KB' }
        'MB' = @{ fromKB = 1/1024; fromMB = 1; label = 'MB' }
        'GB' = @{ fromKB = 1/1048576; fromMB = 1/1024; label = 'GB' }
        'TB' = @{ fromKB = 1/1073741824; fromMB = 1/1048576; label = 'TB' }
        'EB' = @{ fromKB = 1/1099511627776; fromMB = 1/1073741824; label = 'EB' }
    }
    
    if ($B) { $targetUnit = 'B' }
    elseif ($KB) { $targetUnit = 'KB' }
    elseif ($MB) { $targetUnit = 'MB' }
    elseif ($GB) { $targetUnit = 'GB' }
    elseif ($TB) { $targetUnit = 'TB' }
    elseif ($EB) { $targetUnit = 'EB' }
    else {$targetUnit = 'GB'}
    
    # Convert values if a unit is specified
    if ($targetUnit -ne "native") {
# Convert values
$convertedMetal = foreach ($item in $metal) {
    $newItem = $item.PSObject.Copy()
    
    # Determine decimal precision based on unit size
    $decimalPlaces = switch ($targetUnit) {
        'B'  { 0 }  # Bytes - no decimals needed
        'KB' { 0 }  # Kilobytes - no decimals needed
        'MB' { 2 }  # Megabytes - 2 decimals
        'GB' { 2 }  # Gigabytes - 2 decimals
        'TB' { 4 }  # Terabytes - 4 decimals for precision
        'EB' { 6 }  # Exabytes - 6 decimals for very small numbers
        default { 2 }
    }
    
    if ($item.name -eq "memory") {
        # Memory is in KB, convert from KB
        $newItem.allocated = [math]::Round($item.allocated * $conversionFactors[$targetUnit].fromKB, $decimalPlaces)
        $newItem.total = [math]::Round($item.total * $conversionFactors[$targetUnit].fromKB, $decimalPlaces)
        $newItem.units = $conversionFactors[$targetUnit].label
    }
    elseif ($item.name -eq "storage") {
        # Storage is in MB, convert from MB
        $newItem.allocated = [math]::Round($item.allocated * $conversionFactors[$targetUnit].fromMB, $decimalPlaces)
        $newItem.total = [math]::Round($item.total * $conversionFactors[$targetUnit].fromMB, $decimalPlaces)
        $newItem.units = $conversionFactors[$targetUnit].label
    }
    
    $newItem
}
$metal = $convertedMetal

# And update the formatting in the Graph section:
if ($Graph) {
    Write-Host ("=" * 60)
    
    foreach ($item in $metal) {
        # Calculate percentage
        if ($item.total -and $item.total -gt 0) {
            $percent = [math]::Round(($item.allocated / $item.total) * 100)
        }
        else {
            $percent = 0
        }
        
        # Format the name with proper spacing
        $nameDisplay = $item.name.PadRight(8)
        
        Write-Host "$nameDisplay" -NoNewline
        Show-PercentageGraphSolid -percent $percent
        
        # Determine format based on unit size
        $formatString = switch ($targetUnit) {
            'B'  { "N0" }  # No decimals for Bytes
            'KB' { "N0" }  # No decimals for KB
            'MB' { "N2" }  # 2 decimals for MB
            'GB' { "N2" }  # 2 decimals for GB
            'TB' { "N4" }  # 4 decimals for TB
            'EB' { "N6" }  # 6 decimals for EB
            default { "N2" }
        }
        
        # Format numbers
        $allocatedFormatted = "{0:$formatString}" -f $item.allocated
        $totalFormatted = "{0:$formatString}" -f $item.total
        
        Write-Host " - $allocatedFormatted / $totalFormatted $($item.units)"
    }
    
    Write-Host ""
}
  
    else {
        # Return normal object output
        return $metal
    }
}
}

function Get-MetalStorage {
    <#
    .SYNOPSIS
        Gets metal storage (Ceph) information from the Cloud Server.
     
    .DESCRIPTION
        Retrieves metal storage cluster status including health, capacity, OSDs, and PGs.
        Automatically handles token refresh.
     
    .PARAMETER Graph
        Optional. Display storage metrics with visual percentage graphs
     
    .PARAMETER ShowFlags
        Optional. Display Ceph flags in a formatted table
     
    .PARAMETER GB
        Display storage in Gigabytes (default)
     
    .PARAMETER TB
        Display storage in Terabytes
     
    .EXAMPLE
        # Get storage information
        Get-MetalStorage
         
    .EXAMPLE
        # Get storage information and display it graphically
        Get-MetalStorage -Graph
         
    .EXAMPLE
        # Get storage flags currently applied
        Get-MetalStorage -ShowFlags
         
    .EXAMPLE
        # Get storage flags currently applied and show stats graphically
        Get-MetalStorage -Graph -ShowFlags
         
    .EXAMPLE
        # Get storage information in TB and display stats graphically
        Get-MetalStorage -Graph -TB
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [switch]$Graph,
        
        [Parameter(Mandatory = $false)]
        [switch]$ShowFlags,
        
        [Parameter(Mandatory = $false)]
        [switch]$GB,
        
        [Parameter(Mandatory = $false)]
        [switch]$TB
    )
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/storage"
    
    # Use the helper function which handles token refresh automatically
    $response = Invoke-CloudApiRequest -Uri $uri -Method Get
    
    # Extract storage info from the response
    $storage = $response
    
    # If only ShowFlags is specified (without Graph), display flags table and return
    if ($ShowFlags -and -not $Graph) {
        if ($storage.flags) {
            Write-Host "`n--- Ceph Flags ---" -ForegroundColor Cyan
            
            # Create table of flags
            $flagTable = $storage.flags | ForEach-Object {
                [PSCustomObject]@{
                    Flag = $_.name
                    Set = if ($_.is_set) { "Yes" } else { "No" }
                    Type = $_.type
                    Description = $_.description
                }
            }
            
            $flagTable | Format-Table -AutoSize -Wrap
        }
        else {
            Write-Host "No flags available" -ForegroundColor Gray
        }
        return
    }
    
    # Handle graphing if requested
    if ($Graph) {
        # Determine target unit and conversion factor
        $targetUnit = "GB"  # Default to GB
        $conversionFactors = @{
            'GB' = @{ fromMB = 1/1024; label = 'GB'; decimals = 2; format = 'N2' }
            'TB' = @{ fromMB = 1/1048576; label = 'TB'; decimals = 4; format = 'N4' }
        }
        
        if ($TB) { $targetUnit = 'TB' }
        
        $unitConfig = $conversionFactors[$targetUnit]
        
        Write-Host ("=" * 70)
        
        # Cluster Health
        $healthColor = switch ($storage.health) {
            'HEALTH_OK' { 'Green' }
            'HEALTH_WARN' { 'Yellow' }
            'HEALTH_ERR' { 'Red' }
            default { 'White' }
        }
        Write-Host "`nCluster Health: $($storage.health)" -ForegroundColor $healthColor
        Write-Host "Version: $($storage.version_name) ($($storage.version_number))" -ForegroundColor Gray
        
        if ($storage.alert_count -gt 0) {
            Write-Host "Alerts: $($storage.alert_count)" -ForegroundColor Yellow
        }
        
        # Storage Capacity
        Write-Host "`n--- Storage Capacity ---" -ForegroundColor Cyan
        $usedConverted = [math]::Round($storage.storage_used_MB * $unitConfig.fromMB, $unitConfig.decimals)
        $totalConverted = [math]::Round($storage.storage_total_MB * $unitConfig.fromMB, $unitConfig.decimals)
        $freeConverted = $totalConverted - $usedConverted
        $utilizationPercent = [math]::Round($storage.storage_utilization_percent)
        
        Write-Host " capacity " -NoNewline
        Show-PercentageGraphSolid -percent $utilizationPercent
        Write-Host (" - {0:$($unitConfig.format)} / {1:$($unitConfig.format)} {2} ({3:$($unitConfig.format)} free)" -f $usedConverted, $totalConverted, $unitConfig.label, $freeConverted)
        
        # OSD Status
        Write-Host "`n--- OSD Status ---" -ForegroundColor Cyan
        
        # OSDs Up - show the HEALTHY percentage (up/total)
        $osdUpPercent = if ($storage.osd_count -gt 0) {
            [math]::Round(($storage.osd_up / $storage.osd_count) * 100)
        } else { 0 }
        
        Write-Host " up " -NoNewline
        Show-PercentageGraphSolid -percent $osdUpPercent -Invert
        Write-Host (" {0,3}% - {1,3} / {2,3} OSDs" -f $osdUpPercent, $storage.osd_up, $storage.osd_count)
        
        # OSDs In - show the HEALTHY percentage (in/total)
        $osdInPercent = if ($storage.osd_count -gt 0) {
            [math]::Round(($storage.osd_in / $storage.osd_count) * 100)
        } else { 0 }
        
        Write-Host " in " -NoNewline
        Show-PercentageGraphSolid -percent $osdInPercent -Invert
        Write-Host (" {0,3}% - {1,3} / {2,3} OSDs" -f $osdInPercent, $storage.osd_in, $storage.osd_count)
        
        # Show warnings for down/out OSDs
        if ($storage.osd_down -gt 0) {
            Write-Host " down - $($storage.osd_down) OSDs" -ForegroundColor Yellow
        }
        if ($storage.osd_out -gt 0) {
            Write-Host " out - $($storage.osd_out) OSDs" -ForegroundColor Yellow
        }
        
        # Placement Group Status
        Write-Host "`n--- Placement Groups ---" -ForegroundColor Cyan
        
        $activeCleanCount = if ($storage.pgs_by_state.'active+clean') {
            $storage.pgs_by_state.'active+clean'
        } else { 0 }
        
        # Show the HEALTHY percentage
        $pgHealthyPercent = if ($storage.pg_count -gt 0) {
            [math]::Round(($activeCleanCount / $storage.pg_count) * 100)
        } else { 0 }
        
        Write-Host " healthy " -NoNewline
        Show-PercentageGraphSolid -percent $pgHealthyPercent -Invert
        Write-Host (" {0,3}% - {1,5} / {2,5} PGs (active+clean)" -f $pgHealthyPercent, $activeCleanCount, $storage.pg_count)
        
        # Show unhealthy PGs if any
        $unhealthyPGs = $storage.pg_count - $activeCleanCount
        if ($unhealthyPGs -gt 0) {
            Write-Host " unhealthy PGs: $unhealthyPGs" -ForegroundColor Yellow
            
            # Show PG states
            foreach ($state in $storage.pgs_by_state.PSObject.Properties) {
                if ($state.Name -ne 'active+clean') {
                    Write-Host " $($state.Name): $($state.Value)" -ForegroundColor Gray
                }
            }
        }
        
        # Pool and Object Info
        Write-Host "`n--- Cluster Statistics ---" -ForegroundColor Cyan
        Write-Host " Pools: $($storage.pool_count)"
        Write-Host " Objects: $("{0:N0}" -f $storage.object_count)"
        Write-Host " Avg PGs per OSD: $($storage.average_pgs_per_osd)"
        Write-Host " Monitors in quorum: $($storage.monitors_in_quorum)"
        
        # I/O Performance
        Write-Host "`n--- Current I/O ---" -ForegroundColor Cyan
        $readMBps = [math]::Round($storage.read_bytes_per_sec / 1048576, 2)
        $writeMBps = [math]::Round($storage.write_bytes_per_sec / 1048576, 2)
        Write-Host " Read: $("{0:N2}" -f $readMBps) MB/s"
        Write-Host " Write: $("{0:N2}" -f $writeMBps) MB/s"
        Write-Host " IOPS: $($storage.io_per_sec)"
        
        # Show flags if requested
        if ($ShowFlags -and $storage.flags) {
            Write-Host "`n--- Ceph Flags ---" -ForegroundColor Cyan
            
            # Create table of flags
            $flagTable = $storage.flags | ForEach-Object {
                [PSCustomObject]@{
                    Flag = $_.name
                    Set = if ($_.is_set) { "Yes" } else { "No" }
                    Type = $_.type
                    Description = $_.description
                }
            }
            
            $flagTable | Format-Table -AutoSize -Wrap
        }
        
        Write-Host ""
    }
    else {
        # Return normal object output
        return $storage
    }
}

function Rename-MetalDatastore {
    <#
    .SYNOPSIS
        Rename a datastore on the Cloud Server.
     
    .DESCRIPTION
        Renames a datatore. Automatically handles token refresh.
     
    .PARAMETER Name
        Old/current name of the datastore
         
    .PARAMETER NewName
        The new name of the datastore
             
    .EXAMPLE
        # Rename datastore "OldName" to "new-name"
        Rename-CloudImage -Name "OldName" -NewName "new-name"
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [string]$NewName
    )
    
    $rename = @{
        name = $NewName
    }
    
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/datastore/${Name}"
       
    $response = Invoke-CloudApiRequest -Uri $uri -Method Patch -Body $rename
    
    return $response
}

function Remove-MetalDatastore {
    <#
    .SYNOPSIS
        Removes a datastore from the Cloud Server.
     
    .DESCRIPTION
        Deletes a datastore by name. Automatically handles token refresh.
     
    .PARAMETER Name
        Required. Name of the datastore to remove
     
    .EXAMPLE
        # Remove a datastore and prompt for confirmation
        Remove-MetalDatastore -Name "TenantDatastore"
         
    .EXAMPLE
        # Remove a datastore and bypass the confirmation prompt
        Remove-MetalDatastore -Name "TenantDatastore" -Confirm:$false
         
    #>

    
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )
    
    process {
        # Get the Datastore details for confirmation message
        $datastore = (Get-CloudDatastore -Name $Name).name
        
        if (-not $datastore) {
            Write-Warning "Datastore $Name not found."
            return
        }
        
        
        # Confirm before deletion
        if ($PSCmdlet.ShouldProcess("Datastore $datastore", "Remove Datastore")) {
            
            $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/datastore/$Name"
            
            Write-Verbose "Removing Datastore: $Name"
            
            try {
                $response = Invoke-CloudApiRequest -Uri $uri -Method Delete
                Write-Host "Datastore $Name removed successfully." -ForegroundColor Green
                return $response
            }
            catch {
                # Check for specific error messages
                if ($_.Exception.Message -match "not found|does not exist") {
                    Write-Warning "Datastore $Name not found or already deleted."
                    return
                }
                else {
                    # Re-throw other errors
                    throw
                }
            }
        }
    }
}

function New-MetalDatastore {
    <#
    .SYNOPSIS
        Creates a new datastore on the Cloud Server.
     
    .DESCRIPTION
        Creates a new datastore. Automatically handles token refresh.
     
    .PARAMETER Name
        Required. Name of the datastore. Cannot contain spaces
         
    .PARAMETER Protection
        Required. Protection scheme of the datastore. Valid options are triple_replication, ec4+2, ec8+3, ec8+4
     
    .EXAMPLE
        # Creates a new datastore named "3REP-Datastore" that is triple-replicated
        New-MetalDatastore -Name "3REP-Datastore" -Protection triple_replication
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateScript({
            if ($_ -match '\s') {
                throw "Name cannot contain spaces: $_"
            }
            return $true
        })]
        [string]$Name,
        
        [Parameter(Mandatory = $True)]
        [ValidateScript({
            if ($_ -match '\s') {
                throw "Name cannot contain spaces: $_"
            }
            return $true
        })]
        [string]$Protection
        
    )
    
    # Build the configuration - NAME goes INSIDE the template string
    $config = [PSCustomObject]@{
        name = $Name
        scheme = $Protection
    }
    
    # Build the URI
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v2/metal/datastore"
    
    Write-Verbose "Creating datastore '$Name' with protection scheme '$Protection'"
    Write-Verbose "Request body: $($config | ConvertTo-Json -Compress)"
    
    # Use the helper function which handles token refresh automatically
    $response = Invoke-CloudApiRequest -Uri $uri -Method Post -Body $config
    
    Write-Host "Datastore '$Name' created successfully." -ForegroundColor Green
    return $response
}