Powershell 5.0 eelvaade

Veebruaris tuli välja järjekordne versioon Windows Management Framework 5.0-st.  Võrreldes varasemate versioonidega on nüüd näiteks tööle hakanud kõik erinevad linkide versioonid:

New-Item -ItemType File -Path asi.txt
New-Item -ItemType Directory -Path kaust

#Requires -Version 5
New-Item -ItemType HardLink -Name sama.txt -Value .\asi.txt
New-Item -ItemType Junction -Path .\kaustalink -Value .\kaust

#Requires -RunAsAdministrator
New-Item -ItemType SymbolicLink -Path .\uus.txt -Value .\asi.txt
New-Item -ItemType SymbolicLink -Path .\uuskaust -Value .\kaust

Veel üks uus ja põnev omadus on oma skripti lähtekoodi analüüs, kasutades etteantud või ise lisatud reegleid.

Sama versioon (või versiooninumbri järgi isegi värskem versioon) on ka Windows 10 sees.

Advertisements

E-maili aadresside eemaldamine Exchange serveris

Meiliaadresside lisamine on tavaliselt lihtne. Kui seda on vaja paljudele, siis muudame meiliaadreside poliitikat; kui seda on vaja üksikutele, siis lihtsalt lisame.

Eemaldamisega on lugu natuke keerulisem.  Kõigepealt tuleb üles leida adressaadid, kellel on sobiv aadress.  Üldiselt sobib selleks käsk Get-Recipient, aga teeme seda lihtsuse huvides ainult postkastidega:

$EmailFilter = "*@minu.ee"
$kastid = Get-Mailbox -ResultSize Unlimited |
  Where-Object EmailAddresses –Like $emailFilter

Kui adressaadid leitud, siis tuleb üles leida ka konkreetne eemaldamist vajav aadress.  Seda peamiselt seetõttu, et olemasolevate aadresside loendist ei saa eemaldada aadressi mustri järgi, vaid tuleb esitada täisaadress:

$kastid | Foreach-Object {
  foreach ($address in $_.emailaddresses) {
    if ($address -like $EmailFilter) {
      New-Object PSObject -Property @{Name=$_.name; Alias=$_.alias ; SmtpAddress=$address}
    }
  }
}

Tulemuseks kuvatakse hetkel meile kõik leitud meiliaadressid koos postkasti nimega, mille küljes see aadress on.  Nii et nüüd jääb üle asendada kuvamine kustutamisega.  Siin saab ära kasutada Exchange serveri üldist lähenemist: kui atribuudil võib olla mitu väärtust, siis saame olemasolevate väärtuste hulgas teha muutusi (lisada, eemaldada, asendada)

      Set-Mailbox -Identity $_.id -EmailAddresses @{remove=$address}

Ülaltoodud näide töötab Exchange 2010..2016 peal.  Exchange 2007-l ei olnud veel seda mainitud üldist lähenemist loodud, seega peab seal tegema natuke teistmoodi:

      $NewAddresses = $_.emailaddresses - $address
      Set-Mailbox -Identity $_.id -EmailAddresses $NewAddresses

Kui nüüd peaks vajadus tekkima eemaldada aadresse gruppide, kontaktide või muude adressaatide küljest, siis tuleb lihtsalt asendada adressaatide leidmise ja muutmise käsud.  Muu loogika peaks samaks jääma.

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

Kui Sa juhtud kasutama Powershell 5.1, siis sinna on lisatud moodul Microsoft.PowerShell.LocalAccounts ja seal on loomulikult olemas käsk Get-LocalGroupMember.  Ent kõikides masinates seda ei ole.  Ja muuhulgas pole seda enam ka Powershell Core’is.

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.

Kasutajate paroolid ja nende aegumine

Mõni aeg tagasi sai kirjutatud sellest, kuidas avastada domeenist kontosid, mida pole ammu kasutatud.  Muuhulgas sai siis ka mainitud, et selliseid kontosid leiab näiteks selle järgi, et nad pole ammu oma parooli muutnud. Parooli muutmine ja selle regulaarne nõudmine on tänapäeval üpris tavaline.  Selle tõttu aga tekivad pidevalt probleemid inimsete (või teenuste) sisselogimisega.  Seda eriti juhul, kui inimene kasutab veebipõhist rakendust, mis ei oska parooli aegumisel pakkuda paroolivahetust. Või siis on probleemid inimestel, kes lähevad natuke pikemaks ajaks kontorist ära ning parool aegub just sel ajavahemikul. Kuidas siis tuvastada, et inimese parool hakkab kohe (varsti) aeguma.  Esimese hooga võiks ju pakkudua, et Powershelli käsk Search-ADAccount on hea mõte.  Ent natuke edasi vaadates tuleb välja, et see oskab otsida juba aegunud parooliga kodanikke, kes enam ei saa sisse.  Meie aga tahaks hoopis teada, kellel varsti hakkab parool aeguma. Ega siin ei jäägi muud üle, kui otsida domeenist kasutajaid ning vaadata, millal nende parool viimasti muudetud sai:

#Requires -Modules ActiveDirectory
Get-ADUser -filter * -Properties PasswordLastSet

Nüüd tuleb vaid juurde vaadata, et milline on paroolipoliitika:

$passwordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

Ja siis saab arvutada kuupäeva, millal parool aegub ehk siis mitu päeva tänasest veel parool kehtib:

$user = Get-ADUser mati –Properties Mail,PasswordLastSet

$expiresTime = $user.PasswordLastSet.Add($passwordAge) - (Get-Date)

Edasi tuleb tuvastada teavitamisvajadus.  Ütleme, et kasutajat tuleb teavitada, kui parool aegub 5 või vähema päeva pärast:

if ($expiresTime.Days -le 5 ) {
  #teavitame
}

Kas teavitus on ekraani peal tekstiaken või saadetud e-mail, sõltub juba sellest, et kuidas me teavitada soovime. Kui ise ei suuda otsustada, et mitu päeva ette tuleks teavitada, siis võib kasutada ka Windowsi registrisse kirjutatud väärtust:

$notifyDays = Get-ItemProperty -Name PasswordExpiryWarning `
  -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' |
  Select-Object -ExpandProperty PasswordExpiryWarning

if ($expiresTime.Days -le $notifyDays ) {
  # ...
}

Mainitud registri väärtust saab muuta ka Group Policy abil.

Ülaltoodu on piisav, kui ettevõttes ei ole kasutusel Fine-Grained Password Policy.  Kui aga on, siis tuleb parooli aegumise tähtaega hoopis kasutaja küljest otsida:

$domainFunctionalLevel = (get-addomain).DomainMode

if ($domainFunctionalLevel -ge 3) {
  ## Windows2008 domain functional level or greater
  $accountFGPP = Get-ADUserResultantPasswordPolicy $user
  if ($accountFGPP) {
    $passwordAge = $accountFGPP.MaxPasswordAge
  }
}

Kui tuvastada ühe kasutaja parooli aegumist näiteks Logon skripti abil, siis tuleks ülatoodud kasutaja leida ühel järgmistest viisidest:

$user = Get-ADUser ([Security.Principal.WindowsIdentity]::GetCurrent().user)

$user = Get-ADUser ("{0}\{1}" -f $env:USERDOMAIN, $env:USERNAME)

Kui tuvastada kõik kasutajad, kelle parool aegub teatud aja pärast, siis peaks tegema midagi sarnast:

$users = Get-ADUser  –Properties PasswordLastSet `
  –Filter {Enabled –eq $true -and PasswordNeverExpires -eq $false -and PasswordExpired -eq $false -and logonCount -ge 1}  |
  Where-Object {! $_.CannotChangePassword}

foreach ($user in $users) {
  # …
}

Mõned aastad tagasi sai kirjutatud Powershelli script, mille eesmärgiks oligi tuvastada kasutajad, kelle parool aegub teatud arvu päevade pärast, ning saata neile e-maili teel teavitus.  Nimetatud skript sai hiljuti laetud ka Techneti skriptide galeriisse.  Nii et kui selle artikli järgi ise omale sobivat skritpti kokku ei oska/taha/suuda panna, siis võib juba valmis skripti alla laadida.

Jälle Powershell 5 Preview

Juulikuus tuli välja järjekordne  uuendus Windows Management Framework 5 eelvaatele.  Erinevalt varasematest pole seda aga kuskil mainitud.  Tänud Thomas Lee-le, kes jagas uue eelvaate linke ka meiega:

Uued ja kasulikud oskused selles versioonis on oskus saada hakkama .zip arhiivide loomise ja lahtipakkimisega ning failisüsteemis linkide loomise/näitamisega. Lisaks veel ka see, et nüüd on võimalik logida kõik Powershelli poolt täidetavad käsud (ka need, mis oma sisu varjata püüavad).

#Requires -Version 5

Compress-Archive -Path .\asi.txt -DestinationPath .\arhiiv.zip
Compress-Archive -Path .\tööleht.txt -DestinationPath .\arhiiv.zip -Update

Expand-Archive .\arhiiv.zip -DestinationPath .\kaust

#Requires -RunAsAdministrator
New-Item -ItemType SymbolicLink -Path .\uus.txt  -Value .\asi.txt
New-Item -ItemType SymbolicLink -Path .\uuskaust -Value .\kaust
  #need veel ei tööta
New-Item -ItemType HardLink -Name sama.txt -Value .\asi.txt
New-Item -ItemType Junction -Path .\kaustalink -Value .\kaust

Powershell 5 Preview uuenes

Mainisin hiljuti Windows Management Framework 5.0 eelvaadet.  Nüüd on sellest avaldatud järgmine versioon: (Mai 2014) eelvaade.  Selle paigaldamiseks tuleb eelmine eelvaate versioon alt ära võtta (kui see ees on).

Peamine põhjus, miks tahta just maikuu eelvaadet, on lisandunud moodul PowerShellGet, mis võimaldab otsida, paigaldada ja uuendada mooduleid vastu keskset hoidlat.  Ehk siis saab teha midagi järgnevale sarnast:

Find-Module *cim*

Install-Module CimInventory -Scope CurrentUser

Update-Module

Nagu arvata võib, siis esialgu on moodulite hoidla tühjavõitu, aga käsk Publish-Module peaks selle vea kiiresti ära parandama.

Excel, CSV ja Powershell

Aeg-ajalt on meil vaja tegeleda CSV failidega.  See lühend peaks tähendama Comma Separated Value ent Excelil on sellest oma arusaam.  Nimelt kasutab Excel regiooni seadetest loendieraldajat, ning Eesti seadete korral on selleks koma asemel semikoolon.  Ja kui Excelis teha valmis kasutajate nimikiri, kellele oleks vaja teha kasutajakonto ja postkast, siis on CSV-failiks salvestamise tulemus järgmine

Eesnimi;Perenimi;osakond
Mati;Mõisnik;Müük
Kätlin;Pöögelman;Arendus

Ja kui seda faili püüda Powershellis importida, siis tekib kaks probleemi:

  1. Import-CSV eeldab, et CSV fail on salvestatud ASCII kodeeringus (ehk siis täpitähed on paigast ära)
  2. semikoolon on Import-CSV jaoks vale eraldaja

Esimese probleemi vastu on lihtne lahendus: Get-Content oskab faili sisu õiges kodeeringus kätte saada.  Ainult et Get-Content annab meile faili tekstistringidena.  Õnneks on meil olemas käsk ConvertFrom-CSV:

#Requires -Version 3

Get-Content .\Vihik1.csv | ConvertFrom-Csv -UseCulture

Käsurea parameeter UseCulture hoolitseb selle eest, et kasutataks õiget loendieraldajat.

Sarnane probleem tekib meil ka siis, kui oleks vaja näiteks kasutajakonto info eksportida Excelisse:

#Requires -Version 3

Get-ADUser -Filter  {(Enabled -eq $true) -and (City -like "Tallinn")} `
    -Properties mail |
  Select-Object *name, mail |
  ConvertTo-Csv -UseCulture -NoTypeInformation|
  Out-File kasutajad.csv

Aga ehk saaks ka Import-CSV ja Export-CSV õigesti tööle?  Tegelikult saabki:

#Requires -Version 3
import-csv .\Vihik1.csv -Encoding Default -UseCulture

Get-ADUser -Filter  {(Enabled -eq $true) -and (City -like "Tallinn")} `
    -Properties mail |
  Select-Object *name, mail |
  Export-Csv kasutajad.csv -Encoding Default -UseCulture

Häda on vaid selles, et kui nüüd sama skript viia mõnda teise riiki, siis tuleks kasutada teist loendieraldajat.  Õnneks on loendieraldajat võimalik süsteemi käest küsida:

Get-Culture
(Get-Culture).TextInfo
(Get-Culture).TextInfo.ListSeparator

(Get-Culture).GetType().FullName
  # Powershell ISE korral
(Get-Culture).GetType().BaseType.FullName

Tuleb vaid meeles pidada, et loendieraldaja on kasutajapõhine seadistus ja kui CSV fail läheb teise inimese kätte või tuleb teise inimese käest, siis tuleb teada tema keele-eelistusi:

$kultuur = [System.Globalization.CultureInfo]"en-us"
$eraldaja = $kultuur.TextInfo.ListSeparator
Import-Csv .\Vihik1.csv -Encoding Default -Delimiter $eraldaja