This script will loop through all the targets in all spaces on an instance and will return:
- How many cloud region targets which are not counted against your license
- How many duplicate listening Tentacles you have
- How many targets that are disabled
- How many targets are being reported as offline
- How many targets have never been used in a deployment
- How many targets haven’t had a deployment in over
x
days
Usage
Provide values for the following:
- Octopus URL
- Octopus API Key - the user associated with the API key will need read-only permissions on all spaces
- Days Since Last Deployment - the number of days to allow before considering the target is inactive, default is 90
- Include machine lists - boolean specifying whether to include the machines as part of the summary
Script
PowerShell (REST API)
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$octopusUrl = "https://your-octopus-url" ## Octopus URL to look at
$octopusApiKey = "API-YOUR-KEY" ## API key of user who has permissions to view all spaces, cancel tasks, and resubmit runbooks runs and deployments
$daysSinceLastDeployment = 90 ## The number of days since the last deployment to be considered unused. Any target without a deployment in the last [90] days is considered inactive.
$includeMachineLists = $false; ## If true, all machines in each category will get listed out to the console. If false, just a summary of information will be included.
$unsupportedCommunicationStyles = @("None")
$tentacleCommunicationStyles = @("TentaclePassive")
function Invoke-OctopusApi
{
param
(
$octopusUrl,
$endPoint,
$spaceId,
$apiKey,
$method,
$item
)
$octopusUrlToUse = $OctopusUrl
if ($OctopusUrl.EndsWith("/"))
{
$octopusUrlToUse = $OctopusUrl.Substring(0, $OctopusUrl.Length - 1)
}
if ([string]::IsNullOrWhiteSpace($spaceId))
{
$url = "$octopusUrlToUse/api/$EndPoint"
}
else
{
$url = "$octopusUrlToUse/api/$spaceId/$EndPoint"
}
try
{
if ($null -ne $item)
{
$body = $item | ConvertTo-Json -Depth 10
Write-Verbose $body
Write-Host "Invoking $method $url"
return Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -Body $body -ContentType 'application/json; charset=utf-8'
}
Write-Verbose "No data to post or put, calling bog standard Invoke-RestMethod for $url"
$result = Invoke-RestMethod -Method $method -Uri $url -Headers @{"X-Octopus-ApiKey" = "$ApiKey" } -ContentType 'application/json; charset=utf-8'
return $result
}
catch
{
if ($null -ne $_.Exception.Response)
{
if ($_.Exception.Response.StatusCode -eq 401)
{
Write-Error "Unauthorized error returned from $url, please verify API key and try again"
}
elseif ($_.Exception.Response.statusCode -eq 403)
{
Write-Error "Forbidden error returned from $url, please verify API key and try again"
}
else
{
Write-Error -Message "Error calling $url $($_.Exception.Message) StatusCode: $($_.Exception.Response.StatusCode )"
}
}
else
{
Write-Verbose $_.Exception
}
}
Throw $_.Exception
}
function Update-CategorizedMachines
{
param (
$categorizedMachines,
$space
)
$machineList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "machines?skip=0&take=10000" -spaceId $space.Id -method "GET"
foreach ($machine in $machineList.Items)
{
$categorizedMachines.TotalMachines += 1
if ($unsupportedCommunicationStyles -contains $machine.Endpoint.CommunicationStyle)
{
$categorizedMachines.NotCountedMachines += $machine
continue
}
if ($tentacleCommunicationStyles -contains $machine.Endpoint.CommunicationStyle)
{
$duplicateTentacle = $categorizedMachines.ListeningTentacles | Where-Object {$_.Thumbprint -eq $machine.Thumbprint -and $_.EndPoint.Uri -eq $machine.Endpoint.Uri }
if ($null -ne $duplicateTentacle)
{
$categorizedMachines.DuplicateTentacles += $machine
$categorizedMachines.ActiveMachines -= 1
}
$categorizedMachines.ListeningTentacles += $machine
}
if ($machine.IsDisabled -eq $true)
{
$categorizedMachines.DisabledMachines += $machine
continue
}
$categorizedMachines.ActiveMachines += 1
if ($machine.Status -ne "Online")
{
$categorizedMachines.OfflineMachines += $machine
}
$deploymentsList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "machines/$($machine.Id)/tasks?skip=0" -spaceId $space.Id -method "GET"
if ($deploymentsList.Items.Count -le 0)
{
$categorizedMachines.UnusedMachines += $machine
continue
}
$deploymentDate = [datetime]::Parse($deploymentsList.Items[0].CompletedTime)
$deploymentDate = $deploymentDate.ToUniversalTime()
$dateDiff = $currentUtcTime - $deploymentDate
if ($dateDiff.TotalDays -gt $daysSinceLastDeployment)
{
$categorizedMachines.OldMachines += $machine
}
}
}
$currentUtcTime = $(Get-Date).ToUniversalTime()
$categorizedMachines = @{
NotCountedMachines = @()
DisabledMachines = @()
ActiveMachines = 0
OfflineMachines = @()
UnusedMachines = @()
OldMachines = @()
TotalMachines = 0
ListeningTentacles = @()
DuplicateTentacles = @()
}
# Need to check the Octopus Server version for spaces feature
Write-Host "Checking Octopus Server version..."
$apiInfo = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint $null -method "GET"
$version = $apiInfo.Version
$versionParts = $apiInfo.Version.Split(".")
if ($versionParts[0] -ge 2019) {
Write-Host "Octopus Server version $version supports spaces, checking all spaces."
$spaceList = Invoke-OctopusApi -octopusUrl $octopusUrl -apiKey $octopusApiKey -endPoint "spaces?skip=0&take=1000" -spaceId $null -method "GET"
foreach ($space in $spaceList.Items)
{
Update-CategorizedMachines -categorizedMachines $categorizedMachines -space $space
}
} else {
Write-Host "Octopus Server version $version doesn't use spaces."
Update-CategorizedMachines -categorizedMachines $categorizedMachines
}
Write-Host "This instance has a total of $($categorizedMachines.TotalMachines) targets across all spaces."
Write-Host "There are $($categorizedMachines.NotCountedMachines.Count) cloud regions which are not counted."
Write-Host "There are $($categorizedMachines.DisabledMachines.Count) disabled machines that are not counted."
Write-Host "There are $($categorizedMachines.DuplicateTentacles.Count) duplicate listening tentacles that are not counted (assuming you are using 2019.7.3+)."
Write-Host ""
Write-Host "This leaves you with $($categorizedMachines.ActiveMachines) active targets being counted against your license (this script is excluding the $($categorizedMachines.DuplicateTentacles.Count) duplicates in that active count)."
Write-Host "Of that combined number, $($categorizedMachines.OfflineMachines.Count) are showing up as offline."
Write-Host "Of that combined number, $($categorizedMachines.UnusedMachines.Count) have never had a deployment."
Write-Host "Of that combined number, $($categorizedMachines.OldMachines.Count) haven't done a deployment in over $daysSinceLastDeployment days."
if ($includeMachineLists -eq $true){
Write-Host "Offline Targets"
Foreach ($target in $categorizedMachines.OfflineMachines)
{
Write-Host " - $($target.Name)"
}
Write-Host "No Deployment Ever Targets"
Foreach ($target in $categorizedMachines.UnusedMachines)
{
Write-Host " - $($target.Name)"
}
Write-Host " No deployments in the last $daysSinceLastDeployment days"
Foreach ($target in $categorizedMachines.OldMachines)
{
Write-Host " - $($target.Name)"
}
}
PowerShell (Octopus.Client)
# Load assembly
Add-Type -Path 'path:\to\Octopus.Client.dll'
$octopusURL = "https://your-octopus-url"
$octopusAPIKey = "API-YOUR-KEY"
$daysSinceLastDeployment = 90
$includeMachineLists = $false; ## If true, all machines in each category will get listed out to the console. If false, just a summary of information will be included.
$unsupportedCommunicationStyles = @("None")
$tentacleCommunicationStyles = @("TentaclePassive")
function Update-CategorizedMachines
{
param (
$categorizedMachines,
$space,
$client
)
$repositoryForSpace = $client.ForSpace($space)
$machineList = $repositoryForSpace.Machines.GetAll()
foreach ($machine in $machineList)
{
$categorizedMachines.TotalMachines += 1
if ($unsupportedCommunicationStyles -contains $machine.Endpoint.CommunicationStyle)
{
$categorizedMachines.NotCountedMachines += $machine
continue
}
if ($tentacleCommunicationStyles -contains $machine.Endpoint.CommunicationStyle)
{
$duplicateTentacle = $categorizedMachines.ListeningTentacles | Where-Object {$_.Thumbprint -eq $machine.Thumbprint -and $_.EndPoint.Uri -eq $machine.Endpoint.Uri }
if ($null -ne $duplicateTentacle)
{
$categorizedMachines.DuplicateTentacles += $machine
$categorizedMachines.ActiveMachines -= 1
}
$categorizedMachines.ListeningTentacles += $machine
}
if ($machine.IsDisabled -eq $true)
{
$categorizedMachines.DisabledMachines += $machine
continue
}
$categorizedMachines.ActiveMachines += 1
if ($machine.Status -ne "Online")
{
$categorizedMachines.OfflineMachines += $machine
}
$deploymentsList = $repositoryForSpace.Machines.GetTasks($machine)
if ($deploymentsList.Count -le 0)
{
$categorizedMachines.UnusedMachines += $machine
continue
}
$deploymentDate = [datetime]::Parse($deploymentsList[0].CompletedTime)
$deploymentDate = $deploymentDate.ToUniversalTime()
$dateDiff = $currentUtcTime - $deploymentDate
if ($dateDiff.TotalDays -gt $daysSinceLastDeployment)
{
$categorizedMachines.OldMachines += $machine
}
}
}
$endpoint = New-Object Octopus.Client.OctopusServerEndpoint($octopusURL, $octopusAPIKey)
$repository = New-Object Octopus.Client.OctopusRepository($endpoint)
$client = New-Object Octopus.Client.OctopusClient($endpoint)
$currentUtcTime = $(Get-Date).ToUniversalTime()
$categorizedMachines = @{
NotCountedMachines = @()
DisabledMachines = @()
ActiveMachines = 0
OfflineMachines = @()
UnusedMachines = @()
OldMachines = @()
TotalMachines = 0
ListeningTentacles = @()
DuplicateTentacles = @()
}
# Get all spaces
$spaces = $repository.Spaces.GetAll()
# Loop through spaces
foreach ($space in $spaces)
{
Update-CategorizedMachines -categorizedMachines $categorizedMachines -space $space -client $client
}
Write-Host "This instance has a total of $($categorizedMachines.TotalMachines) targets across all spaces."
Write-Host "There are $($categorizedMachines.NotCountedMachines.Count) cloud regions which are not counted."
Write-Host "There are $($categorizedMachines.DisabledMachines.Count) disabled machines that are not counted."
Write-Host "There are $($categorizedMachines.DuplicateTentacles.Count) duplicate listening tentacles that are not counted (assuming you are using 2019.7.3+)."
Write-Host ""
Write-Host "This leaves you with $($categorizedMachines.ActiveMachines) active targets being counted against your license (this script is excluding the $($categorizedMachines.DuplicateTentacles.Count) duplicates in that active count)."
Write-Host "Of that combined number, $($categorizedMachines.OfflineMachines.Count) are showing up as offline."
Write-Host "Of that combined number, $($categorizedMachines.UnusedMachines.Count) have never had a deployment."
Write-Host "Of that combined number, $($categorizedMachines.OldMachines.Count) haven't done a deployment in over $daysSinceLastDeployment days."
if ($includeMachineLists -eq $true){
Write-Host "Offline Targets"
Foreach ($target in $categorizedMachines.OfflineMachines)
{
Write-Host " - $($target.Name)"
}
Write-Host "No Deployment Ever Targets"
Foreach ($target in $categorizedMachines.UnusedMachines)
{
Write-Host " - $($target.Name)"
}
Write-Host " No deployments in the last $daysSinceLastDeployment days"
Foreach ($target in $categorizedMachines.OldMachines)
{
Write-Host " - $($target.Name)"
}
}
C#
class CategorizedMachines
{
// Define private variables
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _notCountedMachines = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _disabledMachines = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _offlineMachines = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _unusedMachines = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _oldMachines = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _listeningTentacles = new System.Collections.Generic.List<MachineResource>();
private System.Collections.Generic.List<Octopus.Client.Model.MachineResource> _duplicateTentacles = new System.Collections.Generic.List<MachineResource>();
// Define public properties
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> NotCountedMachines
{
get
{
return _notCountedMachines;
}
set
{
_notCountedMachines = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> DisabledMachines
{
get
{
return _disabledMachines;
}
set
{
_disabledMachines = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> OfflineMachines
{
get
{
return _offlineMachines;
}
set
{
_offlineMachines = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> UnusedMachines
{
get
{
return _unusedMachines;
}
set
{
_unusedMachines = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> OldMachines
{
get
{
return _oldMachines;
}
set
{
_oldMachines = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> ListeningTentacles
{
get
{
return _listeningTentacles;
}
set
{
_listeningTentacles = value;
}
}
public System.Collections.Generic.List<Octopus.Client.Model.MachineResource> DuplicateTentacles
{
get
{
return _duplicateTentacles;
}
set
{
_duplicateTentacles = value;
}
}
public int ActiveMachines
{
get;
set;
}
public int TotalMachines
{
get;
set;
}
}
static CategorizedMachines UpdateCategorizedMachines (CategorizedMachines categorizedMachines, Octopus.Client.Model.SpaceResource space, Octopus.Client.OctopusClient client, System.Collections.Generic.List<string> unsupportedCommunicationsStyles, System.Collections.Generic.List<string> tentacleCommunicationsStyles, int daysSinceLastDeployment)
{
var currentUtcTime = DateTime.Now.ToUniversalTime();
// Create repository for space
var repositoryForSpace = client.ForSpace(space);
// Get machines in space
var machines = repositoryForSpace.Machines.FindAll();
// Loop through machines
foreach (var machine in machines)
{
categorizedMachines.TotalMachines++;
if (unsupportedCommunicationsStyles.Contains(machine.Endpoint.CommunicationStyle.ToString()))
{
categorizedMachines.NotCountedMachines.Add(machine);
continue;
}
if (tentacleCommunicationsStyles.Contains(machine.Endpoint.CommunicationStyle.ToString()))
{
var duplicateTentacle = categorizedMachines.ListeningTentacles.FirstOrDefault(m => m.Thumbprint == machine.Thumbprint);
switch (machine.Endpoint.CommunicationStyle.ToString())
{
case "TentaclePassive":
{
var machineEndpoint = (Octopus.Client.Model.Endpoints.ListeningTentacleEndpointResource)machine.Endpoint;
if (duplicateTentacle != null && ((Octopus.Client.Model.Endpoints.ListeningTentacleEndpointResource)duplicateTentacle.Endpoint).Uri == machineEndpoint.Uri)
{
categorizedMachines.DuplicateTentacles.Add(machine);
categorizedMachines.ActiveMachines--;
}
categorizedMachines.ListeningTentacles.Add(machine);
break;
}
case "TentacleActive":
{
break;
}
}
}
if (machine.IsDisabled)
{
categorizedMachines.DisabledMachines.Add(machine);
continue;
}
categorizedMachines.ActiveMachines++;
if(machine.Status != Octopus.Client.Model.MachineModelStatus.Online)
{
categorizedMachines.OfflineMachines.Add(machine);
}
var deploymentList = repositoryForSpace.Machines.GetTasks(machine);
if (deploymentList.Count <= 0)
{
categorizedMachines.UnusedMachines.Add(machine);
continue;
}
var deploymentDate = deploymentList[0].CompletedTime.Value.ToUniversalTime();
var dateDiff = currentUtcTime - deploymentDate;
if (dateDiff.TotalDays > daysSinceLastDeployment)
{
categorizedMachines.OldMachines.Add(machine);
}
}
return categorizedMachines;
}
// If using .net Core, be sure to add the NuGet package of System.Security.Permissions
#r "path\to\Octopus.Client.dll"
using Octopus.Client;
using Octopus.Client.Model;
using System.Linq;
var octopusURL = "https://your-octopus-url";
var octopusAPIKey = "API-YOUR-KEY";
DateTime currentUtcTime = DateTime.Now.ToUniversalTime();
CategorizedMachines categorizedMachines = new CategorizedMachines();
int daysSinceLastDeployment = 90;
bool includeMachineLists = false;
System.Collections.Generic.List<string> unsupportedCommunicationsStyles = new System.Collections.Generic.List<string> { "None" };
System.Collections.Generic.List<string> tentacleCommunicationsStyles = new System.Collections.Generic.List<string> { "TentaclePassive" };
// Create repository object
var endpoint = new OctopusServerEndpoint(octopusURL, octopusAPIKey);
var repository = new OctopusRepository(endpoint);
var client = new OctopusClient(endpoint);
// Get all spaces
var spaces = repository.Spaces.FindAll();
// Loop through spaces
foreach (var space in spaces)
{
categorizedMachines = UpdateCategorizedMachines(categorizedMachines, space, client, unsupportedCommunicationsStyles, tentacleCommunicationsStyles, daysSinceLastDeployment);
}
Console.WriteLine(string.Format("This instance has a total of {0} targets across all spaces", categorizedMachines.TotalMachines.ToString()));
Console.WriteLine(string.Format("There are {0} cloud regions which are not counted", categorizedMachines.NotCountedMachines.Count.ToString()));
Console.WriteLine(string.Format("There are {0} disabled machines which are not counted", categorizedMachines.DisabledMachines.Count.ToString()));
Console.WriteLine(string.Format("There are {0} duplicate listening Tentacles that are not counted (assuming you are using 2019.7.3+", categorizedMachines.DuplicateTentacles.Count.ToString()));
Console.WriteLine(string.Empty);
Console.WriteLine(string.Format("This leaves you with {0} active targets being counted against your license (this process is excluding the {1} duplicates in that active count)", categorizedMachines.ActiveMachines.ToString(), categorizedMachines.DuplicateTentacles.Count.ToString()));
Console.WriteLine(string.Format("Of that combined number, {0} are showing up as offline", categorizedMachines.OfflineMachines.Count.ToString()));
Console.WriteLine(string.Format("Of that combined number, {0} have never had a deployment", categorizedMachines.UnusedMachines.Count.ToString()));
Console.WriteLine(string.Format("Of that combined number, {0} haven't done a deployment in over {1} days", categorizedMachines.OldMachines.Count.ToString(), daysSinceLastDeployment));
if (includeMachineLists)
{
Console.WriteLine(string.Format("Offline targets"));
foreach (var machine in categorizedMachines.OfflineMachines)
{
Console.WriteLine(string.Format("\t{0}", machine.Name));
}
Console.WriteLine(string.Format("No deployment ever targets"));
foreach (var machine in categorizedMachines.UnusedMachines)
{
Console.WriteLine(string.Format("\t{0}", machine.Name));
}
Console.WriteLine(string.Format("No deployments in the last {0} days", daysSinceLastDeployment));
foreach(var machine in categorizedMachines.OldMachines)
{
Console.WriteLine(string.Format("\t{0}", machine.Name));
}
}
Python3
import json
import requests
from requests.api import get, head
import datetime
from dateutil.parser import parse
def get_octopus_resource(uri, headers, skip_count = 0):
items = []
skip_querystring = ""
if '?' in uri:
skip_querystring = '&skip='
else:
skip_querystring = '?skip='
response = requests.get((uri + skip_querystring + str(skip_count)), headers=headers)
response.raise_for_status()
# Get results of API call
results = json.loads(response.content.decode('utf-8'))
# Store results
if hasattr(results, 'keys') and 'Items' in results.keys():
items += results['Items']
# Check to see if there are more results
if (len(results['Items']) > 0) and (len(results['Items']) == results['ItemsPerPage']):
skip_count += results['ItemsPerPage']
items += get_octopus_resource(uri, headers, skip_count)
else:
return results
# return results
return items
def find_duplicate_entry(categorized_machines, machine):
machineEndpoint = machine['Endpoint']
for entry in categorized_machines['ListeningTentacles']:
entryEndpoint = entry['Endpoint']
if entryEndpoint['Thumbprint'] == machineEndpoint['Thumbprint'] and entryEndpoint['Uri'] == machine['Uri']:
return entry
return None
def update_categorized_machines(categorized_machines, space, octopus_server_uri, headers, unsupported_communication_styles, tentacle_communication_styles):
# Get machines for space
uri = '{0}/api/{1}/machines'.format(octopus_server_uri, space['Id'])
machine_list = get_octopus_resource(uri, headers)
current_date = datetime.datetime.utcnow()
for machine in machine_list:
categorized_machines['TotalMachines'] += 1
if machine['Endpoint']['CommunicationStyle'] in unsupported_communication_styles:
categorized_machines['NotCountedMachines'].append(machine)
continue
if machine['Endpoint']['CommunicationStyle'] in tentacle_communication_styles:
if machine['Endpoint']['CommunicationStyle'] == "TentaclePassive":
# Search for duplicate
duplicate_machine = find_duplicate_entry(categorized_machines, machine)
if duplicate_machine != None:
categorized_machines['DuplicateTentacles'].append(machine)
categorized_machines['ActiveMachines'] -= 1
categorized_machines['ListeningTentacles'].append(machine)
if machine['IsDisabled'] == True:
categorized_machines['DisabledMachines'].append(machine)
continue
categorized_machines['ActiveMachines'] +=1
if machine['Status'] != "Online":
categorized_machines['OfflineMachines'].append(machine)
uri = '{0}/api/{1}/machines/{2}/tasks'.format(octopus_server_uri, space['Id'], machine['Id'])
deployment_list = get_octopus_resource(uri, headers)
if len(deployment_list) <= 0:
categorized_machines['UnusedMachines'].append(machine)
continue
deployment_date = parse(deployment_list[0]['CompletedTime'])
deployment_date = deployment_date.replace(tzinfo=None)
# Calculate the date difference
date_diff = current_date - deployment_date
if date_diff.days > days_since_last_deployment:
categorized_machines['OldMachines'].append(machine)
return categorized_machines
octopus_server_uri = 'https://your-octopus-url'
octopus_api_key = 'API-YOUR-KEY'
headers = {'X-Octopus-ApiKey': octopus_api_key}
categorized_machines = {
'NotCountedMachines': [],
'DisabledMachines': [],
'ActiveMachines': 0,
'OfflineMachines': [],
'OldMachines': [],
'TotalMachines': 0,
'ListeningTentacles': [],
'DuplicateTentacles': [],
'UnusedMachines': []
}
unsupported_communication_styles = ['None']
tentacle_communication_styles = ['TentaclePassive']
current_date = datetime.datetime.utcnow()
days_since_last_deployment = 90
include_machine_lists = False
# Get spaces
uri = '{0}/api/spaces'.format(octopus_server_uri)
spaces = get_octopus_resource(uri, headers)
# Loop through spaces
for space in spaces:
categorized_machines = update_categorized_machines(categorized_machines, space, octopus_server_uri, headers, unsupported_communication_styles, tentacle_communication_styles)
print('This instance has a total of {0} targets across all spaces'.format(categorized_machines['TotalMachines']))
print('There are {0} cloud regions which are not counted'.format(len(categorized_machines['NotCountedMachines'])))
print('There are {0} disabled machines that are not counted'.format(len(categorized_machines['DisabledMachines'])))
print('There are {0} duplicate listening tentacles that are not counted (assuming you are using 2019.7.3+)'.format(len(categorized_machines['DuplicateTentacles'])))
print('\n')
print('This leaves you with {0} active targets being counted against your license (this script is excluding the {1} duplicates in that active count'.format(categorized_machines['ActiveMachines'], len(categorized_machines['DuplicateTentacles'])))
print('Of that combined number, {0} are showing up as offline'.format(len(categorized_machines['OfflineMachines'])))
print('Of that combined number, {0} have never had a deployment'.format(len(categorized_machines['UnusedMachines'])))
print('Of that combined number, {0} have not done a deployment in over {1} days'.format(len(categorized_machines['OldMachines']), days_since_last_deployment))
if include_machine_lists:
print("Offline targets")
for target in categorized_machines['OfflineMachines']:
print("\t{0}".format(target['Name']))
print("No deployments ever")
for target in categorized_machines['UnusedMachines']:
print("\t{0}".format(target['Name']))
print ("No deployments in the last {0} days".format(days_since_last_deployment))
for target in categorized_machines['OldMachines']:
print("\t{0}".format(target['Name']))
Go
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/OctopusDeploy/go-octopusdeploy/octopusdeploy"
)
type CategorizedMachines struct {
NotCountedMachines []*octopusdeploy.DeploymentTarget
DisabledMachines []*octopusdeploy.DeploymentTarget
ActiveMachines int
OfflineMachines []*octopusdeploy.DeploymentTarget
UnusedMachines []*octopusdeploy.DeploymentTarget
OldMachines []*octopusdeploy.DeploymentTarget
TotalMachines int
ListeningTentacles []*octopusdeploy.DeploymentTarget
DuplicateTentacles []*octopusdeploy.DeploymentTarget
}
func main() {
apiURL, err := url.Parse("https://your-octopus-url")
if err != nil {
log.Println(err)
}
APIKey := "API-YOUR-KEY"
daysSinceLastDeployment := 90
includeMachineLists := true
categorizedMachines := CategorizedMachines{}
unsupportedCommunicationStyles := []string{"None"}
tentacleCommunicationStyles := []string{"TentaclePassive"}
// Create client object
client := octopusAuth(apiURL, APIKey, "")
// Get all spaces
spaces, err := client.Spaces.GetAll()
if err != nil {
log.Println(err)
}
for _, space := range spaces {
categorizedMachines = updateCategorizedMachines(apiURL, APIKey, space, categorizedMachines, unsupportedCommunicationStyles, tentacleCommunicationStyles, daysSinceLastDeployment)
}
fmt.Printf("This instance has a total of %[1]s targets across all spaces \n", strconv.Itoa(categorizedMachines.TotalMachines))
fmt.Printf("There are %[1]s cloud regions which are not counted \n", strconv.Itoa(len(categorizedMachines.NotCountedMachines)))
fmt.Printf("There are %[1]s disabled machines which are not counted \n", strconv.Itoa(len(categorizedMachines.DisabledMachines)))
fmt.Printf("There are %[1]s duplicate listening tentacles that are not counted (assuming you are using 2019.7.3+)\n", strconv.Itoa(len(categorizedMachines.DuplicateTentacles)))
fmt.Println("")
fmt.Printf("This leaves you with %[1]s active targets being counted against your license (this process is excluding %[2]s duplicates in that active count) \n", strconv.Itoa(categorizedMachines.ActiveMachines), strconv.Itoa(len(categorizedMachines.DuplicateTentacles)))
fmt.Printf("Of that combined number, %[1]s are showing up as offline\n", strconv.Itoa(len(categorizedMachines.OfflineMachines)))
fmt.Printf("Of that combined number, %[1]s have never had a deployment\n", strconv.Itoa(len(categorizedMachines.UnusedMachines)))
fmt.Printf("Of that combined number, %[1]s have not done a deployment in over %[2]s days\n", strconv.Itoa(len(categorizedMachines.OldMachines)), strconv.Itoa(daysSinceLastDeployment))
if includeMachineLists {
fmt.Println("Offline targets")
for _, target := range categorizedMachines.OfflineMachines {
fmt.Printf("\t%[1]s\n", target.Name)
}
fmt.Println("No deployments ever")
for _, target := range categorizedMachines.UnusedMachines {
fmt.Printf("\t%[1]s\n", target.Name)
}
fmt.Printf("No deployments in the last %[1]s days\n", strconv.Itoa(daysSinceLastDeployment))
for _, target := range categorizedMachines.OldMachines {
fmt.Printf("\t%[1]s\n", target.Name)
}
fmt.Printf("Duplicates\n")
for _, target := range categorizedMachines.DuplicateTentacles {
fmt.Printf("\t%[1]s\n", target.Name)
}
}
}
func octopusAuth(octopusURL *url.URL, APIKey, space string) *octopusdeploy.Client {
client, err := octopusdeploy.NewClient(nil, octopusURL, APIKey, space)
if err != nil {
log.Println(err)
}
return client
}
func arrayContains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func updateCategorizedMachines(octopusURL *url.URL, APIKey string, space *octopusdeploy.Space, categorizedMachines CategorizedMachines, unsupportedCommunicationStyles []string, tentacleCommunicationStyles []string, daysSinceLastDeployment int) CategorizedMachines {
currentDate := time.Now()
// Get client
client := octopusAuth(octopusURL, APIKey, space.ID)
// Get all machines
machines, err := client.Machines.GetAll()
if err != nil {
log.Println(err)
}
// Loop through machines
for _, machine := range machines {
categorizedMachines.TotalMachines++
if arrayContains(unsupportedCommunicationStyles, machine.Endpoint.GetCommunicationStyle()) {
categorizedMachines.NotCountedMachines = append(categorizedMachines.NotCountedMachines, machine)
continue
}
if arrayContains(tentacleCommunicationStyles, machine.Endpoint.GetCommunicationStyle()) {
if machine.Endpoint.GetCommunicationStyle() == "TentaclePassive" {
duplicateEntry := searchForDuplicateListening(categorizedMachines, machine)
if duplicateEntry != nil {
categorizedMachines.DuplicateTentacles = append(categorizedMachines.DuplicateTentacles, machine)
categorizedMachines.ActiveMachines--
}
categorizedMachines.ListeningTentacles = append(categorizedMachines.ListeningTentacles, machine)
}
}
if machine.IsDisabled {
categorizedMachines.DisabledMachines = append(categorizedMachines.DisabledMachines, machine)
continue
}
categorizedMachines.ActiveMachines++
if machine.Status != "Online" {
categorizedMachines.OfflineMachines = append(categorizedMachines.OfflineMachines, machine)
}
deploymentList := GetMachineTasks(octopusURL, APIKey, space, machine)
if len(deploymentList) <= 0 {
categorizedMachines.UnusedMachines = append(categorizedMachines.UnusedMachines, machine)
continue
}
latestDeployment := deploymentList[0].(map[string]interface{})
deploymentDate, err := time.Parse(time.RFC3339Nano, latestDeployment["CompletedTime"].(string))
if err != nil {
log.Println(err)
}
dateDiff := currentDate.Sub(deploymentDate).Hours() / 24
if dateDiff > float64(daysSinceLastDeployment) {
categorizedMachines.OldMachines = append(categorizedMachines.OldMachines, machine)
}
}
return categorizedMachines
}
func searchForDuplicateListening(categorizedMachines CategorizedMachines, machine *octopusdeploy.DeploymentTarget) *octopusdeploy.DeploymentTarget {
// Loop through listening tentacles
for _, entry := range categorizedMachines.ListeningTentacles {
if entry.Thumbprint == machine.Thumbprint && entry.URI == machine.URI {
return entry
}
}
return nil
}
func GetMachineTasks(octopusURL *url.URL, APIKey string, space *octopusdeploy.Space, machine *octopusdeploy.DeploymentTarget) []interface{} {
// Define api endpoint
tasksEndpoint := octopusURL.String() + "/api/" + space.ID + "/machines/" + machine.ID + "/tasks"
// Create http client
httpClient := &http.Client{}
skipAmount := 0
// Make request
request, _ := http.NewRequest("GET", tasksEndpoint, nil)
request.Header.Set("X-Octopus-ApiKey", APIKey)
response, err := httpClient.Do(request)
if err != nil {
log.Println(err)
}
// Get response
responseData, err := ioutil.ReadAll(response.Body)
var tasksJson interface{}
err = json.Unmarshal(responseData, &tasksJson)
// Map the returned data
returnedTasks := tasksJson.(map[string]interface{})
// Returns the list of items, translate it to a map
returnedItems := returnedTasks["Items"].([]interface{})
for true {
// check to see if there's more to get
fltItemsPerPage := returnedTasks["ItemsPerPage"].(float64)
itemsPerPage := int(fltItemsPerPage)
if len(returnedTasks["Items"].([]interface{})) == itemsPerPage {
// Increment skip amount
skipAmount += len(returnedTasks["Items"].([]interface{}))
// Make request
queryString := request.URL.Query()
queryString.Set("skip", strconv.Itoa(skipAmount))
request.URL.RawQuery = queryString.Encode()
response, err := httpClient.Do(request)
if err != nil {
log.Println(err)
}
responseData, err := ioutil.ReadAll(response.Body)
var releasesJson interface{}
err = json.Unmarshal(responseData, &releasesJson)
returnedTasks = releasesJson.(map[string]interface{})
returnedItems = append(returnedItems, returnedTasks["Items"].([]interface{})...)
} else {
break
}
}
return returnedItems
}
Help us continuously improve
Please let us know if you have any feedback about this page.
Page updated on Sunday, January 1, 2023