mirror of
https://github.com/PowerShell/PowerShell.git
synced 2024-11-23 09:43:57 +08:00
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:
parent
23b7dce320
commit
5f46605a21
86
.vsts-ci/sshremoting-tests.yml
Normal file
86
.vsts-ci/sshremoting-tests.yml
Normal 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()
|
@ -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
|
||||
|
187
test/SSHRemoting/SSHRemoting.Basic.Tests.ps1
Normal file
187
test/SSHRemoting/SSHRemoting.Basic.Tests.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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" }
|
||||
|
@ -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'
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user