Documentation menu

Deployment targets

Use Octopus docs with AI

Find unused targets

An example script the will find provide a list of targets being unused that might cause your target count to be inflated.

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 "nuget: Octopus.Client"

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
}