Add Ubuntu SSH remoting tests CI (#12033)

* Add SSH remoting CI

* Fix typo

* Add install git to Ubuntu CI

* Update .vsts-ci/sshremoting-tests.yml

Co-Authored-By: Aditya Patwardhan <adityap@microsoft.com>

* Fix install git 1

* Add missing tools module import

* Change ubuntu service restart

* Update ssh install

* fix module path

* fix module path

* change module import

* Add tracing

* Add service start retry

* Fix service restart

* Fix options restore

* Fix Restore-PSOptions path

* Fix Pester test output

* fix typo

* Fix test output path

* Debug 1

* Debug 2

* Debug 3

* Change results path

* Fix result publish to use build artifacts directory

* Add more New-PSSession tests

* Remove User test

* Remove env:USER

* Add API tests

* Fix type for Subsytem API test

* Update .vsts-ci/sshremoting-tests.yml

Co-Authored-By: Travis Plunk <travis.plunk@microsoft.com>

* Update .vsts-ci/sshremoting-tests.yml

Co-Authored-By: Travis Plunk <travis.plunk@microsoft.com>

* Update .vsts-ci/sshremoting-tests.yml

Co-Authored-By: Travis Plunk <travis.plunk@microsoft.com>

* Apply suggestions from code review

Co-authored-by: Aditya Patwardhan <adityap@microsoft.com>
Co-authored-by: Travis Plunk <travis.plunk@microsoft.com>
This commit is contained in:
Paul Higinbotham 2020-03-11 14:45:14 -07:00 committed by GitHub
parent 23b7dce320
commit 5f46605a21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 891 additions and 4 deletions

View file

@ -0,0 +1,86 @@
name: PR-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr)
trigger:
# Batch merge builds together while a merge build is running
batch: true
branches:
include:
- master
- release*
- feature*
paths:
include:
- '/src/System.Management.Automation/engine/*'
- '/test/SSHRemoting/*'
pr:
branches:
include:
- master
- release*
- feature*
paths:
include:
- '/src/System.Management.Automation/engine/*'
- '/test/SSHRemoting/*'
variables:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
POWERSHELL_TELEMETRY_OPTOUT: 1
# Avoid expensive initialization of dotnet cli, see: https://donovanbrown.com/post/Stop-wasting-time-during-NET-Core-builds
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
__SuppressAnsiEscapeSequences: 1
resources:
- repo: self
clean: true
jobs:
- job: SSHRemotingTests
container: mcr.microsoft.com/powershell/test-deps:ubuntu-18.04
displayName: SSH Remoting Tests
steps:
- pwsh: |
Get-ChildItem -Path env:
displayName: Capture Environment
condition: succeededOrFailed()
- pwsh: Write-Host "##vso[build.updatebuildnumber]$env:BUILD_SOURCEBRANCHNAME-$env:BUILD_SOURCEVERSION-$((get-date).ToString("yyyyMMddhhmmss"))"
displayName: Set Build Name for Non-PR
condition: ne(variables['Build.Reason'], 'PullRequest')
- template: /tools/releaseBuild/azureDevOps/templates/insert-nuget-config-azfeed.yml
- pwsh: |
sudo apt-get update
sudo apt-get install -y git
displayName: Install Github
condition: succeeded()
- pwsh: |
Import-Module .\tools\ci.psm1
Invoke-CIInstall -SkipUser
displayName: Bootstrap
condition: succeededOrFailed()
- pwsh: |
Import-Module .\tools\ci.psm1
Invoke-CIBuild
displayName: Build
condition: succeeded()
- pwsh: |
Import-Module .\tools\ci.psm1
Restore-PSOptions
$options = (Get-PSOptions)
Import-Module .\test\tools\Modules\HelpersRemoting
Install-SSHRemoting -PowerShellFilePath $options.Output
displayName: Install SSH Remoting
condition: succeeded()
- pwsh: |
Import-Module .\tools\ci.psm1
Restore-PSOptions
$options = (Get-PSOptions)
Import-Module .\build.psm1
Start-PSPester -Path test/SSHRemoting -powershell $options.Output -OutputFile "$PWD/sshTestResults.xml"
displayName: Test
condition: succeeded()

View file

@ -1364,7 +1364,12 @@ function Publish-TestResults
if($env:TF_BUILD)
{
$fileName = Split-Path -Leaf -Path $Path
$tempFilePath = Join-Path ([system.io.path]::GetTempPath()) -ChildPath $fileName
$tempPath = $env:BUILD_ARTIFACTSTAGINGDIRECTORY
if (! $tempPath)
{
$tempPath = [system.io.path]::GetTempPath()
}
$tempFilePath = Join-Path -Path $tempPath -ChildPath $fileName
# NUnit allowed values are: Passed, Failed, Inconclusive or Ignored (the spec says Skipped but it doesn' work with Azure DevOps)
# https://github.com/nunit/docs/wiki/Test-Result-XML-Format

View file

@ -0,0 +1,187 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Describe "SSHRemoting Basic Tests" -tags CI {
# SSH remoting is set up to automatically authenticate current user via SSH keys
# All tests connect back to localhost machine
function VerifySession {
param (
[System.Management.Automation.Runspaces.PSSession] $session
)
$session.State | Should -BeExactly 'Opened'
$session.ComputerName | Should -BeExactly 'localhost'
$session.Transport | Should -BeExactly 'SSH'
Invoke-Command -Session $session -ScriptBlock { whoami } | Should -BeExactly $(whoami)
$psRemoteVersion = Invoke-Command -Session $session -ScriptBlock { $PSSenderInfo.ApplicationArguments.PSVersionTable.PSVersion }
$psRemoteVersion.Major | Should -BeExactly $PSVersionTable.PSVersion.Major
$psRemoteVersion.Minor | Should -BeExactly $PSVersionTable.PSVersion.Minor
}
Context "New-PSSession Tests" {
AfterEach {
if ($script:session -ne $null) { Remove-PSSession -session $script:session }
if ($script:sessions -ne $null) { Remove-PSSession -session $script:sessions }
}
It "Verifies new connection with implicit current User" {
$script:session = New-PSSession -HostName localhost -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
}
It "Verifies new connection with explicit User parameter" {
$script:session = New-PSSession -HostName localhost -UserName (whoami) -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
}
It "Verifies explicit Name parameter" {
$sessionName = 'TestSessionNameA'
$script:session = New-PSSession -HostName localhost -Name $sessionName -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
$script:session.Name | Should -BeExactly $sessionName
}
It "Verifies explicit Port parameter" {
$portNum = 22
$script:session = New-PSSession -HostName localhost -Port $portNum -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
}
It "Verifies explicit Subsystem parameter" {
$portNum = 22
$subSystem = 'powershell'
$script:session = New-PSSession -HostName localhost -Port $portNum -SubSystem $subSystem -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
}
It "Verifies explicit KeyFilePath parameter" {
$keyFilePath = "$HOME/.ssh/id_rsa"
$portNum = 22
$subSystem = 'powershell'
$script:session = New-PSSession -HostName localhost -Port $portNum -SubSystem $subSystem -KeyFilePath $keyFilePath -ErrorVariable err
$err | Should -HaveCount 0
VerifySession $script:session
}
It "Verifies SSHConnection hash table parameters" {
$sshConnection = @(
@{
HostName = 'localhost'
UserName = whoami
Port = 22
KeyFilePath = "$HOME/.ssh/id_rsa"
Subsystem = 'powershell'
},
@{
HostName = 'localhost'
KeyFilePath = "$HOME/.ssh/id_rsa"
Subsystem = 'powershell'
})
$script:sessions = New-PSSession -SSHConnection $sshConnection -Name 'Connection1','Connection2' -ErrorVariable err
$err | Should -HaveCount 0
$script:sessions | Should -HaveCount 2
$script:sessions[0].Name | Should -BeLike 'Connection*'
$script:sessions[1].Name | Should -BeLike 'Connection*'
VerifySession $script:sessions[0]
VerifySession $script:sessions[1]
}
}
function VerifyRunspace {
param (
[runspace] $rs
)
$rs.RunspaceStateInfo.State | Should -BeExactly 'Opened'
$rs.RunspaceAvailability | Should -BeExactly 'Available'
$rs.RunspaceIsRemote | Should -BeTrue
$ps = [powershell]::Create()
try
{
$ps.Runspace = $rs
$psRemoteVersion = $ps.AddScript('$PSSenderInfo.ApplicationArguments.PSVersionTable.PSVersion').Invoke()
$psRemoteVersion.Major | Should -BeExactly $PSVersionTable.PSVersion.Major
$psRemoteVersion.Minor | Should -BeExactly $PSVersionTable.PSVersion.Minor
$ps.Commands.Clear()
$ps.AddScript('whoami').Invoke() | Should -BeExactly $(whoami)
}
finally
{
$ps.Dispose()
}
}
Context "SSH Remoting API Tests" {
AfterEach {
if ($script:rs -ne $null) { $script:rs.Dispose() }
}
$testCases = @(
@{
testName = 'Verifies connection with implicit user'
UserName = $null
ComputerName = 'localhost'
KeyFilePath = $null
Port = 0
Subsystem = $null
},
@{
testName = 'Verifies connection with UserName'
UserName = whoami
ComputerName = 'localhost'
KeyFilePath = $null
Port = 0
Subsystem = $null
},
@{
testName = 'Verifies connection with KeyFilePath'
UserName = whoami
ComputerName = 'localhost'
KeyFilePath = "$HOME/.ssh/id_rsa"
Port = 0
Subsystem = $null
},
@{
testName = 'Verifies connection with Port specified'
UserName = whoami
ComputerName = 'localhost'
KeyFilePath = "$HOME/.ssh/id_rsa"
Port = 22
Subsystem = $null
},
@{
testName = 'Verifies connection with Subsystem specified'
UserName = whoami
ComputerName = 'localhost'
KeyFilePath = "$HOME/.ssh/id_rsa"
Port = 22
Subsystem = 'powershell'
}
)
It "<testName>" -TestCases $testCases {
param (
$UserName,
$ComputerName,
$KeyFilePath,
$Port,
$SubSystem
)
$ci = [System.Management.Automation.Runspaces.SSHConnectionInfo]::new($UserName, $ComputerName, $KeyFilePath, $Port, $Subsystem)
$script:rs = [runspacefactory]::CreateRunspace($host, $ci)
$script:rs.Open()
VerifyRunspace $script:rs
}
}
}

View file

@ -466,6 +466,26 @@ function Install-SSHRemotingOnWindows
}
}
function WriteVerboseSSHDStatus
{
param (
[string] $Msg = 'SSHD service status'
)
$sshdStatus = sudo service ssh status
Write-Verbose -Verbose "${Msg}: $sshdStatus"
}
function DumpTextFile
{
param (
[string] $FilePath = '/etc/ssh/sshd_config'
)
$content = Get-Content -Path $FilePath -Raw
Write-Verbose -Verbose $content
}
function Install-SSHRemotingOnLinux
{
param (
@ -478,7 +498,11 @@ function Install-SSHRemotingOnLinux
{
Write-Verbose -Verbose "Installing openssh-server ..."
sudo apt-get install --yes openssh-server
sudo systemctl restart ssh
Write-Verbose -Verbose "Restarting sshd service after install ..."
WriteVerboseSSHDStatus "SSHD service status before restart"
sudo service ssh restart
WriteVerboseSSHDStatus "SSHD service status after restart"
}
if (! (Test-Path -Path /etc/ssh/sshd_config))
{
@ -519,19 +543,40 @@ function Install-SSHRemotingOnLinux
Write-Verbose -Verbose "Updating known_hosts ..."
ssh-keyscan -H localhost | Set-Content -Path "$HOME/.ssh/known_hosts" -Force
<#
# Install Microsoft.PowerShell.RemotingTools module.
if ($null -eq (Get-Module -Name Microsoft.PowerShell.RemotingTools -ListAvailable))
{
Write-Verbose -Verbose "Installing Microsoft.PowerShell.RemotingTools ..."
Install-Module -Name Microsoft.PowerShell.RemotingTools -Force -SkipPublisherCheck
}
#>
# Add PowerShell endpoint to SSHD.
Write-Verbose -Verbose "Running Enable-SSHRemoting ..."
sudo pwsh -c 'Enable-SSHRemoting -SSHDConfigFilePath /etc/ssh/sshd_config -PowerShellFilePath $PowerShellPath -Force'
Write-Verbose -Verbose "PSScriptRoot: $PSScriptRoot"
$modulePath = "${PSScriptRoot}\..\Microsoft.PowerShell.RemotingTools\Microsoft.PowerShell.RemotingTools.psd1"
$cmdLine = "Import-Module ${modulePath}; Enable-SSHRemoting -SSHDConfigFilePath /etc/ssh/sshd_config -PowerShellFilePath $PowerShellPath -Force"
Write-Verbose -Verbose "CmdLine: $cmdLine"
sudo pwsh -c $cmdLine
# Restart SSHD service for changes to take effect.
Start-Sleep -Seconds 1
WriteVerboseSSHDStatus "SSHD service status before restart"
Write-Verbose -Verbose "Restarting sshd ..."
sudo systemctl restart ssh
sudo service ssh restart
WriteVerboseSSHDStatus "SSHD service status after restart"
# Try starting again if needed.
$status = sudo service ssh status
$result = $status | Where-Object { ($_ -like '*not running*') -or ($_ -like '*stopped*') }
if ($null -ne $result)
{
Start-Sleep -Seconds 1
Write-Verbose -Verbose "Starting sshd again ..."
sudo service ssh start
WriteVerboseSSHDStatus "SSHD service status after second start attempt"
}
# Test SSH remoting.
Write-Verbose -Verbose "Testing SSH remote connection ..."
@ -542,6 +587,10 @@ function Install-SSHRemotingOnLinux
{
throw "Could not successfully create SSH remoting connection."
}
else
{
Write-Verbose -Verbose "SUCCESS: SSH remote connection"
}
}
finally
{
@ -569,6 +618,8 @@ function Install-SSHRemoting
[string] $PowerShellFilePath
)
Write-Verbose -Verbose "Install-SSHRemoting called with PowerShell file path: $PowerShellFilePath"
if ($IsWindows)
{
if ([string]::IsNullOrEmpty($PowerShellFilePath)) { $PowerShellFilePath = "$PSHOME/pwsh.exe" }

View file

@ -0,0 +1,50 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
@{
RootModule = './Microsoft.PowerShell.RemotingTools.psm1'
ModuleVersion = '0.1.0'
GUID = 'e11d52a1-d5a0-4e4d-92cd-e87114bf4a5c'
Author = 'Microsoft Corporation'
CompanyName = 'Microsoft Corporation'
Copyright = '(c) Microsoft Corporation. All rights reserved.'
Description = '
This module contains remoting tool cmdlets.
Enable-SSHRemoting cmdlet:
--------------------------
PowerShell SSH remoting was implemented in PowerShell 6.0 but requries SSH (client) and SSHD (service) components
to be installed. In addition the sshd_config configuration file must be updated to define a PowerShell endpoint
as a subsystem. Once this is done PowerShell remoting cmdlets can be used to establish a PowerShell remoting
session over SSH that works across platforms.
$session = New-PSSession -HostName LinuxComputer1 -UserName UserA -SSHTransport
There are a number of requirements that must be satisfied for PowerShell SSH based remoting:
a. PowerShell 6.0 or greater must be installed on the system.
Since multiple PowerShell installations can appear on a single system, a specific installation can be selected.
b. SSH client must be installed on the system as PowerShell uses it for outgoing connections.
c. SSHD (ssh daemon) must be installed on the system for PowerShell to receive SSH connections.
d. SSHD must be configured with a Subsystem that serves as the PowerShell remoting endpoint.
The Enable-SSHRemoting cmdlet will do the following:
a. Detect the underlying platform (Windows, Linux, macOS).
b. Detect an installed SSH client, and emit a warning if not found.
c. Detect an installed SSHD daemon, and emit a warning if not found.
d. Accept a PowerShell (pwsh) path to be run as a remoting PowerShell session endpoint.
Or try to use the currently running PowerShell.
e. Update the SSHD configuration file to add a PowerShell subsystem endpoint entry.
If all of the conditions are satisfied then PowerShell SSH remoting will work to and from the local system.
'
PowerShellVersion = '6.0'
FunctionsToExport = 'Enable-SSHRemoting'
}

View file

@ -0,0 +1,508 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
##
## Enable-SSHRemoting Cmdlet
##
class PlatformInfo
{
[bool] $isCoreCLR
[bool] $isLinux
[bool] $isOSX
[bool] $isWindows
[bool] $isAdmin
[bool] $isUbuntu
[bool] $isUbuntu14
[bool] $isUbuntu16
[bool] $isCentOS
[bool] $isFedora
[bool] $isOpenSUSE
[bool] $isOpenSUSE13
[bool] $isOpenSUSE42_1
[bool] $isRedHatFamily
}
function DetectPlatform
{
param (
[ValidateNotNull()]
[PlatformInfo] $PlatformInfo
)
try
{
$Runtime = [System.Runtime.InteropServices.RuntimeInformation]
$OSPlatform = [System.Runtime.InteropServices.OSPlatform]
$platformInfo.isCoreCLR = $true
$platformInfo.isLinux = $Runtime::IsOSPlatform($OSPlatform::Linux)
$platformInfo.isOSX = $Runtime::IsOSPlatform($OSPlatform::OSX)
$platformInfo.isWindows = $Runtime::IsOSPlatform($OSPlatform::Windows)
}
catch
{
$platformInfo.isCoreCLR = $false
$platformInfo.isLinux = $false
$platformInfo.isOSX = $false
$platformInfo.isWindows = $true
}
if ($platformInfo.isWindows)
{
$platformInfo.isAdmin = ([System.Security.Principal.WindowsPrincipal]::new([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole( `
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
if ($platformInfo.isLinux)
{
$LinuxInfo = Get-Content /etc/os-release -Raw | ConvertFrom-StringData
$platformInfo.isUbuntu = $LinuxInfo.ID -match 'ubuntu'
$platformInfo.isUbuntu14 = $platformInfo.isUbuntu -and ($LinuxInfo.VERSION_ID -match '14.04')
$platformInfo.isUbuntu16 = $platformInfo.isUbuntu -and ($LinuxInfo.VERSION_ID -match '16.04')
$platformInfo.isCentOS = ($LinuxInfo.ID -match 'centos') -and ($LinuxInfo.VERSION_ID -match '7')
$platformInfo.isFedora = ($LinuxInfo.ID -match 'fedora') -and ($LinuxInfo.VERSION_ID -ge '24')
$platformInfo.isOpenSUSE = $LinuxInfo.ID -match 'opensuse'
$platformInfo.isOpenSUSE13 = $platformInfo.isOpenSUSE -and ($LinuxInfo.VERSION_ID -match '13')
$platformInfo.isOpenSUSE42_1 = $platformInfo.isOpenSUSE -and ($LinuxInfo.VERSION_ID -match '42.1')
$platformInfo.isRedHatFamily = $platformInfo.isCentOS -or $platformInfo.isFedora -or $platformInfo.isOpenSUSE
}
}
class SSHSubSystemEntry
{
[string] $subSystemLine
[string] $subSystemName
[string] $subSystemCommand
[string[]] $subSystemCommandArgs
}
class SSHRemotingConfig
{
[PlatformInfo] $platformInfo
[SSHSubSystemEntry[]] $psSubSystemEntries = @()
[string] $configFilePath
$configComponents = @()
SSHRemotingConfig(
[PlatformInfo] $platInfo,
[string] $configFilePath)
{
$this.platformInfo = $platInfo
$this.configFilePath = $configFilePath
$this.ParseSSHRemotingConfig()
}
[string[]] SplitConfigLine([string] $line)
{
$line = $line.Trim()
$lineLength = $line.Length
$rtnStrArray = [System.Collections.Generic.List[string]]::new()
for ($i=0; $i -lt $lineLength; )
{
$startIndex = $i
while (($i -lt $lineLength) -and ($line[$i] -ne " ") -and ($line[$i] -ne "`t")) { $i++ }
$rtnStrArray.Add($line.Substring($startIndex, ($i - $startIndex)))
while (($i -lt $lineLength) -and ($line[$i] -eq " ") -or ($line[$i] -eq "`t")) { $i++ }
}
return $rtnStrArray.ToArray()
}
ParseSSHRemotingConfig()
{
[string[]] $contents = Get-Content -Path $this.configFilePath
foreach ($line in $contents)
{
$components = $this.SplitConfigLine($line)
$this.configComponents += @{ Line = $line; Components = $components }
if (($components[0] -eq "Subsystem") -and ($components[1] -eq "powershell"))
{
$entry = [SSHSubSystemEntry]::New()
$entry.subSystemLine = $line
$entry.subSystemName = $components[1]
$entry.subSystemCommand = $components[2]
$entry.subSystemCommandArgs = @()
for ($i=3; $i -lt $components.Count; $i++)
{
$entry.subSystemCommandArgs += $components[$i]
}
$this.psSubSystemEntries += $entry
}
}
}
}
function UpdateConfiguration
{
param (
[SSHRemotingConfig] $config,
[string] $PowerShellPath
)
#
# Update and re-write config file with existing settings plus new PowerShell remoting settings
#
# Subsystem
[System.Collections.Generic.List[string]] $newContents = [System.Collections.Generic.List[string]]::new()
$psSubSystemEntry = "Subsystem powershell {0} {1} {2} {3}" -f $powerShellPath, "-SSHS", "-NoProfile", "-NoLogo"
$subSystemAdded = $false
foreach ($lineItem in $config.configComponents)
{
$line = $lineItem.Line
$components = $lineItem.Components
if ($components[0] -eq "SubSystem")
{
if (! $subSystemAdded)
{
# Add new powershell subsystem entry
$newContents.Add($psSubSystemEntry)
$subSystemAdded = $true
}
if ($components[1] -eq "powershell")
{
# Remove all existing powershell subsystem entries
continue
}
# Include existing subsystem entries.
$newContents.Add($line)
}
else
{
# Include all other configuration lines
$newContents.Add($line)
}
}
if (! $subSystemAdded)
{
$newContents.Add($psSubSystemEntry)
}
# Copy existing file to a backup version
$uniqueName = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName())
$backupFilePath = $config.configFilePath + "_backup_" + $uniqueName
Copy-Item -Path $config.configFilePath -Destination $backupFilePath
if ($?)
{
WriteLine "A backup copy of the old sshd_config configuration file has been created at:"
WriteLine $backupFilePath
}
Set-Content -Path $config.configFilePath -Value $newContents.ToArray() -ErrorAction Stop
}
function CheckPowerShellVersion
{
param (
[string] $FilePath
)
if (! (Test-Path $FilePath))
{
throw "CheckPowerShellVersion failed with invalid path: $FilePath"
}
$commandToExec = "& '$FilePath' -noprofile -noninteractive -c '`$PSVersionTable.PSVersion.Major'"
$sb = [scriptblock]::Create($commandToExec)
try
{
$psVersionMajor = [int] (& $sb) 2>$null
Write-Verbose ""
Write-Verbose "CheckPowerShellVersion: $psVersionMajor for FilePath: $FilePath"
}
catch
{
$psVersionMajor = 0
}
if ($psVersionMajor -ge 6)
{
return $true
}
else
{
return $false
}
}
function WriteLine
{
param (
[string] $Message,
[int] $PrependLines = 0,
[int] $AppendLines = 0
)
for ($i=0; $i -lt $PrependLines; $i++)
{
Write-Output ""
}
Write-Output $Message
for ($i=0; $i -lt $AppendLines; $i++)
{
Write-Output ""
}
}
# Windows only GetShortPathName PInvoke
$typeDef = @'
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace NativeUtils
{
public class Path
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetShortPathName(
[MarshalAs(UnmanagedType.LPTStr)]
string path,
[MarshalAs(UnmanagedType.LPTStr)]
StringBuilder shortPath,
int shortPathLength);
public static string ConvertToShortPath(
string longPath)
{
int shortPathLength = 2048;
StringBuilder shortPath = new StringBuilder(shortPathLength);
GetShortPathName(
path: longPath,
shortPath: shortPath,
shortPathLength: shortPathLength);
return shortPath.ToString();
}
}
}
'@
<#
.Synopsis
Enables PowerShell SSH remoting endpoint on local system
.Description
This cmdlet will set up an SSH based remoting endpoint on the local system, based on
the PowerShell executable file path passed in. Or if no PowerShell file path is provided then
the currently running PowerShell file path is used.
The end point is enabled by adding a 'powershell' subsystem entry to the SSHD configuration, using
the provided or current PowerShell file path.
Both the SSH client and SSHD server components are detected and if not found a terminating
error is emitted, asking the user to install the components.
Then the sshd_config is parsed, and if a new 'powershell' subsystem entry is added.
.Parameter SSHDConfigFilePath
File path to the SSHD service configuration file. This file will be updated to include a
'powershell' subsystem entry to define a PowerShell SSH remoting endpoint, so current credentials
must have write access to the file.
.Parameter PowerShellFilePath
Specifies the file path to the PowerShell command used to host the SSH remoting PowerShell
endpoint. If no value is specified then the currently running PowerShell executable path is used
in the subsytem command.
.Parameter Force
When true, this cmdlet will update the sshd_config configuration file without prompting.
#>
function Enable-SSHRemoting
{
[CmdletBinding()]
param (
[string] $SSHDConfigFilePath,
[string] $PowerShellFilePath,
[switch] $Force
)
# Detect platform
$platformInfo = [PlatformInfo]::new()
DetectPlatform $platformInfo
Write-Verbose "Platform information"
Write-Verbose "$($platformInfo | Out-String)"
# Non-Windows platforms must run this cmdlet as 'root'
if (!$platformInfo.isWindows)
{
$user = whoami
if ($user -ne 'root')
{
if (! $PSCmdlet.ShouldContinue("This cmdlet must be run as 'root'. If you continue, PowerShell will restart under 'root'. Do you wish to continue?", "Enable-SSHRemoting"))
{
return
}
# Spawn new PowerShell with sudo and exit this session.
$modFilePath = (Get-Module -Name Microsoft.PowerShell.RemotingTools | Select-Object -Property Path).Path
$modName = [System.IO.Path]::GetFileNameWithoutExtension($modFilePath)
$modFilePath = Join-Path -Path (Split-Path -Path $modFilePath -Parent) -ChildPath "${modName}.psd1"
$parameters = ""
foreach ($key in $PSBoundParameters.Keys)
{
$parameters += "-${key} "
$value = $PSBoundParameters[$key]
if ($value -is [string])
{
$parameters += "'$value' "
}
}
& sudo "$PSHOME/pwsh" -NoExit -c "Import-Module -Name $modFilePath; Enable-SSHRemoting $parameters"
exit
}
}
# Detect SSH client installation
if (! (Get-Command -Name ssh -ErrorAction SilentlyContinue))
{
Write-Warning "SSH client is not installed or not discoverable on this machine. SSH client must be installed before PowerShell SSH based remoting can be enabled."
}
# Detect SSHD server installation
$SSHDFound = $false
if ($platformInfo.IsWindows)
{
$SSHDFound = $null -ne (Get-Service -Name sshd -ErrorAction SilentlyContinue)
}
elseif ($platformInfo.IsLinux)
{
$sshdStatus = sudo service ssh status
$SSHDFound = $null -ne $sshdStatus
}
else
{
# macOS
$SSHDFound = ($null -ne (launchctl list | Select-String 'com.openssh.sshd'))
}
if (! $SSHDFound)
{
Write-Warning "SSHD service is not found on this machine. SSHD service must be installed and running before PowerShell SSH based remoting can be enabled."
}
# Validate a SSHD configuration file path
if ([string]::IsNullOrEmpty($SSHDConfigFilePath))
{
Write-Warning "-SSHDConfigFilePath not provided. Using default configuration file location."
if ($platformInfo.IsWindows)
{
$SSHDConfigFilePath = Join-Path -Path $env:ProgramData -ChildPath 'ssh' -AdditionalChildPath 'sshd_config'
}
elseif ($platformInfo.isLinux)
{
$SSHDConfigFilePath = '/etc/ssh/sshd_config'
}
else
{
# macOS
$SSHDConfigFilePath = '/private/etc/ssh/sshd_config'
}
}
# Validate a PowerShell command to use for endpoint
$PowerShellToUse = $PowerShellFilePath
if (! [string]::IsNullOrEmpty($PowerShellToUse))
{
WriteLine "Validating provided -PowerShellFilePath argument." -AppendLines 1 -PrependLines 1
if (! (Test-Path $PowerShellToUse))
{
throw "The provided PowerShell file path is invalid: $PowerShellToUse"
}
if (! (CheckPowerShellVersion $PowerShellToUse))
{
throw "The provided PowerShell file path is an unsupported version of PowerShell. PowerShell version 6.0 or greater is required."
}
}
else
{
WriteLine "Validating current PowerShell to use as endpoint subsystem." -AppendLines 1
# Try currently running PowerShell
$PowerShellToUse = Get-Command -Name "$PSHome/pwsh" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
if (! $PowerShellToUse -or ! (CheckPowerShellVersion $PowerShellToUse))
{
throw "Current running PowerShell version is not valid for SSH remoting endpoint. SSH remoting is only supported for PowerShell version 6.0 and higher. Specify a valid PowerShell 6.0+ file path with the -PowerShellFilePath parameter."
}
}
# SSHD configuration file uses the space character as a delimiter.
# Consequently, the configuration Subsystem entry will not allow argument paths containing space characters.
# For Windows platforms, we can a short cut path.
# But for non-Windows platforms, we currently throw an error.
# One possible solution is to crete a symbolic link
# New-Item -ItemType SymbolicLink -Path <NewNoSpacesPath> -Value $<PathwithSpaces>
if ($PowerShellToUse.Contains(' '))
{
if ($platformInfo.IsWindows)
{
Add-Type -TypeDefinition $typeDef
$PowerShellToUse = [NativeUtils.Path]::ConvertToShortPath($PowerShellToUse)
if (! (Test-Path -Path $PowerShellToUse))
{
throw "Converting long Windows file path resulted in an invalid path: ${PowerShellToUse}."
}
}
else
{
throw "The PowerShell executable (pwsh) selected for hosting the remoting endpoint has a file path containing space characters, which cannot be used with SSHD configuration."
}
}
WriteLine "Using PowerShell at this path for SSH remoting endpoint:"
WriteLine "$PowerShellToUse" -AppendLines 1
# Validate the SSHD configuration file path
if (! (Test-Path -Path $SSHDConfigFilePath))
{
throw "The provided SSHDConfigFilePath parameter, $SSHDConfigFilePath, is not a valid path."
}
WriteLine "Modifying SSHD configuration file at this location:"
WriteLine "$SSHDConfigFilePath" -AppendLines 1
# Get the SSHD configurtion
$sshdConfig = [SSHRemotingConfig]::new($platformInfo, $SSHDConfigFilePath)
if ($sshdConfig.psSubSystemEntries.Count -gt 0)
{
WriteLine "The following PowerShell subsystems were found in the sshd_config file:"
foreach ($entry in $sshdConfig.psSubSystemEntries)
{
WriteLine $entry.subSystemLine
}
Writeline "Continuing will overwrite any existing PowerShell subsystem entries with the new subsystem." -PrependLines 1
WriteLine "The new SSH remoting endpoint will use this PowerShell executable path:"
WriteLine "$PowerShellToUse" -AppendLines 1
}
$shouldContinue = $Force
if (! $shouldContinue)
{
$shouldContinue = $PSCmdlet.ShouldContinue("The SSHD service configuration file (sshd_config) will now be updated to enable PowerShell remoting over SSH. Do you wish to continue?", "Enable-SSHRemoting")
}
if ($shouldContinue)
{
WriteLine "Updating configuration file ..." -PrependLines 1 -AppendLines 1
UpdateConfiguration $sshdConfig $PowerShellToUse
WriteLine "The configuration file has been updated:" -PrependLines 1
WriteLine $sshdConfig.configFilePath -AppendLines 1
WriteLine "You must restart the SSHD service for the changes to take effect." -AppendLines 1
}
}