Mis keskkonnas mu skript jookseb

Iga Windowsi süsteemiülem peab oskama PowerShelli kasutada.  Kui ta seda ei oska, siis ei saa ta ennast Windowsi süsteemiülemaks nimetada.  Ja vähehaaval nad seda ka õpivad (kui nad juba ei oska).

Kõik on väga tore, kuni me teame, millises masinas me oma asju jooksutame.  Aga kui me oleme jõudnud juba sinnamaale, et koodi jooksutatakse automaatselt, siis läheb elu keerulisemaks.  Nimelt tuleb siis hakata tuvastama, et mis keskkonnas kood parasjagu jookseb.  Sest kui seda mitte teha, siis võib kood mitte töötada.

Üks esimesi asju mida tuvastada on see, et kas meil on ka süsteemiülema õigused.  Seda on lihtsaim teha, kui luua omale abistav funktsioon:

function Test-IsAdmin {
  $AdminRole = [Security.Principal.WindowsBuiltinRole]::Administrator
  $CurrentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
  $CurrentUser = [Security.Principal.WindowsPrincipal] $CurrentIdentity
  $CurrentUser.IsInRole($AdminRole)
}

Ja siis saab seda kasutada.  Alates Powershelli versioonist 4.0 on ka teine viis (täpsem info Powershelli spikris):

if (-not (Test-IsAdmin)) {
  throw "Süsteemiülema õigused vajalikud, katkestan"
}

#Requires -RunAsAdministrator

Kui süsteemiülema õigused on puudu, siis saab neid vajadusel omandada.  Sellest sai kirjutatud varem.

Tasub tähele panna ka seda, et #Requires kontrollid töötavad ainult salvestatud skripti sees ning neid kontrollitakse juba skripti laadimisel.  See tähendab, et kui nõue pole täidetud, ei käivitata skripti üldse ning seetõttu ei saa ka skripti sees midagi selle suhtes ette võtta.

Kui Sa jooksutad oma skripte erinevate Powershelli versioonide peal, siis on vaja ka seda kontrollida.  Selleks oli kaks meetodit:

if ($PSVersionTable.PSVersion -lt "5.0") {
  throw "see on vale PowerShelli versioon"
}

#Requires -Version 5.0

Neist esimese puhul ei pea tingimata skripti tööd katkestama.

Windows 8-st alates hakkasid Powershelli moodulid kaasa tulema ka opsüsteemiga.  Samuti on vaja moodulite olemasolu kontrollida juhul, kui kasutada mõnd lisatud funktsionaalsust.  Ka seda saab kahte moodi teha:

if (-not (get-module Hyper-V -ListAvailable)) {
  Write-Error "Vajalik funktsioon puudu, katkestan" -EA Stop
}

#Requires -Modules Hyper-V

Vajadusel saab kontrollida ka mooduli versiooni:

$ModuleId = @{ModuleName='Hyper-V';ModuleVersion="2.0.0.0"}
if (-not (Get-Module -FullyQualifiedName $ModuleId -ListAvailable)) {
  throw "Vajalik moodul puudu, katkestan"
}

#Requires -Modules @{ModuleName="Hyper-V";ModuleVersion="2.0.0.0"}

Kui moodulite halduseks on oma koodihoidla, siis saab muidugi lihtsamalt:

#Requires -RunAsAdministrator
#Requires -Modules PowerShellGet
Find-Module UserProfile -MinimumVersion 1.0 -Repository PSGallery |
  Install-Module -Force -Scope AllUsers

Get-InstalledModule | Update-Module

Aga vahel ei piisa ka mooduli olemasolu/versiooni kontrollist.  Näiteks on Windows 8.1-ga kaasas olevas moodulis (NetTCPIP) olemas käsk Test-NetConnection, mida Windows 8-ga kaasas olevas moodulis pole.  Aga mooduli versiooninumber on mõlemal sama.  Siis tuleb kontrollida konkreetse käsu olemasolu:

if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) {
  #olemas, võib tegutseda
} else {
  Write-Error "käsk puudu"
}

Get-Command telnet

Vahel oleks vaja hoopis teada, kas mõni opsüsteemi funktsioon on antud masinas olemas või puudu.  Windows Serveri haldusliidesed on funktsionaalsustest eraldi paigaldatavad ning võivad paikneda hoopis teises masinas.  Serveriga on asi lihtne.  Kui skripti jooksutavas masinas on ServerManager moodul (seda saab ka klient-OSile paigaldada, kui tõmmata omale RSAT), töötab järgmine kood:

if (-not (Get-WindowsFeature -Name RSAT-DNS-Server).Installed) {
  Install-WindowsFeature -Name RSAT-DNS-Server
}
#olemas, võib tegutseda

Klient-OSi puhul on see natuke keerulisem, kuna alates Win10 v1607-st otsustati osad OS funktsioonid muuta teistsuguseks, kui ülejäänud. Ja loomulikult tuli siis nende haldamiseks ka uued käsud välja mõelda:

#Requires -Modules Dism
#Requires -RunAsAdministrator

$Feature = Get-WindowsOptionalFeature -Online -FeatureName telnet*
if ($Feature.State -eq [Microsoft.Dism.Commands.FeatureState]::Disabled) {
  Enable-WindowsOptionalFeature -Online -FeatureName TelnetClient
}

# Win10/Srv16/Srv19
if (Get-Command Get-WindowsCapability -ErrorAction SilentlyContinue) {
  Get-WindowsCapability -Online -Name OpenSSH.Client*
}

Käsk Get-WindowsCapability on jälle üks neist, mis lisati tema moodulisse ilma mooduli versiooninumbrit muutmata. Ja ka Windows Sever (2016 või värskem) omab sama häda.

Edasi jääb veel vaid üle kontrollida, et kas meil on käes server-OS või midagi muud:

#Requires -Modules CimCmdlets
switch ((Get-CimInstance Win32_OperatingSystem).ProductType) {
  1 { "Workstation"}
  2 { "DC" }
  3 { "Server" }
}

Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" |
  Select-Object -Property EditionId, InstallationType

#Requires -Modules Dism
#Requires -RunAsAdministrator
if ((Get-WindowsEdition -Online).Edition -like 'Server*') {
  "on server"
} else {
  "klient, vist"
}

Server OS-i puhul võib veel tekkida vajadus teha vahet Semi-Annual Channel ja Long-Term Support Channel masinate vahel. Dokumentatsioonis on ära toodud kood, millega vajalik info kätte saada, ent neid on veel:

# ettevalmistus
$RegPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"

$Product = (Get-ItemProperty -Path $RegPath).ProductName
$Edition = (Get-ItemProperty -Path $RegPath).EditionId

#Requires -Modules CimCmdlets
$Product = (Get-CimInstance Win32_OperatingSystem).Caption

#Requires -Modules Dism
#Requires -RunAsAdministrator
$Edition = (Get-WindowsEdition -Online).Edition

# lõplik kontroll
switch -wildcard ($Edition) {
  'Server*ACor' {
    "Semi-Annual Channel"
    break
  }
  'Server*' {
    "Long-Term Support Channel or older than Server 2016"
  }
  Default { 'Not a Server product' }
}

switch -regex ($Product) {
  'Server 201[69] ' {
    "Long-Term Support Channel"
    break
  }
  ' Server [DS]' {
    "Semi-Annual Channel"
 }
}

Olen seni teadlikult vältinud OS versiooni kontrolli.  nagu ülaltpoolt näha, pole seda enamasti vaja.  Ent arvestades Windows 10 (ja Windows Serveri) väljalaske tsükliga, võib seda vaja minna:

if ([environment]::OSVersion.Version -ge "6.3") {
  "Windows 8.1 või värskem"
}

Õnnetuseks otsustas Microsoft antud viisile panna peale kontrolli: kui rakendus ise ei deklareeri, et ta tunneb uuemaid OS versioone, siis valetatakse talle versiooniks alati “6.2.9200” ehk siis Windows 8/Server 2012. Tundub, et Powershell 4 ja värskem deklareerib, ent algusepoole oli ka seal näha valet versiooni, seetõttu ei saa tagastatud vastuses alati kindel olla.  Lisaks on Windows 10 versiooninumbri algus kõigil väljalasetel sama, muutub ainult BuildNumber.  Seda viimast aga saab kontrollida:

#Requires -Modules CimCmdlets
switch ((Get-CimInstance Win32_OperatingSystem).BuildNumber) {
  {[int]$_ -lt 10240} {"Pre-Win10 version"}
  10240 {"Win10 RTM"}
  10586 {"Win10 1511"}
  14393 {"Win10/Svr16 1607"}
  15063 {"Win10 1703"}
  16299 {"Win10/Svr 1709"}
  17134 {"Win10/Svr 1803"}
  17763 {"Win10/Svr19/Svr 1809"}
  18362 {"Win10/Svr 1903"}
  18363 {"Win10/Svr 1909"}
  19041 {"Win10/Svr 2004"}
  default {"Insider/future build"}
}

Tegelikult saaks siin registrist info mugavamini kätte:

#Requires -Version 5.0
$RegPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
Get-ItemPropertyValue -Path $RegPath -Name ReleaseId

Get-CimInstance kasutamine võimaldab teha kontrolli ka eemalt.  Samamoodi saab ka versiooninumbri kätte.  Tuleb lihtsalt arvestada, et CIM tagastab versiooninumbri kui stringi ja see tuleb ise versiooninumbriks teisendada, kui seda vaja peaks olema:

#Requires -Modules CimCmdlets
(Get-CimInstance Win32_OperatingSystem).Version -as [version]

Varem oli vahel vaja ka teada Service Pack versiooni, aga uuemater versioonide puhul pole neid enam välja antud. Igaks juhuks paneme siia kirja selle tuvastamise moodused:

# text
$RegPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion"
Get-ItemProperty -Path $RegPath |
  Select-Object -Property CSDVersion

# number
$ServicePack = [environment]::OSVersion.Version.MajorRevision
if ($ServicePack -lt 2) {
  Write-Error 'Service Pack level too low'
}

Mis mälu mul masinas/serveris on

Aeg-ajalt tekib küsimus, et masinas on Windowsi (ja minu) teada miski hulk mälu, aga kui suured ja kui palju mooduleid masinas on ja kas midagi annab veel juurde panna, pole teada.  Või äkki tuleb hoopis olemasolevad moodulid välja vahetada?

Siin aitab vana hea WMI.  Nimelt annab selle käest küsida, et palju mälu kontroller üldse toetab ning palju emaplaadil pesasid on:

Get-WmiObject Win32_PhysicalMemoryArray

Siin tuleb muidugi teada, et atribuut MaxCapacity on kilobaitides. Seega võiks tulemust loetavuse huvides natuke parandada.

$MaxCapacity = @{name='Max memory (MB)'; expression={$_.MaxCapacity/1KB}}

Get-WmiObject Win32_PhysicalMemoryArray |
  Format-Table $MaxCapacity, MemoryDevices –AutoSize

Siit saime kätte maksimaalse mälu hulga ja pesade arvu, aga mis ühikutes mälu praegu masinas on?

Get-WmiObject Win32_PhysicalMemory

Jällegi oleks hea tulemus natuke loetavamaks teha:

$capacity = @{name="Capacity (MB)"; expression={$_.Capacity/1MB}}

Get-WmiObject Win32_PhysicalMemory |
  Format-Table DeviceLocator, $capacity, speed –AutoSize

Kõik ülaltoodu töötab lokaalses masinas, aga kui oleks vaja sama info saada kätte eemalt?  WMI liides on ka eemalt kättesaadav, vaja vaid läheneda kasutajana, kellel on lubatud WMI-liidese poole pöörduda:

$me = Get-Credential domain\kasutaja

Get-WmiObject Win32_PhysicalMemoryArray –ComputerName masin –Credential $me |
  Format-Table $MaxCapacity, MemoryDevices –AutoSize

Get-WmiObject Win32_PhysicalMemory –ComputerName masin –Credential $me |
  Format-Table DeviceLocator, $capacity, speed –AutoSize

Ainult et nüüd on meil probleem.  Iga Get-WmiObject käsk loob eemal olevasse masinasse ühenduse, saab info ja paneb ühenduse uuesti kinni.  Lisaks tuleb eraldi oodata iga masina taga, millega soovitakse ühendust saada.  Ja kui minu ning eemalasuva masina vahel on tulemüür, siis võib seal WMI jaoks vajalikud pordid kinni olla.  Ning nende lahtitegemine on natuke problemaatiline (WMI kasutab DCOM-liidest ehk dünaamilisi porte).

Antud situatsioonile on kaks võimalikku lahendust.  Esimeseks oleks kasutada Powershelli kaugühendusi.  Võrgutulemüürides on vaja avada vaid üks port (tcp/5985) ja sihtmasinas saaks ligipääsu lubada ka mitte-adminkontole:

#Requires -Version 2
$session = New-PSSession –Computername masin1, masin2 –Credential $me

Invoke-Command –Session $session {Get-WmiObject Win32_PhysicalMemoryArray} |
  Format-Table PSComputername, $MaxCapacity, MemoryDevices –AutoSize

Invoke-Command –Session $session { Get-WmiObject Win32_PhysicalMemory} |
  Format-Table PSComputername, DeviceLocator, $capacity, speed –AutoSize

Remove-PSSession $session

Windows 8/Server 2012 (ja värskemad) võimadavad aga veelgi lihtsamat lähenemist.  Nimelt on seal Powershelli sisse tehtud WMI alternatiivliides, mis samuti kasutab sessioone: CIM.  Sessioonid muide on samad, mis Powershelli kaugühenduse puhulgi.

#Requires -Version 3
#Requires -Modules CimCmdlets
$session = New-CimSession -ComputerName masin1, masin2 -Credential $me

Get-CimInstance Win32_PhysicalMemoryArray -CimSession $session |
  Select-Object  PsComputerName, $MaxCapacity, MemoryDevices |
  Format-Table –AutoSize

Get-CimInstance Win32_PhysicalMemory -CimSession $session |
  Sort-Object PsComputerName |
  Select-Object PsComputerName, DeviceLocator, $capacity, speed |
  Format-Table –AutoSize

Remove-CimSession $session

Üllatuste vältimiseks tasub veel ära mainida, et ülaltoodud tehnika abil saab kätte mitte ainult süsteemimälu, vaid võib leida ka muud, nagu näiteks videomälu jms.