I'm looking to write a Powershell function to assign a static IP address remotely to a computer specified at runtime. I've done some research and found the Win32_NetworkAdapterConfiguration class, which seems like exactly what I need. So I sat down and wrote myself a function:
Function Set-StaticIPAddress
{
param
(
[Parameter(Mandatory = $true,
Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)]
[String] $ComputerName
,
[Parameter(Mandatory = $true,
Position = 1)]
[Alias("IPv4Address")]
[String] $IPAddress
,
[Parameter(Position = 2)]
[String] $SubnetMask = "none"
,
[Parameter(Position = 3)]
[String] $DefaultGateway = "none"
,
[Parameter(Position = 4)]
[String[]] $DNSServers = ("172.16.1.36","172.16.1.78")
,
[Parameter(Position = 5)]
[PSCredential] $Credential
)
process
{
# There's some error-checking here that I've snipped out for convenience
Write-Verbose "Testing connection to $ComputerName"
if (-not (Test-Connection $ComputerName))
{
Write-Error "Unable to connect to $ComputerName."
return
}
Write-Verbose "Obtaining remote WMI reference"
if ($Credential)
{
$wmi = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Filter "IPEnabled = 'True'" -Credential $Credential
} else {
$wmi = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Filter "IPEnabled = 'True'"
}
Write-Verbose "Attempting to set DNS servers"
$wmi.SetDNSServerSearchOrder($DNSServers)
Write-Verbose "Attempting to set dynamic DNS registration"
$wmi.SetDynamicDNSRegistration($true)
Write-Verbose "Attempting to set static IP address and subnet mask"
$wmi.EnableStatic($IPAddress, $SubnetMask)
Clear-DnsClientCache #This may not be necessary; I added it as a troubleshooting step
Write-Verbose "Attempting to set default gateway"
$wmi.SetGateways($DefaultGateway, 1)
Write-Output $wmi
}
}
The trouble is, the EnableStatic method never seems to return a value - either a success or failure code. I tested this function on a machine sitting right next to me, and while the script was still waiting at the "Attempting to set static IP address and subnet mask" stage, I pulled up the configuration on the machine and found a static IP and subnet mask set. There was no default gateway (which makes sense, since I didn't get that far in the script). The computer in question didn't have network access, because the default gateway was missing.
I have also tried running the same commands from an interactive shell, and the same "freeze" happens on the same command:
$wmi.EnableStatic($IPAddress, $SubnetMask)
My best guess is that changing the network adapter configuration is breaking the remote WMI connection. Is there a way to make this work so I can script the assignment of a static IP address 100% remotely?
Edit: I MacGyvered another attempt which creates a ScriptBlock object and sends it to the remote computer using Invoke-Command. I had to do some interesting footwork to get an array of IP addresses to turn into a String literal, including the quotes, but I can now confirm that the script block has correct syntax and all that. Unfortunately, doing it this way causes my PS window to complain that the network connection has been lost (since the IP address has changed) and the script block does not complete successfully.
$wmiLiteral = '$wmi' # used so the script block actually creates and references a variable, $wmi
$script =
"$wmiLiteral = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter ""IPEnabled = 'True'"";
$wmiLiteral.EnableStatic(""$IPAddress"", ""$SubnetMask"");
$wmiLiteral.SetGateways(""$DefaultGateway"", 1);
$wmiLiteral.SetDNSServerSearchOrder($DNSServersList);
Write-Output $wmiLiteral"
Write-Verbose "Script block:`n-------------`n$script`n---------------"
$scriptBlock = [scriptblock]::Create($script)
Write-Verbose "Testing connection to $ComputerName"
if (-not (Test-Connection $ComputerName))
{
Write-Error "Unable to connect to $ComputerName."
return
}
Write-Verbose "Invoking scriptblock"
if ($Credential)
{
$output = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock -Credential $Credential
} else {
$output = Invoke-Command -ComputerName $ComputerName -ScriptBlock $scriptBlock
}
You might have more luck using Invoke-Command to run netsh on the remote computer, which sets the IP address, subnet mask, and gateway in one command. However, netsh uses a different name for the interface, which you can get from the NetConnectionID property of the Win32_NetworkAdapter class.
$InterfaceName = $Get-WmiObject Win32_NetworkAdapter | ?{$_.Description -eq $wmi.Description} | select -ExpandProperty NetConnectionID
Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock {netsh interface ip set address $InterfaceName static $IPAddress $SubnetMask $DefaultGateway 1}
You can wrap the second line in another if ($Credential) block.
However, I strongly recommend that you verify that the filter is returning one object rather than an array of objects, as suggested in my comments above. Or, to be safe, change
$wmi = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Filter "IPEnabled = 'True'" -Credential $Credential
to
$wmi = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $ComputerName -Filter "IPEnabled = 'True'" -Credential $Credential | select -First 1
That will ensure you're getting only one object, but of course it can't ensure that it's necessarily the one you want if there's more than one with IPEnabled true.
I would call your function in a script that use localHost as $computername parameter and then remote execute (invoke-command)this script on the remote computer, using powershell remote sessions (New-PSSession), or creating a remote PowerShell process with this script as parameter with WMI (in this case you have to copy the srcript on the remote compter).
You can also schedule this script on the remote computer ... the general idea is to not use the network during the operation.
Related
Quick one? - As I'm still learning Powershell. I was wondering if it was possible to combine parameters inside a script with parameters entered on the command line?
i.e. I have a function like this as an example...
function GetInfo {
param ($SiteName, $Subnet, $Cred, $ComputerName)
Write-Host "Checking $Site and $ComputerName"
<# ... Additional logic to check computername prefix & subnet etc. #>
}
$SiteName = "London1"
$Subnet= "192.168.10.1/24"
$Cred = <supplied>
#$ComputerName = "blah1"
GetInfo $SiteName $Subnet $Cred $ComputerName
$SiteName = "London2"
$Subnet= "192.168.11.1/24"
$Cred = <supplied>
#$ComputerName = "blah2"
GetInfo $SiteName $Subnet $Cred $ComputerName
Now say inside the script I would specify the SiteName, Subnet, Cred.... but on the command line I would like to specify -ComputerName
But as I'm using the script & let's say I know that Lon1-PC1 is in "London1" I would like to do this on the calling command:
.\GetPCInf.ps1 -ComputerName "Lon1-PC1"
or
.\GetPCInf.ps1 -ComputerName "Lon2-PC1"
Obviously there will be additional logic inside the script to say that if the -ComputerName prefix is "Lon1" then do X or if -ComputerName prefix is "Lon2" then do Y..
Obviously I know I can just put the computername in the script, save it & run it.
So far when I try, nothing happens in relation to the -Computername return..
I haven't yet tried combining a parameter & Args - but I've read Args is not the best to use so I'm trying to avoid it.
If this can't be done then fair enough, just wondered if someone might know if this can be done, as it saves me typing in:
.\GetPCInf.ps1 -SiteName London1 -Subnet "192.168.10.1/24" -Cred mycreds -ComputerName "Lon1-PC1"
each time I want to run it....
I suppose I could create batch files to call the script & put %1 for computername in the batch file & call it that way, but just curious really..
Many thanks.
So far when I try, nothing happens in relation to the -Computername return..
Scripts are just functions stored in files - and they support param blocks and parameter declarations just like functions - so declare a $ComputerName parameter:
# GetPCInf.ps1
param(
[Parameter(Mandatory = $true)]
[string]$ComputerName
)
# define GetInfo function
function GetInfo {
param ($SiteName, $Subnet, $Cred, $ComputerName)
Write-Host "Checking $Site and $ComputerName"
<# ... Additional logic to check computername prefix & subnet etc. #>
}
# define table of arguments to pass to GetInfo
$GetInfoArgs = #{
ComputerName = $ComputerName
}
# add additional arguments based on computer name value
if($ComputerName -like 'Lon1-*'){
$GetInfoArgs += #{
SiteName = "London1"
Subnet= "192.168.10.1/24"
Cred = $(<# credential goes here #>)
}
} elseif($ComputerName -like 'Lon2-*') {
$GetInfoArgs += #{
SiteName = "London2"
Subnet= "192.168.11.1/24"
Cred = $(<# credential goes here #>)
}
} else {
$GetInfoArgs += #{
SiteName = "DefaultSite"
Subnet= "192.168.12.1/24"
Cred = $(<# credential goes here #>)
}
}
# invoke GetInfo function
GetInfo #GetInfoArgs
My goal is to loop through all devices , stop a specific service for all of those devices ( in this case, IntenAudioService), then go kill speciifc tasks realted to that service ( let's just say task IntelX and task IntelY, if they exist)
Then just loop through again and re-start those services. can this be done all in 1 for loop? Is the syntax correct?
$devices= <<user can populate devices in this object. DeviceName or deviceID??>>
>Foreach ($device in $devices){
Invoke-Command -ComputerName $device {
net-stop IntelAudioService
taskkill /IM IntelX.exe /F
net start IntelAudioService
}
}
What if I wanted to also set a service for each device? Something like this?
foreach ($device in $devices){
Invoke-Command -ComputerName $device {
Set-Service -Name BITS -StartupType Automatic
}
}
Try with this, note that you can Invoke-Command to multiple hostnames at the same time. You can also create a New-PSession with multiple computers at the same time.
$ErrorActionPreference = 'Stop'
$devices = 'Hostname1','Hostname2'
$serviceName = 'IntelAudioService' # This can be an array
$processName = 'IntelX' # This can be an array
# Note: Looping through the devices an attempting to establish a
# PSSession like below is good if you're not sure if the remote host
# is up or if the device name is the right one, etc. Using a Try {} Catch {}
# statement in this case will let you know if you couldn't connect with a
# specific remote host and which one.
# You can also simply do: $session = New-PSSession $devices without
# any loop which will be a lot faster of course, however,
# if you fail to connect to one of the remote hosts
# you will get an error and the the PSSession cmdlet will stop.
$session = foreach($device in $devices)
{
try
{
New-PSSession $device
}
catch
{
Write-Warning $_
}
}
Invoke-Command -Session $session -ScriptBlock {
Get-Service $using:serviceName | Stop-Service -Force -Verbose
Get-Process $using:processName | Stop-Process -Force -Verbose
Start-Service $using:serviceName -Verbose
# Set-Service -Name $using:serviceName -StartupType Automatic
}
Remove-PSSession $session
I remotely try to set the DNS server addresses by, passing the Invoke-Command a string array of DNS server IP addresses as an argument.
This code only sets the first address (10.1.1.2)
$dnsIPCollection = #("10.1.1.2", "10.1.1.2")
Invoke-Command -ComputerName $serverNameHere -ScriptBlock { param($dnsIPCollection) Get-DnsClientServerAddress -InterfaceIndex 12 -AddressFamily IPv4 | Set-DnsClientServerAddress -ServerAddresses $dnsIPCollection } -Credential $creds -ArgumentList $dnsIPCollection
Now, if I try the following code, It sets both IP addresses
Invoke-Command -ComputerName $serverNameHere -ScriptBlock { Get-DnsClientServerAddress -InterfaceIndex 12 -AddressFamily IPv4 | Set-DnsClientServerAddress -ServerAddresses ("10.1.1.2", "10.1.1.2") } -Credential $creds
If I execute the following code from the remote server, it also sets both IP addresses
$dnsIPCollection = #("10.1.1.2", "10.1.1.2")
Set-DnsClientServerAddress -ServerAddresses $dnsIPCollection
I'm not sure if this is a bug or I'm doing something wrong. According to the ServerAddresses parameter documentation, the data type is String[].
Is anyone able to share some insight on my observations?
My $PSVersionTable output:
The problem is how you're passing your -ArgumentList $dnsIPCollection to Invoke-Command - you're expecting it to pass the whole array as a single argument, but PowerShell is exploding it out into a list of arguments.
It's basically doing this:
PS> $myArgs = #("10.1.1.x", "10.1.1.y")
PS> Invoke-Command -ScriptBlock { param( $p1, $p2 )
write-host "p1 = '$p1'"
write-host "p2 = '$p2'"
} -ArgumentList $myArgs
which gives the output
p1 = '10.1.1.x'
p2 = '10.1.1.y'
Except that your script block only declares one parameter so it only receives the first value in the array of arguments - you're effectively only passing $p1 into Set-DnsClientServerAddress.
If you want your array to be passed into your script block as a single argument then you need to wrap it in another array so that your array becomes $p1 when PowerShell explodes the -ArgumentList:
$myArgs = #("10.1.1.x", "10.1.1.y")
Invoke-Command -ScriptBlock { param( $p1 )
write-host "p1 = '$p1'"
} -ArgumentList #(, $myArgs )
This now outputs:
p1 = '10.1.1.x 10.1.1.y'
Hopefully that helps - I'll leave applying this back into your original question as an exercise for the reader, but leave a comment if you get stuck doing it :-).
Update
Just to labour the point, the reason PowerShell explodes the argument list might be more obvious with an example with meaningful variable names:
PS> Invoke-Command -ScriptBlock { param( $username, $password )
write-host "username = '$username'"
write-host "password = '$password'"
} -ArgumentList #( "myUsername", "myPassword" )
Your example where only one IP address is being set is pretty much the same as this:
PS> Invoke-Command -ScriptBlock { param( $username )
write-host "username = '$username'"
} -ArgumentList #( "myUsername", "myPassword" )
And then it should be easy to see why your second IP address isn't being set - your array of IP addresses is being treated as a list of arguments rather than a single array argument...
I want to create a PowerShell script which will disable the windows account, the target host name will be provided as an argument. Only admin should be able to execute this task.
This is what I have tried. Could someone please tell me if this approach is right or is there any better way to do this.
param( [Parameter(Mandatory=$true)] [String] $TargetHost ,
[Parameter(Mandatory=$true)] [String] $TargetUserName ,
[String] $User ,
[String] $Password)
# Set up a trap to properly exit on terminating exceptions
trap [Exception] {
write-error $("TRAPPED: " + $_)
exit 1
}
function DeactivateAccount($TargetHost , $TargetUserName ,$User , $Password){
$TargetHost = $TargetHost #Target Host on which windows account deactivation will be done.
$TargetUserName = $TargetUserName #User Name of Target.
$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() #Domain name of the localhost.
$localHost = [System.Net.Dns]::GetHostName()
$localIP = [System.Net.Dns]::GetHostAddresses("$localHost")
#if TargetHost and LocalHost are same.
if($localHost -like $TargetHost -OR $localIP -like $TargetHost) {
if($Domain -eq [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()){
$process = net user $TargetUsername /domain /active:no #Performs the operation on the domain controller in the computer's primary domain.
} else {
$process = net user $TargetUsername /active:no
}
Write-host " $TargetUsername account deactivated "
}
#If TargetHost is remote Host.
else {
$User = $User #Creds to perform admin function.
$Password = $Password
$SecurePassword = new-Object System.Security.SecureString #Convert password into secure string.
$Password.ToCharArray() | % { $SecurePassword.AppendChar($_) }
$Cred = New-Object -typename System.Management.Automation.PSCredential -argumentlist "$User",$securePassword
$newSession = New-PSSession -ComputerName "$TargetHost" -credential $Cred #Used PSSession for persistent connection and credentials to Specify a user account that has permission to perform this action.
$export_username = Invoke-Command -Session $newSession -ScriptBlock {$username=args[1]} # Invoke-Command command uses the Session parameter(here newSession) to run the commands in same session.
if($Domain -eq [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()){
$process = Invoke-Command -Session $newSession -ScriptBlock {net user $username /domain /active:no}
} else {
$process = Invoke-Command -Session $newSession -ScriptBlock {net user $username /active:no}
}
Write-host " $TargetUsername account deactivated "
Remove-PSSession $newSession # Closes Windows PowerShell sessions.
}
if(-not $?) { # Returns true if last command was successful.
Write-Error "Windows Deactivation Failed!!"
exit 1
}
}
DeactivateAccount($TargetHost , $TargetUserName ,$User , $Password)
Couple of things:
Your meant to show some code to show you tried but since you're new to Powershell I'll let that slide :)
Is it a local windows account you are trying to disable or an AD one? For the purpose of this I'll assume local.
Grab this module: https://gallery.technet.microsoft.com/PowerShell-Module-to-255637a3
The dude basically made a module for exactly what you want to do :)
Note: If you have Powershell 5.1+ you won't need the module they added new cmdlets to do this natively.
Credential-wise I wouldn't worry, Powershell can't bypass windows security, it will execute with the permissions of the user that ran the script unless your script specifically gives credentials for another user in the commands.
Let me know how you get on.
WMI can do it, but I have an issue, PCs are on, but logged off. If I try to run:
wmic /node:%strIP% printer where DeviceID="lp1" set DriverName="Lexmark Universal v2"
It fails with a message about a "generic failure". I RDP in and then run the same command from my end, and it works. Powershell version I am using is older, so it does not have some of the printer cmdlets, and updating PS is currently out of the question. Is there a way to remotely log someone in, without actually having to RDP in? Via PS, cmd, PSEXEC, etc?
The other avenue I've taken is using regedit, but I'm hitting some hicups with that, namely that I cannot figure out what to copy. In regedit, I can change the drivername and the setting that enable duplex and tray2 (in printer settings), but I cannot figure how to change the settings in printer preferences for printing double sided and doing so along the long edge.
What I did to figure out what to change, I did a find on the printer name in regedit as a data value and exported the keys before changing the settings. Then I exported it again AFTER changing the settings. I then used fc /c /a /u before.reg after.reg to get the changes. I chopped up the .reg to include only the changed values. Running the .reg seems to change everything, but the print both sides, along the long edge settings. It is a lexmark printer, so I am wondering if maybe preferences for it are stored elsewhere.
This is my most up to date PS1 script. I've commented out some lines as I tried different ways of doing things:
$Cred = Get-Credential
$Str = Read-Host "Please select a site ID [###] "
$PC = Read-Host "Please select a PC number [##] "
Clear-Host
$PCNm = "$Str-CCPC-$PC"
function Test-PsRemoting
{
try
{
$errorActionPreference = "Stop"
$result = Invoke-Command -ComputerName $PCNm { 1 }
}
catch
{
Write-Verbose $_
return $false
}
if($result -ne 1)
{
Write-Verbose "Remoting to $PCNm returned an unexpected result."
return $false
}
$true
}
If(!(Test-PsRemoting)){
PSEXEC \\$PCNm powershell Enable-PSRemoting -force 2>&1 >nul
Clear-Host
Write-Host "Enabled PsRemoting"
}else{Write-Host "PsRemoting already enabled"}
Invoke-Command -ComputerName $PCNm -Credential $Cred -ScriptBlock {
#$lp1 = Get-WMIObject -Query "SELECT * from Win32_Printer Where DeviceID='lp1'"
$lp1 = Get-WmiObject Win32_Printer | ?{$_.name -eq "lp1"}
$lp1.Scope.Options.EnablePrivileges = $true
$lp1.DriverName = "Lexmark Universal v2"
$lp1R = $lp1.Put()
#$lp2 = Get-WMIObject -Query "SELECT * from Win32_Printer Where DeviceID='lp2'"
$lp2 = Get-WmiObject Win32_Printer | ?{$_.name -eq "lp2"}
$lp2.Scope.Options.EnablePrivileges = $true
$lp2.DriverName = "Lexmark Universal v2"
$lp2R = $lp2.Put()
}
#$lp1 = Get-WMIObject -Impersonation Delegate -Authentication Call -Credential $Cred -ComputerName $PCNm -Query "SELECT * from Win32_Printer Where DeviceID='lp1'"
#$lp1.DriverName = "Lexmark Universal v2"
#$lp1.Put()
No matter which way I try it, invoke-command, or get-wmiobject, I get:
Exception calling "Put" with "0" argument(s): "Generic failure "
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
+ PSComputerName : 150-CCPC-02
This doesn't particularly answer your actual question but as a solution for how I do this very same thing I thought I would give you what I threw together to update printer properties. I have not cleaned this up at all as I was porting it from my create printer function.
Function Set-SSPrinter {
Param(
[Parameter(Mandatory=$true,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[string]$PortName,
[string]$DriverName,
[string]$Comment,
[string]$Location,
[bool]$Shared,
[string]$ShareName = $Name,
[string]$PermissionSDDL,
[string]$PrintProcessor,
[string]$DataType,
[bool]$RawOnly
)
try {
$modprinter = Get-WmiObject Win32_Printer -ComputerName $ComputerName | ?{$_.name -eq $Name}
$modprinter.Scope.Options.EnablePrivileges = $true
if($DriverName) {
$modprinter.DriverName = $DriverName
}
if($PortName) {
$modprinter.PortName = $PortName
}
if($Shared) {
$modprinter.Shared = $Shared
}
if($ShareName) {
$modprinter.ShareName = $ShareName
}
if($Location) {
$modprinter.Location = $Location
}
if($Comment) {
$modprinter.Comment = $Comment
}
if($Name) {
$modprinter.DeviceID = $Name
}
if($PrintProcessor) {
$modprinter.PrintProcessor = $PrintProcessor
}
if($DataType) {
$modprinter.PrintJobDataType = $DataType
}
if($RawOnly) {
$modprinter.RawOnly = $RawOnly
}
$result = $modprinter.Put()
if($PermissionSDDL) {
$modprinter.SetSecurityDescriptor($objHelper.SDDLToWin32SD($PermissionSDDL).Descriptor) | Out-Null
}
$("Update Complete: " + $Name)
} catch {
$("Update Failed: " + $Name)
Write-Warning $_.Exception.Message
$error.Clear()
}
}
Unfortunately I use the printer name to figure out which device to modify on the remote machine. Your executing credentials from the powershell session you have open must have admin rights on the remote machine. if necessary do a runas different user on powershell.exe
Example usage:
Set-SSPrinter -ComputerName "10.210.20.100" -Name "TestPrinter" -DriverName "Lexmark Universal v2"
wmic /node:servername /user:username /password:password path win32_something call methodname
Is how to do it.
Things with users are best done with logon scripts because that is how windows is designed.