Powershell kaugtöö ja töövood

Sageli on meil vaja teha automaatselt midagi mitmes masinas.  toome näiteks üsna levinud vajaduse korjata masinatest kokku, et kes on lokaalse administraatorite grupi liikmed.

Alustame kõigepealt ühest masinast.  Siin pole vaja eriti midagi leiutada, sest internetis ringi vaadates leiab hulgaliselt variatsioone sellel teemal.  Valime neist minu arust kõige lihtsama ja kiirema lahenduse:

net localgroup Administrators

Seda tulemust tuleb natuke ilusamaks teha ja ära kaotada käsurea käsu asjasse mittepuutuv eelinfo, tabeli päis ja lõputeade, et kõik läks hästi (plus paar tühja rida)

net localgroup Administrators |
  Select-Object -Skip 6 |
  Where-Object {$_ -and $_ -notmatch "completed successfully."}

Nüüd jääb veel üle võimalus/vajadus muuta tulemus lihtsalt stringidest objektideks, mis muuhulgas teevad vahet domeeni ja lokaalsetel kasutajatel.  Ja teeme selle veel kohe funktsiooniks, et oleks mugavam korduvalt kasutada:

function Get-LocalAdmins {
  net localgroup Administrators |
    Select-Object -Skip 6 |
    Where-Object {$_ -and $_ -notmatch "completed successfully."} |
    ForEach-Object {
      $name = $_.split("\")
      $member = New-Object PSObject -Property @{DisplayName=$_
                                                Name=$name[-1]
                                                Domain=""}
      if ($name.length -eq 2) { $member.domain = $name[0] }
      $member
    }
}

Täiesti töötav lahendus.  Aga meil oleks vaja sama asja mitmes erinevas masinas.  Põhimõtteliselt, kui lisada ülaltoodud koodi lõppu funktsiooni väljakutse või jätta funktsioon ära, siis saaks koodi kirjutada faili ja siis kasutada tavalist Powershelli kaugkasutust:

"server1", "server2" | Out-File servers.txt
$session = New-PSSession -ComputerName (Get-Content .\servers.txt) -Credential domain\admin

Invoke-Command -FilePath .\get-localadmins.ps1 -Session $session |
  Select-Object Name, Domain, PSComputerName |
  Group-Object PSComputerName

Paarikümne masinaga polegi rohkem vaja, aga kui masinaid on rohkem, siis saaks teha ka natuke teistmoodi.  Nimelt saaksime me funktsiooni ümber defineerida töövooks, jättes funktsiooni sisu täpselt samaks.:

#Requires -Version 3

workflow Test-LocalAdmins {
  net localgroup Administrators |
    Select-Object -Skip 6 |
    Where-Object -FilterScript {$_ -and $_ -notmatch "completed successfully"} |
    ForEach-Object {
      $name = $_.split("\")
      $member = New-Object psobject -Property @{DisplayName=$_
                                                Name=$name[-1]
                                                Domain=""}
      if ($name.length -eq 2) { $member.domain = $name[0] }
      $member
    }
}

Ja nüüd saab seda töövoogu kasutada kohe võõraste masinate peal:

"server1", "server2" | Out-File servers.txt

Test-LocalAdmins -PSComputerName (Get-Content .\servers.txt) -PSCredential domain\admin |
  Select-Object Name, Domain, PSComputerName |
  Group-Object PSComputerName

Vaikimisi jookseb töövoog paralleelselt 100 masina peal (Invoke-Command jookseb vaikimisi 32 masina peal).  Lisaks võimaldab töövoog veel muid eeliseid, mida ilma töövoota on raskem teha, ent mis praeguses näites ei leia eriti kasutust.  Loomulikult võib ka Invoke-Command käsule ütelda paralleeltöötluse limiidiks suurema numbri kui 32 ja vajadusel saaks lisada ka parameetri –AsJob (et panna kogu krempel taustal tööle), ent töövoog tundub siiski lihtsam.  Kasvõi juba selle poolest, et sama töövoogu saab kasutada ka ainult lokaalse masina peal (kui parameeter –PSComputerName ära jätta).  Samuti aitab kaasa see, et töövoog võib olla osa imporditud moodulist (nagu funktsioon) töövoogu jooksutavas masinas, aga kaugtöö jaoks on enamasti lihtsam kirjutada skript (fail kettal), sest funktsioon tuleks defineerida ja käivitada eemal olevates (paljudes) masinates.

Töövoogude ainsaks puuduseks võiks pidada vajadust Powershell 3.0 järgi, aga kuna juba Server 2008/Vista võimaldavad antud versiooni kasutada, siis tänapäeval ei tohiks see enam problemaatiline olla.  Samas, kui masinate hulgas on ikka veel Windows XP/Server 2003 masinaid, siis tuleb otsustada tavalise kaugtöö kasuks.

Powershell Remoting ja TrustedHosts

Powershelliga teises masinas toimetamine on süsteemiülema jaoks väga mugav.  Ent ilma erilise vaevata töötab see vaid domeeni keskkonnas.  Niipea, kui masin(ad) ei kuulu domeeni või kuuluvad domeenidesse, mis teineteist ei usalda, ei ole see enam nii lihtne.

Põhjus peitub selles, et kaugühenduse loomisel kontrollitakse nii kasutajat (et see tohib üldse kaugsessiooni luua) kui ka masinat (ühenduse looja kontrollib, et sihtmasin on ikka õige).  Ja kui masinad saavad autentimiseks oma domeenikontosid kasutada, ongi asi korras.

Kui aga domeenikontod ei sobi, siis saab asja siiski tööle.  Selleks tuleb ühendust loovas masinas kuidagi korraldada sihtmasina usaldusväärsus.  Üks võimalus oleks kasutada serte, aga see muudaks asja enamasti veelgi keerulisemaks.  Natuke lihtsam oleks sihtmasina lisamine usaldusväärsete masinate nimekirja.  Sinna nimekirja lisamine tähendab sisuliselt seda, et sihtmasina identiteeti enam ei kontrollita.

Nimetatud tegevust saab teostada ainult süsteemiülema õigustes kasutaja.  Ning teha saab seda näiteks järgnevalt:

#Requires -RunAsAdministrator
#Requires -Version 2

$NewList = "uusmasin, 192.168.2.3"

# salvestame praeguse listi
$OldHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value

# salvestame listi
Set-Item WSMan:\localhost\Client\TrustedHosts -Value $NewList -Concatenate

Viimases reas lisatud käsurea parameeter korraldab selle, et uus väärtus lisatakse olemasolevale, mitte ei kirjutata üle. Tänud Kaidole, kes sellele parameetrile peale sattus 🙂

Nüüd tasuks sihtmasinas üle vaadata, kas ja kes kaugsessioonide kaudu üldse sisse saavad.  Vaikimisi saavad kaugsessioone kasutada vaid süsteemiülemad.  Alates Windows 8/Server 2012 keskkonnast on ligipääs lubatud veel lokaalse grupi Remote Management Users liikmetele.  Varasemate OS-versioonide puhul sellist gruppi pole ning seetõttu tuleks ligipääsuõigusi täiendada.  Lihtsaim viis oleks vastava dialoogiakna avamine:

#Requires -RunAsAdministrator
#Requires -Version 2
Set-PSSessionConfiguration Microsoft.PowerShell –ShowSecurityDescriptorUI

#Requires -Version 3
Set-PSSessionConfiguration microsoft.powershell.workflow –ShowSecurityDescriptorUI

Kui sihtmasin on Windows Server 2012 või kui sihtvõrgus on selline OS kättesaadav, siis saab kaugsessiooni häälestust mõnevõrra lihtsustada.  Nimelt on Server 2012-s olemas komponent Windows PowerShell Web Access (PSWA).  Ja see on sisuliselt veebisait, mis vahendab Powershelli käsurida veebilehitsejasse.  Jääb muidugi veel probleem, et Server 2012 (PSWA) masinast tuleb edasi sihtmasinasse saada ikkagi WinRM sessiooni kaudu ning kogu ülaltoodud jutt võib olla oluline.

RemoteApp Windows kliendi-OS masinast

Termina Services RemoteApp oli Windows Server 2008 kaugtöölaua teenuste uus kasutusviis, mis võimaldas kaugtöölaua ühenduses käivitatud rakenduse pilti näidata otse füüsilise töölaua peal.  Ainsaks tingimuseks kliendile oli, et kaugtöölaua klient toetaks RDP protokolli versiooni 6 (või värskemat).

Sarnast funktsionaalsust pakub ka MED-V: lahendus, kus virtuaalmasinas töötava rakenduse pilt tuuakse füüsilise masina töölauale.  Ainuke erinevus ülaltooduga on see, et rakendus peab töötama virtuaalmasinas, mis omakorda töötab samas arvutis töölauaga.  Ja virtuaalmasinas toetatakse muuhulgas ka Windows XP-d.  Kuna MED-V on osa Microsoft Desktop Optimization Pack’ist, siis on antud lahendus kättesaadav ainult Windows Enterprise (Windows + tarkvara tagatis) hulgilitsentsi omanikele, siis ei kõlba see kodus kasutamiseks.

Kui Windows 7 hakkas valmis saama, siis tuli Microsoft välja Windows Virtual PC ja XP Mode lahendusega, mis samuti pakkusid sarnast lahendust, seekord aga juba ka Windows 7 Pro omanikele.  Puuduseks jälle oli see, et Windows Virtual PC nõudis protsessorilt riistvaras virtualiseerimise tuge.  Hiljem avaldati küll parandus, mis selle nõude ära kaotab, ent ikkagi on vaja Windows 7 Pro’d, et omandada õigus Windows XP Mode virtuaalmasina kasutamiseks.

Tegelikult on RemoteApp funktsionaalsus kogu aeg olnud kaugtöölaua protokolli oskus.  Ja Windows klient-OSi  võib õpetada seda ka serverina tegema.  Ehk siis me saame Windows’i masinast käivitada rakendusi ning kasutada neid teises masinas.  Windows XP ja Vista jaoks tuleb alla tõmmata ning paigaldada parandus.  Windows 7 puhul pole parandust vaja.  Edasi tuleb masinas teha järgmist:

  1. Lülitada sisse kaugtöölaua tugi (tuletame meelde, et Windows Home väljaannetel seda pole)
  2. Häälestada kaugtöölaua teenus lubama RemoteApp rakendusi.  Selleks tuleb natuke käia registri kallal. Õige registrivõti on järgmine:
    HKLM\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\TsAppAllowList
    
    1. Kui soovime, et masinas saaks käivitada kõiki rakendusi kasutades RemoteApp funktsionaalsust, siis tuleks võtmesse lisada (DWord) väärtus fDisabledAllowList (=1)
    2. kui soovime kontrollida rakendusi, mida RemoteApp abil käivitada saab, siis tuleb natuke rohkem vaeva näha.  Toon siin ära eksporditud registri faili, mida importides saab RemoteApp rakenduste hulka lisada cmd.exe:
      Windows Registry Editor Version 5.00
      
      [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TsAppAllowList\Applications\cmd]
      "Name"="Command Prompt"
      "Path"="c:\windows\system32\cmd.exe"
      

Sellega on server ette valmistatud.  Jääb üle vaid luua .RDP fail, millega rakendus käivitada.  Alustuseks võta kaugtöölaua kliendiga salvestatud .RDP fail, millega saad sihtmasinasse tavalise kaugtöölaua ühenduse teha.  Ava see fail tekstiredaktoriga (näiteks Notepad) ning tee järgmised muudatused:

  1. leia ning asenda rida:
    remoteapplicationmode:i:1
    
  2. Lisa read:
    RemoteApplicationName:s:<rakenduse kasutajasõbralik nimi>
    RemoteApplicationProgram:s:||<programmi nimi>
    

    kusjuures <rakenduse kasutajasõbralik nimi> on mis tahes string ning <programmi nimi> on “serveri” registris kirjeldatud rakenduse registrivõtme nimi.

  3. Windows XP masinast RemoteApp rakenduse käivitamiseks tuleb teha veel muudatusi:
    1. leia ning asenda rida:
      Alternate shell:s:rdpinit.exe
      
    2. lisa read:
      DisableRemoteAppCapsCheck:i:1
      Prompt for Credentials on Client:i:1
      

Edasi jääb vaid äsja muudetud .RDP fail käivitada.

Kui ülaltoodud juhend .RDP faili loomiseks tundub liiga keerukas, siis tõenäoliselt saab laenata Server 2008 (R2) kaugtöölaua serveri RemoteApp Manageriga loodud .RDP faili ning muuta seal vaid serveri nimi

Powershell ja admin õigused

Vahel on vaja, et skriptis saaks käivitada mingi osa teise kasutaja (adminni) õigustes.  Windows 2000/XP/Srv2k3 sees on võimalik kasutada käsku RunAs.exe, ja Powershell 1.0 aegu oli see ka pea-aegu kõik, mida kasutada sai.  Ainsaks alternatiiviks oli sarnane kood:

$credential = Get-Credential domain\user
[System.Diagnostics.Process]::Start("cmd.exe",
                                    "/c dir",
                                    "user",
                                    $credential.Password,
                                    "domain")

Vindows Vista väljatuleku järel muutus aktuaalseks User Account Control.  See tegi osad tegevused keerulisemaks ning tuli hakata kasutama kolmanda partei utiliite, nagu näiteks tutvustas Technet Magazine.  Nimetatud utiliidid kasutavad tegelikult Vista standardset rakenduste käivitamise liidest, mida saab kasutada ka otse skriptis, nii et ülaltoodud näite saaks ümber kirjutada kujule:

$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.Verb = "runas"
$psi.FileName = "cmd.exe"
$psi.Arguments = "/c dir"
[System.Diagnostics.Process]::Start($psi)

Powershell 2.0 teeb asja palju lihtsamaks.  Ainus asi, mida peab kasutama, on järgmine koodilõik:

#requires –Version 2.0
Start-Process -Verb runas -FilePath cmd.exe -ArgumentList "/c dir"

Mõlemad ülaltoodud skriptid kasutavad võimalust käivitada protsess režiimis Run As Administrator.  Pole vaja hiirega paremklõpsata või käivitada käsurida otsingulahtrist klahvikombinatsiooniga Ctrl-Shift-Enter.  Ent kui me tahaks rakendust käivitada mitte adminni, vaid kellegi teise õigustes?  Pole midagi lihtsamat:

#requires –Version 2.0
Start-Process -Credential domainuser -FilePath cmd.exe -ArgumentList "/c dir"

Ent mis saab siis, kui me tahaksime teise kasutaja (adminni) õigustes käivitada ainult osa skriptist?  Powershell 1.0 ajal tuli selleks see osa koodist panna eraldi skripti ning kahe ülemise koodinäite võimalusi käivitamaks powershell.exe koos käsurea parameetritega skripti käivitamiseks.  Powershell 2.0 lisab uusi võimalusi.  Järgnevate näidete eelduseks on see, et arvutis, kus koodinäide jooksma peaks, on Powershelli kaugkasutus sisse lülitatud.

Juhul, kui on vaja käivitada üksikuid käske/skripte:

#requires –Version 2.0
$session = New-PSSession -ComputerName . -Credential domainuser
Invoke-Command -Session $session -ScriptBlock {whoami.exe}

Juhul, kui on vaja pikemat interaktiivset sessiooni:

#requires –Version 2.0
$session = New-PSSession -ComputerName . -Credential domainuser
Enter-PSSession -Session $session

Mõlemad ülaltoodud näited võivad kasutada sessioonimuutujat korduvalt, kuni sessioon eksisteerib ja püsti on.

Lisaks ülaltoodule omavad paljud Powershelli käsud argumenti –Credential , mille abil saab nende käskude käivitamiseks kasutada teist identiteeti.  Selliste käskude loendi saab järgmise käsureaga:

Get-Help * -Parameter credential

#Requires -Version 3.0
Get-Command -ParameterName credential

Nüüd võib muidugi tekkida kiusatus salvestada identiteet ja seda edaspidi failist lugeda:

Get-Credential | Export-Clixml .\admin.xml

$user = Import-Clixml .\admin.xml
Start-Process -Credential $user -FilePath cmd.exe -ArgumentList "/c dir"

Siinjuures tuleb arvestada, et kuna salvestatud faili läheb nii kasutajanimi, kui parooli räsi, siis saab sama faili abil igaüks salvestatud identiteeti kasutada. Natuke parem variant oleks see, kui me salvestaksime ainult parooli:

#requires –Version 2.0
Read-Host -AsSecureString "Ütle üks parool" |
  ConvertFrom-SecureString |
  out-file .\parool.txt

$pass = Get-Content .\parool.txt | ConvertTo-SecureString
$user = New-Object -TypeName System.Management.Automation.PSCredential `
                   -ArgumentList "domain\kasutaja", $pass
Start-Process -Credential $user -FilePath cmd.exe -ArgumentList "/k dir"

Ka sellel lähenemisel on probleem, et igaüks, kes saab kätte korraga skripti ja paroolifaili, saab salvestatud parooli samas masinas kasutada.  Ainult skriptile või paroolifailile ligipääsu korral tuleb teist osapoolt mõistatama hakata. Ja kui püüda salvestatud parooli kasutada teises masinas, siis see ei tööta.

Saaks veel ka nii, et me kodeerime parooli räsi ära. See garanteerib, et salvestatud parooli saab erinevates masinates kasutada. Ent siis jääb küsimus, et kus/kuidas hoida koodi, millega parooli räsi kätte saab:

#Requires –Version 2.0

$parool = Read-Host -AsSecureString "Ütle üks parool"
$kood = Read-Host -AsSecureString "Ütle kood, millega parool kodeerida"
ConvertFrom-SecureString $parool -SecureKey $kood |
    Out-File .\turvaline.txt

$pass = Get-Content .\turvaline.txt |
    ConvertTo-SecureString -SecureKey $kood
$user = New-Object -TypeName System.Management.Automation.PSCredential `
                   -ArgumentList "domain\kasutaja", $pass
Start-Process -Credential $user -FilePath cmd.exe -ArgumentList "/k whoami"