Use PowerShell to Refresh CenturyLink Cloud VM Snapshots

Summary: VMs created in the CenturyLink Cloud, can have a snapshot (only 1 per VM), and that snapshot has a maximum life of 10 days. Therefore if it’s necessary to maintain a perpetual snapshot (like for test VMs that have a baseline configuration), VMs need to have their snapshots routinely refreshed. The following PowerShell script leverages the powerful CenturyLink Cloud REST v2 API to automate this process. This script relies on the presence of a ‘bearer token’ for authentication into the CLC API. Details on how to create this ‘bearer token’ can be found in this blog post (as well as details on how to discover your Group ID). The complete reference to CenturyLink’s Cloud REST API can be found here: https://www.ctl.io/api-docs/v2/

This script basically leverages CLC APIs to restore, delete, and create a VM snapshot. After each API is called, the script will wait until the task is complete, by querying the status of the task requested.

Note: This script sample requires PowerShell v4+

Refresh-Snapshot.ps1

#
# Name:: Refresh-Snapshot.ps1
# Version:: 0.1.2 (3/5/2016)
# Script Description:: Refreshes a server's snapshot by restoring the snapshot, deleting it, then creating a new one.
#
# API Documentation:: https://www.ctl.io/api-docs/v2/
#
# Author(s):: Otto Helweg
#

param($s)

# Display help
if (($Args -match "-\?|--\?|-help|--help|/\?|/help") -or !($s)) {
  Write-Host "Usage: Refresh-Snapshot.ps1"
  Write-Host "     -s [server name]       (Required) Server's name according to Centurylink Cloud"
  Write-Host "     -r                     Revert VM to existing snapshot before refreshing"
  Write-Host ""
  Write-Host "EXAMPLES:"
  Write-Host "     Refresh-Snapshot.ps1 -s CA3TESTTEST01 -r"
  Write-Host ""
  exit
}

$goodStatus = @("notStarted","executing","succeeded","resumed")

# PowerShell v4 is required for the REST cmdlets
if (!($psversiontable.PSVersion.Major -ge 4)) {
  Write-Host -ForegroundColor "red" "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
  exit
}

# Check to make sure the Bearer Token is less than 2 weeks old
if (!(Test-Path .\bearerToken.txt)) {
  Write-Host -ForegroundColor Red "Error: Bearer Token file is missing. Run Save-BearerToken.ps1"
  exit
} else {
  $fileInfo = dir .\bearerToken.txt
  if ($fileInfo.LastWriteTime -lt (Get-Date).AddDays(-11)) {
    Write-Host -ForegroundColor Yellow "Warning: Bearer Token file is almost out of date. Run Save-BearerToken.ps1"
  }
  if ($fileInfo.LastWriteTime -lt (Get-Date).AddDays(-13)) {
    Write-Host -ForegroundColor Red "Error: Bearer Token file is out of date. Run Save-BearerToken.ps1"
    exit
  }
}

$bearerTokenInput = Get-Content ".\bearerToken.txt"
$accountAlias = "SXSW"
$bearerToken = " Bearer " + $bearerTokenInput
$header = @{}
$header["Authorization"] = $bearerToken
$serverName = $s

Write-Host -ForegroundColor Green "Getting server properties for $serverName..."
$requestUri = "https://api.ctl.io/v2/servers/$accountAlias/$serverName"
$serverProperties = Invoke-RestMethod -Method GET -Headers $header -Uri $requestUri -ContentType "application/json"

if (!$serverProperties) {
  Write-Host -ForegroundColor Red "Error: $serverName does not appear to exist!"
  exit
}

if (($Args -contains "-r") -and $serverProperties.details.snapshots) {
  Write-Host -ForegroundColor Green "Restoring snapshot for $serverName..."
  $startTime = Get-Date
  $requestUri = "https://api.ctl.io$($serverProperties.details.snapshots.links.href[0])/restore"
  $restoreResult = Invoke-RestMethod -Method POST -Headers $header -Uri $requestUri -ContentType "application/json"
  $statusUri = "https://api.ctl.io/v2/operations/$accountAlias/status/$($restoreResult.id)"
  $continue = $true
  Write-Host "Waiting for snapshot restore to complete..."
  while ($continue) {
    $statusResult = Invoke-RestMethod -Method GET -Headers $header -Uri $statusUri -ContentType "application/json"
    [int]$elapsedSeconds = ($(Get-Date) - $startTime).TotalSeconds
    Write-Host "   [$elapsedSeconds seconds] $($statusResult.status)"
    if ($statusResult.status -eq "succeeded") {
      $continue = $false
    }
    if ($statusResult.status -notin $goodStatus) {
      Write-Host -ForegroundColor Red "Error: Restoring snapshot for $serverName failed!"
      exit
    }
    Sleep 2
  }
}

if ($serverProperties.details.snapshots) {
  Write-Host -ForegroundColor Green "Deleting snapshot for $serverName..."
  $startTime = Get-Date
  $requestUri = "https://api.ctl.io$($serverProperties.details.snapshots.links.href[0])"
  $restoreResult = Invoke-RestMethod -Method DELETE -Headers $header -Uri $requestUri -ContentType "application/json"
  $statusUri = "https://api.ctl.io/v2/operations/$accountAlias/status/$($restoreResult.id)"
  $continue = $true
  Write-Host "Waiting for snapshot delete to complete..."
  while ($continue) {
    $statusResult = Invoke-RestMethod -Method GET -Headers $header -Uri $statusUri -ContentType "application/json"
    [int]$elapsedSeconds = ($(Get-Date) - $startTime).TotalSeconds
    Write-Host "   [$elapsedSeconds seconds] $($statusResult.status)"
    if ($statusResult.status -eq "succeeded") {
      $continue = $false
    }
    if ($statusResult.status -notin $goodStatus) {
      Write-Host -ForegroundColor Red "Error: Deleting snapshot for $serverName failed!"
      exit
    }
    Sleep 2
  }
}

Write-Host -ForegroundColor Green "Creating snapshot for $serverName..."
$startTime = Get-Date
$createBody = "{
  ""snapshotExpirationDays"":""10"",
  ""serverIds"":[
      ""$serverName""
    ]
}"

$requestUri = "https://api.ctl.io/v2/operations/$accountAlias/servers/createSnapshot"
$restoreResult = Invoke-RestMethod -Method POST -Headers $header -Body $createBody -Uri $requestUri -ContentType "application/json"
$statusUri = "https://api.ctl.io/v2/operations/$accountAlias/status/$($restoreResult.links.id)"

$continue = $true
Write-Host "Waiting for snapshot creation to complete..."
while ($continue) {
  $statusResult = Invoke-RestMethod -Method GET -Headers $header -Uri $statusUri -ContentType "application/json"
  [int]$elapsedSeconds = ($(Get-Date) - $startTime).TotalSeconds
  Write-Host "   [$elapsedSeconds seconds] $($statusResult.status)"
  if ($statusResult.status -eq "succeeded") {
    $continue = $false
  }
  if ($statusResult.status -notin $goodStatus) {
    Write-Host -ForegroundColor Red "Error: Creating snapshot for $serverName failed!"
    exit
  }
  Sleep 2
}

 

Enjoy!

Using PowerShell to Pause CenturyLink Cloud Virtual Servers

Summary

The following script will pause all servers in the specified group for the account alias ‘SXSW’. You can discover your Group ID by querying an existing server (see example towards the end of this blog post). Pausing your Virtual Servers in the CenturyLink Cloud merely saves their state (memory and disk) and can easily be resumed by Starting them (unlike shutting them down or turning them off which does not save the state of their memory). Pausing servers is similar to ‘sleep’ mode supported by most computers. This can be useful for reducing your CLC costs when virtual servers are not being used. This script reads the necessary BearerToken credentials from a text file (bearerToken.txt) that is created by the following script (keep in mind that this token has a life of only 2 weeks and will then need to be regenerated):

Note: All of these script samples are using PowerShell v4 (required)

Example: Save-BearerToken.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
 Write-Host -ForegroundColor Red "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
 exit
}

$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUri = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Uri $logonUri -Body $creds -ContentType "application/json"

Write-Host -ForegroundColor "green" "Account Summary:"
$logonResponse

Write-Host -ForegroundColor "green" "Bearer Token (2 week TTL):"
$logonResponse.bearerToken
$logonResponse.bearerToken | Set-Content "bearerToken.txt"
Example: Pause-Servers.ps1
$bearerTokenInput = Get-Content ".\bearerToken.txt"
$groupId = "7c3a1aee32241223a1aee32241979a28"
$accountAlias = "SXSW"
$bearerToken = " Bearer " + $bearerTokenInput
$header = @{}
$header["Authorization"] = $bearerToken

# Discover all servers in the specified group
$requestUri = "https://api.ctl.io/v2/groups/$accountAlias/$groupId"
$groupOutput = Invoke-RestMethod -Method GET -Headers $header -Uri $requestUri -ContentType "application/json" -SessionVariable "theSession"

$serverArray = @()
$serverPause = @()
foreach ($item in $groupOutput.links) {
  if ($item.rel -eq "server") {
    $serverName = $item.href.split("/")[($item.href.split("/").count - 1)]
    $serverArray = $serverArray + ($serverName)
  }
}

# Gather server status
$serverList = ""
foreach ($serverName in $serverArray) {
  Write-Host -ForegroundColor Green "Getting server properties for $serverName..."
  $requestUri = "https://api.ctl.io/v2/servers/$accountAlias/$serverName"
  $serverProperties = Invoke-RestMethod -Method GET -Headers $header -Uri $requestUri -ContentType "application/json" -SessionVariable "theSession"
  $powerState = $serverProperties.details.powerState
  Write-Host -ForegroundColor Green "  PowerState: $powerState"
  if ($serverProperties.details.powerState -eq "started") {
    Write-Host -ForegroundColor Yellow "  Pausing $serverName"
    $serverPause = $serverPause + ($serverName)
    $serverList = "$serverList, $serverName"
  }
}

$serverList = $serverList.TrimStart(", ")
if ($serverPause.Count -eq 1) {
  $servers = "[ ""$serverPause"" ]"
} else {
  $servers = $serverPause | ConvertTo-Json
}

Write-Host -ForegroundColor Green "Sending Server pause request for $serverList"
Write-Host "servers: $servers"
Write-Host "serverPause: $serverPause"
$requestUri = "https://api.ctl.io/v2/operations/$accountAlias/servers/pause"
$pauseRequest = Invoke-RestMethod -Method POST -Headers $header -Uri $requestUri -Body $servers -ContentType "application/json" -SessionVariable "theSession"

Write-Host "Request Output: $pauseRequest"

Getting Your Group ID From Your Server

The following sample script will retrieve your Group ID from an existing server. You will need to incorporate your specific server name in this script (we use ‘CA3TESTTEST01’).

Example: List-ServerGroup.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
  Write-Host -ForegroundColor Red "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
  exit
}
$dataCenter = "CA3"
$serverName = "CA3TESTTEST01"
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"

Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUri = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUri -Body $creds -ContentType "application/json" -SessionVariable "theSession"

$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias

$headers.Add("Authorization",$bearerToken)

Write-Host -ForegroundColor "green" "Getting datacenter capabilities for $dataCenter..."
$RequestUri = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3/deploymentCapabilities"
$deployCapabilities = Invoke-RestMethod -Method GET -Headers $headers -Uri $RequestUri -ContentType "application/json" -SessionVariable "theSession"

Write-Host -ForegroundColor "green" "This datacenter supports the following platform templates:"
$deployCapabilities.templates
Output:

CLC Output 3

Extra: Task Scheduler Rule

The following is a rule that can be imported into Task Scheduler to run the above script on a regular basis.

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2015-06-19T10:56:50.2629119</Date>
<Author>CA3SXSWTEST01\Administrator</Author>
</RegistrationInfo>
<Triggers>
<CalendarTrigger>
<StartBoundary>2015-06-19T19:00:00</StartBoundary>
<Enabled>true</Enabled>
<ScheduleByDay>
<DaysInterval>1</DaysInterval>
</ScheduleByDay>
</CalendarTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>CA3SXSWTEST01\Administrator</UserId>
<LogonType>Password</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>.\Pause-Servers.ps1</Arguments>
<WorkingDirectory>C:\Users\Administrator\Documents\Scripts</WorkingDirectory>
</Exec>
</Actions>
</Task>

Enjoy!

PowerShell and the CenturyLink Cloud API

Summary

CenturyLink has a powerful REST API for automating the provisioning and management of virtual servers in their environment. And since PowerShell v4 now has cmdlets that support interacting with REST APIs, it is an easy way to work with entities in the CenturyLink Cloud. The following post will cover the steps required (and sample code) to use PowerShell to provision a CenturyLink Cloud virtual server. Plain old Internet connectivity is all that’s required between PowerShell and CenturyLink, since their API is on the open Internet. The complete reference to CenturyLink’s Cloud REST API can be found here: https://www.ctl.io/api-docs/v2/

Steps:

  1. Get a free account for CenturyLink Cloud here: https://www.centurylinkcloud.com/
  2. Get your Account Alias
  3. Get your Group ID (Default Group)
  4. Query for the capabilities of your desired datacenter (you can find a list of datacenters from the CenturyLink Cloud Control Portal here: https://control.ctl.io/)
  5. Create your server!

Note: All of these script samples are using PowerShell v4 (required)

Get Your Account Alias

Once you get your CenturyLink account, you set your account alias to an abbreviation between 2 and 4 characters. You can also get your AccountAlias from the REST API logon response. You also need to get the BearerToken from the logon response to use as your authentication mechanism with communicating with the CenturyLink REST API (a bearer token has a life of 2 weeks, so don’t hard code it into your scripts).

Example: LogonResponse.ps1
if (!($psversiontable.PSVersion.Major -ge 4)) {
  Write-Host -ForegroundColor "red" "Requires PowerShell v.4 or greater. Please install the Windows Management Framework 4 or above."
  exit
}
 
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json
 
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
 
Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "Account Summary:"
$logonResponse
 
Write-Host -ForegroundColor "green" "Bearer Token (2 week TTL):"
$logonResponse.bearerToken
Output:
PS-CLC Output 1

Find the Default Group for Your Server

This example is going to use your account’s datacenter “Default Group” as the destination for your new server, specifically for the datacenter ‘CA3’ (in Calgary, Canada). Any group can be used, but your account will have a “Default Group” for each datacenter your account can access. This group ID is required in order to identify the destination for your new server.

Example:  GetDefaultDatacenter.ps1
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"

# Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"

$dataCenter = "CA3"
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias

$headers.Add("Authorization",$bearerToken)

Write-Host -ForegroundColor Green "Getting datacenter groups for $dataCenter..."
$requestURL = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3?groupLinks=true"
$datacenterLinks = Invoke-RestMethod -Method GET -Headers $headers -Uri $requestURL -ContentType "application/json" -SessionVariable "theSession"
Write-Host "Available links:"
Write-Host $datacenterLinks.links

foreach ($link in $datacenterLinks.links) {
  if ($link.rel -eq "group") {
    Write-Host -ForegroundColor Green "Getting default datacenter group for group $($link.id)..."
    $requestURL = "https://api.ctl.io/v2/groups/$accountAlias/$($link.id)"
    $datacenterGroups = Invoke-RestMethod -Method GET -Headers $headers -Uri $requestURL -ContentType "application/json" -SessionVariable "theSession"
    foreach ($group in $datacenterGroups.Groups) {
      if ($group.type -eq "default") {
        Write-Host "Found group:"
        Write-Host $group
        Write-Host "`n"
        Write-Host -ForegroundColor Green "Default Group for $($dataCenter): $($group.id)"
      }
    }
  }
}
Output:

PS-CLC Output 2

Query for the Capabilities of Your Desired Datacenter

You can also get a list of available datacenters through the API with the following URI: https://api.ctl.io/v2/datacenters/yourAccountAlias. For this example we’re going to query CA3 (Canada – Toronto). As of this writing, the list includes: CA1, CA2, CA3, DE1, GB1, GB3, IL1, NY1, SG1, UC1, UT1, VA1, WA1. You will want to select the desired platform for your virtual server from the datacenter’s available templates as well as the ID of your destination network. If you don’t have any networks yet, the easiest way to create on is to first deploy a virtual server through the Control Portal web site. This can also be created through the API, but for the sake of brevity, we’ll use a network that already exists.

Partial Example (using the results from logonResponse.ps1): Query the Datacenter
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias
 
$headers.Add("Authorization",$bearerToken)
 
Write-Host -ForegroundColor "green" "Getting datacenter capabilities for $dataCenter..."
$RequestURL = "https://api.ctl.io/v2/datacenters/$accountAlias/CA3/deploymentCapabilities"
$deployCapabilities = Invoke-RestMethod -Method GET -Headers $headers -Uri $RequestURL -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "This datacenter supports the following platform templates:"
$deployCapabilities.templates
 
Write-Host -ForegroundColor "green" "This datacenter supports the following networks:"
$deployCapabilities.deployableNetworks
Output

CLC Output 2

Finally, Create Your Server

After gathering the destination group (default group for this example), provisioning template (sourceServerId) and the destination network, you can use the following script to create your virtual server in the CenturyLink Cloud.

Note: There is a character limitation for the ‘name’ field (6 I believe but this example uses a 4 character name).

Example: CreateServer.ps1
$server = @{
  name = "test"
  description = "My test server"
  groupId = "7c39b8a1aee32241979a282241979a28"
  sourceServerId = "WIN2012R2DTC-64"
  isManagedOS = "false"
  networkId = "7J3aCCm0GGF5ev4cmh6U58279b6f056a"
  password = "SomePassword!"
  cpu = 2
  memoryGB = 4
  type = "standard"
  storageType = "standard"
}
$server = $server | ConvertTo-Json
 
$psCreds = Get-Credential -Message "Use CLC Web Portal Credentials"
$creds = @{
 username = $psCreds.username
 password = $psCreds.GetNetworkCredential().Password
}
$creds = $creds | ConvertTo-Json
 
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
 
Write-Host -ForegroundColor "green" "Getting authentication 'bearerToken'..."
$logonUrl = "https://api.ctl.io/v2/authentication/login"
$logonResponse = Invoke-RestMethod -Method Post -Headers $headers -Uri $logonUrl -Body $creds -ContentType "application/json" -SessionVariable "theSession"
 
$bearerToken = " Bearer " + $logonResponse.bearerToken
$accountAlias = $logonResponse.accountAlias
 
$headers.Add("Authorization",$bearerToken)
 
Write-Host -ForegroundColor "green" "Sending Server build request..."
$requestURL = "https://api.ctl.io/v2/servers/$accountAlias"
$buildRequest = Invoke-RestMethod -Method POST -Headers $headers -Uri $requestURL -Body $server -ContentType "application/json" -SessionVariable "theSession"
 
Write-Host -ForegroundColor "green" "Request Output:"
Write-Host -ForegroundColor "green" "Server Name:"
$buildRequest.server
Write-Host -ForegroundColor "green" "Is Queued?"
$buildRequest.isQueued
Write-Host -ForegroundColor "green" "Queue Status Links:"
$buildRequest.links
Output

PS-CLC Output 4

Enjoy!