diff --git a/hyperv-sync/ITGlue-VMHost-CreateFlexibleAsset.ps1 b/hyperv-sync/ITGlue-VMHost-CreateFlexibleAsset.ps1
new file mode 100644
index 0000000..cae92b6
--- /dev/null
+++ b/hyperv-sync/ITGlue-VMHost-CreateFlexibleAsset.ps1
@@ -0,0 +1,251 @@
+# Script name: ITGlue-VMHost-CreateFlexibleAsset.ps1
+# Script type: Powershell
+# Script description: Creates a custom Flexible Asset called "VMHost". Use "ITGlue-VMHost-CreateFlexibleAsset.ps1" to update.
+# Dependencies: Powershell 3.0
+# Script maintainer: powerpack@upstream.se
+# https://en.upstream.se/powerpack/
+# --------------------------------------------------------------------------------------------------------------------------------
+
+$data = @{
+ type = "flexible_asset_types"
+ Attributes = @{
+ icon = "cubes"
+ description = "This Flexible Asset is to be used to automate VM host documentation."
+ Name = "VM Host"
+ enabled = $true
+ }
+ relationships = @{
+ flexible_asset_fields = @{
+ data = @(
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 1
+ Name = "VM host name"
+ kind = "Text"
+ hint = "This is the unique name and identifier of this Flexible Asset. It has to match the actual name of the VM Host to be docuemented with the associated Powershell script."
+ required = $true
+ use_for_title = $true
+ expiration = $false
+ show_in_list = $true
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 2
+ Name = "VM host configuration"
+ kind = "Header"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 3
+ Name = "VM host related IT Glue configuration"
+ kind = "Tag"
+ tag_type = "Configurations"
+ required = $true
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 4
+ Name = "Virtualization platform"
+ kind = "Select"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ default_value = "Hyper-V
+VMware"
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 5
+ Name = "CPU"
+ kind = "Number"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 6
+ Name = "RAM (GB)"
+ kind = "Number"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 7
+ Name = "Disk information"
+ kind = "Textbox"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 8
+ Name = "Virtual switches"
+ kind = "Textbox"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 9
+ Name = "VM guests configuration"
+ kind = "Header"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 10
+ Name = "Current number of VM guests on this VM host"
+ kind = "Number"
+ hint = "Number of guests detected on this VM host based on latest execution of the ducumentation atutomation script."
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 11
+ Name = "VM guest names and information"
+ kind = "Textbox"
+ hint = "VM guest names vCPUs RAM and other infromation."
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 12
+ Name = "VM guest virtual disk paths"
+ kind = "Textbox"
+ hint = "VM guests and virtual disk paths discovered on this VM host."
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 13
+ Name = "VM guests snapshot information"
+ kind = "Textbox"
+ hint = "All snapshots found on the host"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 14
+ Name = "VM guests BIOS settings"
+ kind = "Textbox"
+ hint = "Specifies the BIOS boot settings in each each discovered guest on this VM host."
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 15
+ Name = "Assigned virtual switches and IP information"
+ kind = "Textbox"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = ""
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 16
+ Name = "Force manual sync?"
+ kind = "Select"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $false
+ default_value = "Yes
+No"
+ }
+ },
+ @{
+ type = "flexible_asset_fields"
+ Attributes = @{
+ order = 17
+ Name = "This automated documentation is powered by Upstream Power Pack"
+ kind = "Header"
+ required = $false
+ use_for_title = $false
+ expiration = $false
+ show_in_list = $true
+ }
+ }
+ )
+ }
+ }
+}
+
+
+New-ITGlueFlexibleAssetTypes -data $data
\ No newline at end of file
diff --git a/hyperv-sync/ITGlue-VMHost-FeedFlexibleAssetHyperV.ps1 b/hyperv-sync/ITGlue-VMHost-FeedFlexibleAssetHyperV.ps1
new file mode 100644
index 0000000..f5f8b20
--- /dev/null
+++ b/hyperv-sync/ITGlue-VMHost-FeedFlexibleAssetHyperV.ps1
@@ -0,0 +1,497 @@
+#Requires -Version 3
+#Requires -Modules @{ ModuleName="ITGlueAPI"; ModuleVersion="2.0.7" }
+
+
+[cmdletbinding()]
+param(
+ [Parameter(HelpMessage='The id of the asset in IT Glue')]
+ [long]$flexible_asset_id,
+
+ [Parameter(HelpMessage='IT Glue api key')]
+ [string]$api_key,
+
+ [Parameter(HelpMessage='Where is your data stored? EU or US?')]
+ [ValidateSet('US', 'EU')]
+ [string]$data_center,
+
+ [Parameter(HelpMessage='The first part of your IT Glue URL when logging in')]
+ [string]$subdomain
+)
+
+# Import the IT Glue wrapper module
+Import-Module ITGlueAPI -ErrorAction Stop
+
+# If any parameter is missing ...
+# (Cannot use mandatory because it would break setting parameters inside the script.)
+
+if($api_key) {
+ try {
+ Write-Verbose "Decrypting API key."
+ $api_key = [PSCredential]::new('null', ($api_key | ConvertTo-SecureString -ErrorAction Stop)).GetNetworkCredential().Password
+ Write-Verbose "Decrypted and stored."
+ } catch {
+ Write-Verbose "API key not encrypted."
+ }
+
+ # Set API key for this sessions
+ Write-Verbose "Using specified API key."
+ Add-ITGlueAPIKey -api_key $api_key
+} elseif(!$api_key -and $ITGlue_API_Key) {
+ # Use API key imported from module settings
+ Write-Verbose "Using API key from module settings already saved."
+} else {
+ return "No API key was found or specified, please use -api_key to specify it and run the script again."
+}
+
+if($data_center) {
+ # Set URL for this sessions
+ Write-Verbose "Using specified data center $data_center for this session."
+ Add-ITGlueBaseURI -data_center $data_center
+} elseif(!$data_center -and $ITGlue_Base_URI) {
+ # Use URL imported from module settings
+ Write-Verbose "Using URL from module settings already saved."
+} else {
+ return "No data center was found or specified, please use -data_center to specify it (US or EU) and run the script again."
+}
+
+if(!$flexible_asset_id) {
+ return "flexible_asset_id is missing. Please specify it and run the script again. This script will not continue."
+}
+
+# Flexible asset to update
+Write-Verbose "Retreving IT Glue flexible asset id: $flexible_asset_id..."
+$flexibleAsset = Get-ITGlueFlexibleAssets -id $flexible_asset_id
+Write-Verbose "Done."
+
+# The asset's organization id
+Write-Verbose "Retreving organization id..."
+$organization_id = $flexibleAsset.data.attributes.'organization-id'
+Write-Verbose "Done."
+
+Write-Verbose "Formating URL..."
+$url = (Get-ITGlueBaseURI).replace('https://api', 'https://{0}' -f $subdomain)
+Write-Verbose "Done."
+
+Write-Verbose "Retrieving configurations from IT Glue (org id: $organization_id)..."
+$configurations = @{}
+$MACs = @{}
+$page_number = 1
+do{
+ Write-Verbose "Calling the IT Glue api for configurations (page $page_number, page size 1000)..."
+ $api_call = Get-ITGlueConfigurations -organization_id $organization_id -page_size 1000 -page_number ($page_number++)
+ foreach($_ in $api_call.data) {
+ $configurations[$_.attributes.name] = $_
+ if($_.attributes.'mac-address') {
+ $MACs[$_.attributes.'mac-address'.replace(':','')] = $_
+ }
+ }
+} while($api_call.links.next)
+Write-Verbose "Done."
+
+# All VMs on the host (with some data)
+Write-Verbose "Trying to match VMs against IT Glue configurations and building VM data object..."
+$VMs = @{}
+foreach($vm in Get-VM) {
+ $htmlname = $vm.name
+ $conf_id = -1
+
+ if($configurations[$vm.Name]) {
+ $htmlname = '{3}' -f $url, $configurations[$vm.Name].attributes.'organization-id', $configurations[$vm.Name].id, $vm.name
+ $conf_id = $configurations[$vm.Name].id
+ Write-Verbose "Matched $($vm.Name) on name to $($configurations[$vm.Name].id)."
+ } elseif($MACs[($vm.Name | Get-VMNetworkAdapter).MacAddress]) {
+ $config = $MACs[($vm.Name | Get-VMNetworkAdapter).MacAddress]
+ $conf_id = $config.id
+ $htmlname = '{3}' -f $url, $config.attributes.'organization-id', $config.id, $config.attributes.name
+ Write-Verbose "Matched $($vm.Name) on MAC address to $($config.id)."
+ } else {
+ $configurations.GetEnumerator() | Where {$_.Name -like "*$($vm.name)*"} | ForEach-Object {
+ $htmlname = '{3}' -f $url, $_.value.attributes.'organization-id', $_.value.id, $vm.name
+ $conf_id = $_.value.id
+ Write-Verbose "Matched $($vm.Name) on wildcard to $($_.value.id)."
+ }
+ }
+
+ Write-Verbose "name = $($vm.name), vm = $($vm), conf_id = $($conf_id), htmlname = $($htmlname)"
+
+ $VMs[$vm.name] = [PSCustomObject]@{
+ name = $vm.name
+ vm = $vm
+ htmlname = $htmlname
+ conf_id = $conf_id
+ }
+}
+Write-Verbose "[1/9] VM data object done."
+
+# Hyper-V host's disk information / "Disk information"
+Write-Verbose "Getting host's disk data..."
+$diskDataHTML = '
+
+
+
+ | Disk name |
+ Total(GB) |
+ Used(GB) |
+ Free(GB) |
+
+ {0}
+
+
+
' -f ((Get-PSDrive -PSProvider FileSystem).foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+
' -f $_.Root, [math]::round(($_.free+$_.used)/1GB), [math]::round($_.used/1GB), [math]::round($_.free/1GB)} | Out-String)
+Write-Verbose "[2/9] Host's disk data done."
+
+# Virtual swtiches / "Virtual switches"
+Write-Verbose "Getting virtual swtiches..."
+$virtualSwitchsHTML = '
+
+
+
+ | Name |
+ Switch type |
+ Interface description |
+
+ {0}
+
+
+
' -f ((Get-VMSwitch).foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+
' -f $_.Name, $_.SwitchType, $_.NetAdapterInterfaceDescription} | Out-String)
+Write-Verbose "[3/9] Virtual swtiches done."
+
+# General information about virtual machines / "VM guest names and information"
+Write-Verbose "Getting general guest information..."
+$guestInformationHTML = '
+
+
+
+ | VM guest name |
+ Start action |
+ RAM (GB) |
+ vCPU |
+ Size (GB) |
+
+ {0}
+
+
+
' -f ($VMs.GetEnumerator().foreach{
+ $diskSize = 0
+ ($_.value.vm.HardDrives | Get-VHD).FileSize.foreach{$diskSize += $_}
+ $diskSize = [Math]::Round($diskSize/1GB)
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+ {4} |
+
' -f $_.value.htmlname, $_.value.vm.AutomaticStartAction, [Math]::Round($_.value.vm.MemoryStartup/1GB), $_.value.vm.ProcessorCount, $diskSize} | Out-String)
+Write-Verbose "[4/9] General guest information done."
+
+# Virutal machines' disk file locations / "VM guest virtual disk paths"
+Write-Verbose "Getting VM machine paths..."
+$virtualMachinePathsHTML = '
+
+
+
+ | VM guest name |
+ Path |
+
+ {0}
+
+
+
' -f ($VMs.GetEnumerator().foreach{
+ '
+ | {0} |
+ {1} |
+
' -f $_.value.htmlname, ((Get-VHD -id $_.value.vm.id).path | Out-String).Replace([Environment]::NewLine, '
').TrimEnd('
')} | Out-String)
+Write-Verbose "[5/9] VM machine paths done."
+
+# Snapshot data / "VM guests snapshot information"
+Write-Verbose "Getting snapshot data..."
+$vmSnapshotHTML = '
+
+
+
+ | VMName |
+ Name |
+ Snapshot type |
+ Creation time |
+ Parent snapshot name |
+
+ {0}
+
+
+
' -f ((Get-VMSnapshot -VMName * | Sort VMName, CreationTime).foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+ {4} |
+
' -f $VMs[$_.VMName].htmlname, $_.Name, $_.SnapshotType, $_.CreationTime, $_.ParentSnapshotName} | Out-String)
+Write-Verbose "[6/9] Snapshot data done."
+
+# Virutal machines' bios settings / "VM guests BIOS settings"
+Write-Verbose "Getting VM BIOS settings..."
+# Generation 1
+$vmBiosSettingsTableData = (Get-VMBios * -ErrorAction SilentlyContinue).foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+ Gen 1 |
+
' -f $VMs[$_.VMName].htmlname, ($_.StartupOrder | Out-String).Replace([Environment]::NewLine, ', ').TrimEnd(', '), 'N/A', 'N/A'}
+Write-Verbose "Generation 1 done..."
+
+# Generation 2
+$vmBiosSettingsTableData += (Get-VMFirmware * -ErrorAction SilentlyContinue).foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+ Gen 2 |
+
' -f $VMs[$_.VMName].htmlname, ($_.BootOrder.BootType | Out-String).Replace([Environment]::NewLine, ', ').TrimEnd(', '), $_.PauseAfterBootFailure, $_.SecureBoot}
+Write-Verbose "Generation 2 done..."
+
+$vmBIOSSettingsHTML = '
+
+
+
+ | VM guest name |
+ Startup order |
+ Pause After Boot Failure |
+ Secure Boot |
+ Generation |
+
+ {0}
+
+
+
' -f ($vmBiosSettingsTableData | Out-String)
+Write-Verbose "[7/9] VM BIOS settings done."
+
+# Guest NICs and IPs
+Write-Verbose "Getting VM NICs..."
+$guestNICsIPsHTML = '
+
+
+
+ | VM guest name |
+ Swtich name |
+ IPv4 |
+ IPv6 |
+ MAC address |
+
+ {0}
+
+
+
' -f ((Get-VMNetworkAdapter * | Sort 'VMName').foreach{
+ '
+ | {0} |
+ {1} |
+ {2} |
+ {3} |
+ {4} |
+
' -f $VMs[$_.VMName].htmlname, $_.switchname, $_.ipaddresses[0], $_.ipaddresses[1], $($_.MacAddress -replace '(..(?!$))','$1:') } | Out-String)
+Write-Verbose "[8/9] VM NICs done."
+
+
+Write-Verbose "Building final data structure..."
+$asset_data = @{
+ type = 'flexible-assets'
+ attributes = @{
+ traits = @{
+ # Manual sync
+ 'force-manual-sync-now' = 'No'
+ # Host platform
+ 'virtualization-platform' = 'Hyper-V'
+ # Host CPU data
+ 'cpu' = Get-VMHost | Select -ExpandProperty LogicalProcessorCount
+ # Host RAM data
+ 'ram-gb' = ((Get-CimInstance CIM_PhysicalMemory).capacity | Measure -Sum).Sum/1GB
+ # Host disk data
+ 'disk-information' = $diskDataHTML
+ # Virutal network cards (vNIC)
+ 'virtual-switches' = $virtualSwitchsHTML
+ # Number of VMs on host
+ 'current-number-of-vm-guests-on-this-vm-host' = ($VMs.GetEnumerator() | measure).Count
+ # General VM data (start type, cpu, ram...)
+ 'vm-guest-names-and-information' = $guestInformationHTML
+ # VMs' name and VHD paths
+ 'vm-guest-virtual-disk-paths' = $virtualMachinePathsHTML
+ # Snapshop data
+ 'vm-guests-snapshot-information' = $vmSnapshotHTML
+ # VMs' bios settings
+ 'vm-guests-bios-settings' = $vmBIOSSettingsHTML
+ # NIC and IP assigned to each VM
+ 'assigned-virtual-switches-and-ip-information' = $guestNICsIPsHTML
+ }
+ }
+}
+Write-Verbose "[9/9] Finished building the final structure."
+
+
+Write-Verbose "Comparing data.."
+
+$update = $false
+
+if($flexibleAsset.data.attributes.traits.'force-manual-sync-now' -eq 'Yes') {
+ $update = $true
+} elseif($asset_data.attributes.traits.cpu -ne $flexibleAsset.data.attributes.traits.cpu) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif($asset_data.attributes.traits.'ram-gb' -ne $flexibleAsset.data.attributes.traits.'ram-gb') {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'disk-information'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'disk-information'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'virtual-switches'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'virtual-switches'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif($asset_data.attributes.traits.'current-number-of-vm-guests-on-this-vm-host' -ne $flexibleAsset.data.attributes.traits.'current-number-of-vm-guests-on-this-vm-host') {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'vm-guest-names-and-information'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'vm-guest-names-and-information'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'vm-guest-virtual-disk-paths'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'vm-guest-virtual-disk-paths'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'vm-guests-snapshot-information'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'vm-guests-snapshot-information'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'vm-guests-bios-settings'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'vm-guests-bios-settings'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+} elseif(($asset_data.attributes.traits.'assigned-virtual-switches-and-ip-information'.replace("`n","").replace("`r","")) -ne ($flexibleAsset.data.attributes.traits.'assigned-virtual-switches-and-ip-information'.replace("`n","").replace("`r",""))) {
+ Write-Verbose "Change detected. Will update asset."
+ $update = $true
+}
+
+if($update) {
+ Write-Verbose "Begin updating asset.."
+ $response = @{}
+
+ $asset_data["attributes"]["id"] = $flexible_asset_id
+ Write-Verbose "Added id to hash table."
+ # Visible name
+ $asset_data["attributes"]["traits"]["vm-host-name"] = $flexibleAsset.data.attributes.traits.'vm-host-name'
+ Write-Verbose "Added VM host name to hash table."
+ # Tagged asset (i.e the host)
+ $asset_data["attributes"]["traits"]["vm-host-related-it-glue-configuration"] = $flexibleAsset.data.attributes.traits.'vm-host-related-it-glue-configuration'.Values.id
+ Write-Verbose "Added VM host related IT Glue configuration to hash table."
+
+ Write-Verbose "Uploading data for id $flexible_asset_id."
+ $response['asset'] = Set-ITGlueFlexibleAssets -data $asset_data
+ Write-Verbose "Uploading data: done."
+
+
+ Write-Verbose "Creating new related items (because there it no index/show endpoint to compare data against...)."
+ $new_related_items_hash = @{}
+ $new_related_items = New-Object System.Collections.ArrayList
+ $VMs.GetEnumerator() | Where {$_.value.conf_id -ne '-1'} | Foreach {
+ [void]$new_related_items.Add(
+ @{
+ type= 'related_items'
+ attributes = @{
+ destination_id = $_.value.conf_id
+ destination_type = 'Configuration'
+ }
+ }
+ )
+
+ $new_related_items_hash[$_.value.conf_id] = $_.value.conf_id
+ }
+
+ Write-Verbose "Done."
+
+ if(Test-Path $PSScriptRoot\hyperv-related-items.txt) {
+ Write-Verbose "$("$PSScriptRoot\hyperv-related-items.txt") found, importing last response."
+
+ Write-Verbose "Creating a list of old related items from file..."
+ $old_related_items = Get-Content $PSScriptRoot\hyperv-related-items.txt | ConvertFrom-Json
+ Write-Verbose "Done."
+
+
+ Write-Verbose "Comparing with related items..."
+ $related_items_remove = New-Object System.Collections.ArrayList
+
+ foreach($old_id in $old_related_items) {
+ if(-not $new_related_items_hash["$($old_id.attributes.'destination-id')"]) {
+ Write-Verbose "$($old_id.id) is no longer on the host and will be removed."
+
+ [void]$related_items_remove.Add(
+ @{
+ type = 'related_items'
+ attributes = @{
+ id = $old_id.id
+ }
+ }
+ )
+ }
+ }
+
+ Write-Verbose "Done comparing related items."
+
+ if($related_items_remove) {
+ Write-Verbose "Removing the old related items..."
+
+ $body = @{}
+
+ $body += @{'data'= $related_items_remove}
+
+ $body = ConvertTo-Json -InputObject $body -Depth $ITGlue_JSON_Conversion_Depth
+
+ $resource_uri_related_items_remove = '/{0}/{1}/relationships/related_items' -f 'flexible_assets', $flexible_asset_id
+
+ try {
+ $ITGlue_Headers.Add('x-api-key', (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'N/A', $ITGlue_API_Key).GetNetworkCredential().Password)
+ $response['removed_related_items'] = Invoke-RestMethod -method 'DELETE' -uri ($ITGlue_Base_URI + $resource_uri_related_items_remove) -headers $ITGlue_Headers `
+ -body $body -ErrorAction Stop
+ Write-Verbose "Old realted items removed."
+ } catch {
+ Write-Error $_
+ } finally {
+ [void] ($ITGlue_Headers.Remove('x-api-key')) # Quietly clean up scope so the API key doesn't persist
+ }
+ }
+ }
+
+ Write-Verbose "Begin uploading related items (because there is not endpoint to check current ones)..."
+
+ $body = @{}
+
+ $body += @{'data'= $new_related_items}
+
+ $body = ConvertTo-Json -InputObject $body -Depth $ITGlue_JSON_Conversion_Depth
+
+ $resource_uri_related_items = '/{0}/{1}/relationships/related_items' -f 'flexible_assets', $flexible_asset_id
+
+ try {
+ $ITGlue_Headers.Add('x-api-key', (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'N/A', $ITGlue_API_Key).GetNetworkCredential().Password)
+ $response['related_items'] = Invoke-RestMethod -method 'POST' -uri ($ITGlue_Base_URI + $resource_uri_related_items) -headers $ITGlue_Headers `
+ -body $body -ErrorAction Stop
+ Write-Verbose "New related items updated."
+ } catch {
+ Write-Error $_
+ } finally {
+ [void] ($ITGlue_Headers.Remove('x-api-key')) # Quietly clean up scope so the API key doesn't persist
+ }
+
+ $response['related_items'].data | ConvertTo-Json -Depth 100 | Out-File $PSScriptRoot\hyperv-related-items.txt -Force
+
+ return $response
+
+} else {
+ Write-Verbose "No change detected. Not updating."
+}
\ No newline at end of file
diff --git a/hyperv-sync/ITGlue-VMHost-Setup.ps1 b/hyperv-sync/ITGlue-VMHost-Setup.ps1
new file mode 100644
index 0000000..681c839
--- /dev/null
+++ b/hyperv-sync/ITGlue-VMHost-Setup.ps1
@@ -0,0 +1,27 @@
+try {
+ Import-Module ITGlueAPI -ErrorAction Stop
+ Get-Variable ITGlue_API_Key -ErrorAction Stop > $null
+ Get-Variable ITGlue_Base_URI -ErrorAction Stop > $null
+} catch {
+ $apikey = Read-Host "Enter IT Glue API key"
+ do{
+ $datacenter = Read-Host "Enter IT Glue data center (EU/US)"
+ } until($datacenter -eq 'EU' -or $datacenter -eq 'US')
+
+ Add-ITGlueAPIKey -Api_Key $apikey
+ Add-ITGlueBaseURI -data_center $datacenter
+ Export-ITGlueModuleSettings
+}
+
+$flexible_asset_id = Read-Host "Enter flexible asset id (unique ID for asset to update)"
+$subdomain = Read-Host "Enter the subdomain of your IT Glue domain (without eu if present)"
+
+# Create scheduled task
+# Action
+$action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument ('-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden "{0}\ITGlue-VMHost-FeedFlexibleAssetHyperV.ps1 -flexible_asset_id {1} -subdomain {2}"' -f $PSScriptRoot, $flexible_asset_id, $subdomain)
+# Trigger
+$trigger = New-ScheduledTaskTrigger -Daily -At (Get-Date)
+# Settings
+$settings = New-ScheduledTaskSettingsSet -WakeToRun -RestartCount 3 -RunOnlyIfNetworkAvailable -StartWhenAvailable -RestartInterval (New-TimeSpan -Minutes 3)
+# Add to task scheduler
+Register-ScheduledTask -Action $action -Trigger $trigger -TaskPath "\ITGlueSync\" -TaskName "Sync HyperV with IT Glue" -Settings $settings -Force
\ No newline at end of file
diff --git a/hyperv-sync/README.md b/hyperv-sync/README.md
new file mode 100644
index 0000000..9386b82
--- /dev/null
+++ b/hyperv-sync/README.md
@@ -0,0 +1,18 @@
+1. Install the PowerShell wrapper for ITGlue's API on each Hyper-V server to sync. https://github.com/itglue/powershellwrapper
+2. Run `ITGlue-VMHost-CreateFlexibleAsset.ps1` once to create the flexible asset needed and add it to the side bar ("_VM Host_").
+3. Create a new _VM Host_ asset for each server you want to sync. Take note of individual IDs.
+3a. subdomain.itglue.com/1234568/assets/records/**1153985665234565**
+3b. You only need to give it a name and tag a related a configuration in IT Glue when creating the assets.
+
+From here on, there are multiple options on how to specify the script's paramaters *(i.e. in the file, as paramaters when calling, via module settings)* but these instructions will store API settings via the wrapper module and asset ID in the scheduled task.
+
+4. Note down the subdomain of your IT Glue URL:
+4a. happyfrog.itglue.com translate to `happyfrog`.
+4b. froghappy.eu.itglue.com translates to `froghappy`.
+5. Place `ITGlue-VMHost-Setup.ps1` and `ITGlue-VMHost-FeedFlexibleAssetHyperV.ps1` in a folder to house these script for the duration of their lives *(i.e. somewhere they will not be moved from)*.
+
+**IMPORTANT: Do the next step as the user who will be running the script on the server**
+
+6. Run `ITGlue-VMHost-Setup.ps1`. If the IT Glue wrapper module settings are missing or not found, the script will ask for IT Glue API key and data center (EU/US). These will be saved with `Export-ITGlueModuleSettings`. Next it will ask for **flexible_asset_id** and **subdomain**. These variables will be saved in the scheduled task.
+
+The script will now run once every day at the time `ITGlue-VMHost-Setup.ps1` was run. It will detect changes and only updated if something changes or "Force manual sync now" is set to "Yes".