dev.core.tds.psm1
Import-Module "$PSScriptRoot\dev.core.utils.psm1" -DisableNameChecking; Import-ModuleIfNotExist ADAL.PS -Global; Import-ModuleIfNotExist CredentialManager -Global; $AdalToken = $null; $TdsAppFolder = "$Home\AppData\Local\DevPowerShell\dev.core.tds"; $TdsTempFolder = "$env:TEMP\DevPowerShell\dev.core.tds" $TdsInstance = [PSCustomObject]@{ Environment = "Classic"; Stack = "B"; BaseUrl = "https://tdswebuistackb.azurewebsites.net"; } # The RDP file template. $RdpTemplate = "screen mode id:i:2 use multimon:i:0 desktopwidth:i:1440 desktopheight:i:900 session bpp:i:32 winposstr:s:0,1,451,102,1897,885 compression:i:1 keyboardhook:i:2 audiocapturemode:i:0 videoplaybackmode:i:1 connection type:i:2 displayconnectionbar:i:1 disable wallpaper:i:1 allow font smoothing:i:0 allow desktop composition:i:0 disable full window drag:i:1 disable menu anims:i:1 disable themes:i:0 disable cursor setting:i:0 bitmapcachepersistenable:i:1 audiomode:i:0 redirectprinters:i:1 redirectcomports:i:0 redirectsmartcards:i:1 redirectclipboard:i:1 redirectposdevices:i:0 redirectdirectx:i:1 autoreconnection enabled:i:1 authentication level:i:2 prompt for credentials:i:0 negotiate security layer:i:1 remoteapplicationmode:i:0 alternate shell:s: shell working directory:s: gatewayusagemethod:i: gatewaycredentialssource:i: gatewayprofileusagemethod:i: promptcredentialonce:i:0 use redirection server name:i:0 administrative session:i:1 drivestoredirect:s:* full address:s:__ADDRESS__ username:s:.\administrator gatewayhostname:s:"; # # Private Functions # function Logon-Tds { if ($script:AdalToken -eq $null -or $script:AdalToken.ExpiresOn.DateTime -le (get-date).AddMinutes(-15)) { Write-Verbose "Requesting authentication token"; # Get oauth token for TDS API $authority = "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"; $audience = "b51e4b8d-44f8-4beb-b426-d57ed395cb57"; $clientId= "2c883697-cfd4-46a5-9962-cdc74ba55125"; $redirectUri = "urn:ietf:wg:oauth:2.0:oob"; $script:AdalToken = Get-AdalToken -Resource $audience -ClientId $clientId -RedirectUri $redirectUri -Authority $authority -PromptBehavior Auto; Write-Verbose "Authentication token is acquired successfully."; } } function Get-TdsUser { Logon-Tds; return $script:AdalToken.UserInfo.DisplayableId; } function Invoke-TdsMethod { [CmdletBinding()] param( [string]$PathAndQuery, [Microsoft.PowerShell.Commands.WebRequestMethod]$Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Get, [object]$Body = $null); Logon-Tds; $uri = "$(${script:TdsInstance}.BaseUrl)/$PathAndQuery"; $headers = @{ "Authorization" = "Bearer $($AdalToken.AccessToken)" }; if ($Body) { Write-Verbose "body: $Body"; Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body $Body -ContentType "application/json"; } else { Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers; } } function Get-ToplogyTypePayload { param([PSCustomObject]$TopologyType); $environment = [PSCustomObject]@{ "name" = "stack$($Script:TdsInstance.Stack.ToLower())"; "display" = "CORP"; "topoName" = $Script:TdsInstance.Environment; "clientId" = "b51e4b8d-44f8-4beb-b426-d57ed395cb57"; "baseUrl" = $Script:TdsInstance.BaseUrl; "toolTip" = ""; }; $topologyTypeCopy = $TopologyType | ConvertTo-Json | ConvertFrom-Json; $topologyTypeCopy | Add-Member -MemberType NoteProperty -Name "environment" -Value $environment | Out-Null; return $topologyTypeCopy; } function Connect-TdsMachineInternal { # param([string]$Name, [string]$IPAddress, [string]$Port, [string]$Password); [CmdletBinding()] param([PSTypeName("TDS.Machine")]$Machine); $Address = $Machine.IPAddress; $Port = $Machine.RdpPort; $Name = $Machine.Name; $Credential = $Machine.Credential; if ($Port -and $Port -ne "UNKNOWN") { $Address = "${Address}:${Port}"; } $RdpContent = $script:RdpTemplate.Replace("__ADDRESS__", $Address); $RdpFilePath = "${TdsTempFolder}\${Name}.rdp"; Write-Verbose "Writing temporary RDP file ${RdpFilePath}"; Set-Content -Path $RdpFilePath -Value $RdpContent; # Store the credential in local machine credential manager New-StoredCredential -Target $Machine.IPAddress -UserName $Credential.UserName -SecurePassword $Credential.Password -Type Generic -Persist LocalMachine | Out-Null; Invoke-Item -Path $RdpFilePath; } function Clean-TdsMachine { [CmdletBinding()] param([PSTypeName("TDS.Machine")]$Machine); # Remove TDS rdp file. $Name = $Machine.Name; $RdpFilePath = "${TdsTempFolder}\${Name}.rdp"; if (Test-Path $RdpFilePath) { Remove-Item -Path $RdpFilePath; } # Clear stored credential. Remove-StoredCredential -Target $Machine.IPAddres -ErrorAction SilentlyContinue; } function Update-TdsMachineDnsEntryInternal { param([string]$Name, [string]$IPAddress); $hostsFile = "${env:SystemRoot}\system32\drivers\etc\hosts"; $lines = Get-Content -Path $hostsFile; $machineDnsLine = "${IPAddress}`t${Name}"; $done = $false; for ($i = 0; $i -lt $lines.Length; $i++) { $line = $lines[$i]; if ($line.Trim().StartsWith("#")) { continue; } if ($line.Contains($Name)) { $lines[$i] = $machineDnsLine; $done = $true; } } if (!$done) { $lines += $machineDnsLine; } Set-Content -Path $hostsFile -Value $lines; } function ConvertTo-TdsMachine { param([Parameter(ValueFromPipeline=$true)][PSCustomObject]$Machine); $DomainName = "$($Machine.Name)DOM.EXTEST.MICROSOFT.COM"; $UserName = "$DomainName\Administrator"; $SecurePassword = ConvertTo-SecureString -String $Machine.VmAdmin -AsPlainText -Force; $Credential = New-Object System.Management.Automation.PSCredential($UserName, $SecurePassword); return [PSCustomObject]@{ PSTypeName = "TDS.Machine"; Name = $Machine.Name; BuildNumber = $Machine.BuildNumber; Fqdn = $Machine.Fqdn; IPAddress = $Machine.IPAddress; OperatingSystem = $Machine.OperatingSystem; RdpPort = $Machine.RdpPort; WrmPort = $Machine.WrmPort; Credential = $Credential; }; } function ConvertTo-TdsTopology { param([Parameter(ValueFromPipeline=$true)][PSCustomObject]$Topology); return [PSCustomObject]@{ PSTypeName = "TDS.Topology"; TopologyId = $Topology.TopologyId; WorkgroupId = $Topology.WorkgroupId; Name = $Topology.Name; Owner = $Topology.Owner; TopologyState = $Topology.State; TopologyType = $Topology.TopologyType; Machines = ($Topology.MachinesExt | ConvertTo-TdsMachine); CreatedTime = [datetime]($Topology.CreatedTime); RecoverTime = [datetime]($Topology.RecoverTime); WarningTime = [datetime]($Topology.WarningTime); }; } $TdsMachinePair = $null; function New-TdsMachinePairInternal { param([PSCustomObject]$Machine); $dnsEntries = @( (New-LocalDnsEntry -HostName $Machine.Name -IPAddress $Machine.IPAddress) ); $dnsEntries | Set-LocalDnsEntry; Clear-DnsClientCache; $session = $Machine | New-TdsMachinePsSession; Invoke-Command -Session $session -ScriptBlock { Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 } # TODO: Download the sti certificate from TDS machine and install to LOCAL_MACHINE\MY $script:TdsMachinePair = [PSCustomObject]@{ PSTypeName = "TDS.MachinePair"; Machine = $Machine; Session = $session; Data = @{}; PairedTime = (Get-Date); }; return $script:TdsMachinePair; } function Ensure-TdsMachinePair { if (!$script:TdsMachinePair) { throw "TDS machine is not paired"; } } function Invoke-PairedCommand { param( [scriptblock]$ScriptBlock, [psobject]$InputObject, [object[]]$ArgumentList); Ensure-TdsMachinePair; Invoke-Command -Session $script:TdsMachinePair.Session -ScriptBlock $ScriptBlock -InputObject $InputObject -ArgumentList $ArgumentList; } # # Public Functions # function Set-TdsInstance { [CmdletBinding()] param( [ValidateSet("Classic", "Cloud")][string]$Environment = "Classic", [ValidateSet("A", "B")][string]$Stack = "B" ); $s = $Stack.ToLower(); $BaseUrl = switch ($Environment) { "Classic" { "https://tdswebuistack${s}.azurewebsites.net" } "Cloud" { "https://uw2tdsprdapi-for-stack${s}.azurewebsites.net" } } $script:TdsInstance = [PSCustomObject]@{ Environment = $Environment; Stack = $Stack; BaseUrl = $BaseUrl; } } function Get-TdsInstance { [CmdletBinding()] param(); return $script:TdsInstance; } function Ping-Tds { Invoke-TdsMethod -PathAndQuery "api/servicestatus"; } function Get-TdsTopologyType { [CmdletBinding(DefaultParameterSetName="GetAll")] param( [Parameter(ParameterSetName="ByName")][string]$Name, [Parameter(ParameterSetName="ById")][int]$Id, [switch]$Refresh); # Try to load topology types from local cache file first. $TopologyTypesJsonFilePath = "${script:TdsAppFolder}\topologytypes.json"; $CacheFileAvailable = Test-Path $TopologyTypesJsonFilePath; if ($CacheFileAvailable) { # The topology types local cache file expires in 24 hours. $FileItem = Get-Item -Path $TopologyTypesJsonFilePath; if ($FileItem.LastWriteTime.AddDays(1) -lt (Get-Date)) { Write-Verbose "Typology topology types local cache file ${TopologyTypesJsonFilePath} has expired."; $CacheFileAvailable = $false; } } else { Write-Verbose "Typology topology types local cache file ${TopologyTypesJsonFilePath} not found."; } if ($Refresh -or !$CacheFileAvailable) { $topologyTypes = Invoke-TdsMethod -PathAndQuery "api/topologytype/enumerate"; Write-Verbose "Updating topology types local cache file ${TopologyTypesJsonFilePath}..."; $topologyTypes | ConvertTo-Json | Out-File -FilePath $TopologyTypesJsonFilePath; } if (!$topologyTypes) { $topologyTypes = Get-Content -Path $TopologyTypesJsonFilePath -Raw | ConvertFrom-Json; } if ($PSCmdlet.ParameterSetName -eq "ByName") { $topologyTypes = $topologyTypes | ? { $_.DisplayName -like $Name }; } elseif ($PSCmdlet.ParameterSetName -eq "ById") { $topologyTypes = $topologyTypes | ? { $_.Id -eq $Id }; } foreach ($topologyType in $topologyTypes) { Write-Output $topologyType; } } function Get-TdsTopologyTypeBuild { [CmdletBinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyType")][PSCustomObject]$TopologyType, [Parameter(Mandatory=$true, ParameterSetName="ByTopologyTypeId")][int]$TopologyTypeId, [Parameter(Mandatory=$true, ParameterSetName="ByTopologyTypeName")][string]$TopologyTypeName ); if ($PSCmdlet.ParameterSetName -eq "ByTopologyTypeId") { $TopologyType = Get-TdsTopologyType | ? { $_.Id -eq $TopologyTypeId }; if (!$TopologyType) { throw "Topology type with id ${TopologyTypeId} not found"; } } elseif ($PSCmdlet.ParameterSetName -eq "ByTopologyTypeName") { $TopologyType = Get-TdsTopologyType | ? { $_.DisplayName -eq $TopologyTypeName }; if (!$TopologyType) { throw "Topology type with name ${TopologyTypeName} not found"; } } $payload = Get-ToplogyTypePayload -TopologyType $TopologyType; $builds = Invoke-TdsMethod -Method Post -PathAndQuery "api/buildnumber/bytopologytype" -Body (ConvertTo-Json $payload); $builds | % { Write-Output $_ }; } function New-TdsTopology { [CmdletBinding(DefaultParameterSetName="ByTopologyTypeId")] param( [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByTopologyTypeId")][int]$TopologyTypeId, [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyType")][PSCustomObject]$TopologyType, [Parameter(Mandatory=$true, ParameterSetName="ByTopologyTypeName")][string]$TopologyTypeName, [string]$Build ); if ($PSCmdlet.ParameterSetName -eq "ByTopologyTypeId") { $TopologyType = Get-TdsTopologyType | ? { $_.Id -eq $TopologyTypeId }; if (!$TopologyType) { throw "Topology type with id ${TopologyTypeId} not found"; } } elseif ($PSCmdlet.ParameterSetName -eq "ByTopologyTypeName") { $TopologyType = Get-TdsTopologyType | ? { $_.DisplayName -eq $TopologyTypeName }; if (!$TopologyType) { throw "Topology type with name ${TopologyTypeName} not found"; } } if (!$Build) { $builds = Get-TdsTopologyTypeBuild -TopologyType $TopologyType; if (!$builds) { throw "No build available for topology type $($TopologyType.DisplayName)."; } $Build = $builds[0]; # The first build is the latest build. Write-Verbose "Using latest build ${Build} for topology type $($TopologyType.DisplayName)."; } $payload = [PSCustomObject]@{ "TopologyType" = (Get-ToplogyTypePayload -TopologyType $TopologyType); "UserData" = $null; }; $topologyOwner = Get-TdsUser; $path = "api/topology/checkoutWithUserData"; $query = "topologyName=$($TopologyType.DisplayName) ${Build}&topologyOwner=${topologyOwner}&buildNumber=${Build}&source=v2website"; Invoke-TdsMethod -Method Post -PathAndQuery "${Path}?${Query}" -Body (ConvertTo-Json $payload) | Out-Null; # The REST API doesn't return response, so get it back by finding the topology with biggest id. Get-TdsTopology | Sort-Object -Property TopologyId -Descending | Select-Object -First 1; } function Get-TdsTopology { [CmdletBinding()] param([string]$TopologyId); $topologies = Invoke-TdsMethod -PathAndQuery "api/jobs/byownerwithpasswords?owner=$(Get-TdsUser)"; foreach ($topology in $topologies) { if (!$PSBoundParameters.ContainsKey("TopologyId") -or $TopologyId -eq $topology.TopologyId) { Write-Output (ConvertTo-TdsTopology -Topology $topology); } } } function Remove-TdsTopology { [CmdletBinding()] param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline)][PSTypeName('TDS.Topology')]$Topology, [bool]$Confirm = $true ); begin { $removeAll = !$Confirm; } process { $WorkgroupId = $Topology.WorkgroupId; $shouldRemove = $removeAll; if (!$shouldRemove) { # Display confirmation prompt $caption = "Remove Topology"; $message = "Are you sure to remove TDS topology ${WorkgroupId}:" [int]$defaultChoice = 1; # No $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes"; $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No"; $all = New-Object System.Management.Automation.Host.ChoiceDescription "&All"; $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no, $all); $choice = $host.UI.PromptForChoice($caption, $message, $options, $defaultChoice); if ($choice -eq 0) # Yes { $shouldRemove = $true; } elseif ($choice -eq 2) # All { $removeAll = $true; $shouldRemove = $true; } } if ($shouldRemove) { Write-Verbose "Deleting topology with workgroup id ${WorkgroupId}"; Invoke-TdsMethod -PathAndQuery "api/topology/checkin?topologyId=${WorkgroupId}"; } } } function Get-TdsMachine { [CmdletBinding()] param([string]$Name); $topologies = Get-TdsTopology; foreach ($topology in $topologies) { foreach ($machine in $topology.Machines) { if (!$Name -or $Name -eq $machine.Name) { Write-Verbose "Found TDS machine $($machine.Name)"; Write-Output $machine; } } } } function Resolve-TdsMachine { [CmdletBinding()] param( [PSCustomObject]$TopologyOrMachine, [string]$Name, [Hashtable]$State ); if ($TopologyOrMachine) { # The input object could be a TDS topology or a TDS machine if ($TopologyOrMachine.PSTypeNames -contains "TDS.Topology") { foreach ($machine in $TopologyOrMachine.Machines) { Write-Output $machine; } } elseif ($TopologyOrMachine.PSTypeNames -contains "TDS.Machine") { Write-Output $TopologyOrMachine; } elseif ($TopologyOrMachine.PSTypeNames -contains "TDS.MachinePair") { Write-Output $TopologyOrMachine.Machine; } } else { $machines = $null; # Try to get machine list from external state first. if ($State) { if (!$State.Machines) { $State.Machines = Get-TdsMachine; } $machines = $State.Machines; } if (!$machines) { $machines = Get-TdsMachine; } $machine = $machines | ? { $_.Name -eq $Name }; if (!$machine) { throw "TDS machine ${Name} not found."; } Write-Output $machine; } } function Connect-TdsMachine { [CmdletBinding(DefaultParameterSetName="ByName")] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyOrMachine")][PSCustomObject]$TopologyOrMachine, [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByName")][string]$Name ); begin { $State = @{}; } process { $machines = Resolve-TdsMachine -TopologyOrMachine $TopologyOrMachine -Name $Name -State $State; foreach ($machine in $machines) { Write-Verbose "Connecting to machine $($machine.Name)"; Connect-TdsMachineInternal -Machine $machine; } } } function New-TdsMachinePsSession { [CmdletBinding(DefaultParameterSetName="ByName")] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyOrMachine")][PSCustomObject]$TopologyOrMachine, [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByName")][string]$Name ); begin { $State = @{}; } process { $machines = Resolve-TdsMachine -TopologyOrMachine $TopologyOrMachine -Name $Name -State $State; foreach ($machine in $machines) { Write-Verbose "Creating PSSession to machine $($machine.Name)"; New-PSSession -ComputerName $machine.Name -Credential $machine.Credential; } } } function Set-TdsMachineDnsEntry { [CmdletBinding(DefaultParameterSetName="ByName")] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyOrMachine")][PSCustomObject]$TopologyOrMachine, [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByName")][string]$Name ); begin { $State = @{}; } process { $machines = Resolve-TdsMachine -TopologyOrMachine $TopologyOrMachine -Name $Name -State $State; foreach ($machine in $machines) { Write-Verbose "Updating $($machine.Name)=$($machine.IPAddress) DNS entry"; Set-LocalDnsEntry -HostName $machine.Name -IPAddress $machine.IPAddress; } } } # # Public Functions # function Test-TdsMachinePair { [CmdletBinding()] param(); return $script:TdsMachinePair -ne $null; } function Get-TdsMachinePair { [CmdletBinding()] param(); if ($script:TdsMachinePair) { return $script:TdsMachinePair; } } function Set-TdsMachinePair { [CmdletBinding(DefaultParameterSetName="ByName")] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="ByTopologyOrMachine")][PSCustomObject]$TopologyOrMachine, [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByName")][string]$Name ); begin { $State = @{}; $First = $true; Reset-TdsMachinePair; } process { $machines = Resolve-TdsMachine -TopologyOrMachine $TopologyOrMachine -Name $Name -State $State; foreach ($machine in $machines) { if (!$First) { Write-Warning "Skipping pairing with $($machine.Name), only one machine is allowed to pair."; } Write-Verbose "Connecting to machine $($machine.Name)"; New-TdsMachinePairInternal -Machine $machine; $First = $false; } } } function Reset-TdsMachinePair { [CmdletBinding()] param(); if ($script:TdsMachinePair) { Write-Verbose "Removing paired PSSession..."; Remove-PSSession -Session $script:TdsMachinePair.Session -ErrorAction SilentlyContinue; } $script:TdsMachinePair = $null; } # Test if the current machine is TDS machine function Test-IsTdsMachine { [CmdletBinding()] param(); return $env:USERNAME -eq "Administrator" -and $env:USERDNSDOMAIN.EndsWith(".EXTEST.MICROSOFT.COM"); } # # Module Initialization # New-DirectoryIfNotExist -Path $TdsAppFolder | Out-Null; New-DirectoryIfNotExist -Path $TdsTempFolder | Out-Null; $OnRemoveScript = { Write-Warning "Removing TDS machine pair..."; Reset-TdsMachinePair; } $ExecutionContext.SessionState.Module.OnRemove += $OnRemoveScript; Export-ModuleMember -Function Get-TdsInstance; Export-ModuleMember -Function Set-TdsInstance; Export-ModuleMember -Function Ping-Tds; Export-ModuleMember -Function Get-TdsTopologyType; Export-ModuleMember -Function Get-TdsTopologyTypeBuild; Export-ModuleMember -Function New-TdsTopology; Export-ModuleMember -Function Get-TdsTopology; Export-ModuleMember -Function Remove-TdsTopology; Export-ModuleMember -Function Get-TdsMachine; Export-ModuleMember -Function Connect-TdsMachine; Export-ModuleMember -Function Resolve-TdsMachine; Export-ModuleMember -Function New-TdsMachinePsSession; Export-ModuleMember -Function Set-TdsMachineDnsEntry; Export-ModuleMember -Function Get-TdsMachinePair; Export-ModuleMember -Function Set-TdsMachinePair; Export-ModuleMember -Function Reset-TdsMachinePair; Export-ModuleMember -Function Test-TdsMachinePair; Export-ModuleMember -Function Test-IsTdsMachine; |