Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
nielsm
Jun 1, 2009



If you can Invoke-Command to a remote system, you can just pass plain PowerShell code in a script block without having to wrap it in another call to powershell.exe like that.
Invoke-Command uses WinRM to execute PowerShell directly on the target machine, unlike PSExec which installs a temporary service to start an arbitrary process on the target.

Also, if you can Invoke-Command on the target machine, you don't really need to change execution policy at all, because you're already executing PowerShell code on the machine at that point.


So instead of this:
code:
Invoke-Command -cn $computer -scriptblock { powershell -executionpolicy bypass -file myscript.ps1 }
or even this:
code:
Invoke-Command -cn $computer -scriptblock {
    set-executionpolicy bypass -scope process -force
    myscript.ps1
}
just do this:
code:
Invoke-Command -cn $computer -scriptblock {
    # paste contents of myscript.ps1 here
}

nielsm fucked around with this message at 22:27 on Jul 18, 2016

Adbot
ADBOT LOVES YOU

nielsm
Jun 1, 2009



Invoke-Command can also take a -FilePath argument instead of -ScriptBlock.
When you use -FilePath, the path is local and not remote, so you don't have to copy the script over.

Do get used to reading the manual pages. They tend to be quite detailed especially for the core commands: https://technet.microsoft.com/library/hh849719.aspx

nielsm
Jun 1, 2009



Funny reasoning, because WinRM is far more secure than PSExec. Or are you just copying files over than walking up to each workstation?

nielsm
Jun 1, 2009



Get-ADUser grabs very little data by default. If you need more than the limited default set, use the -Properties parameter to specify what fields you want. Or just use "-Properties *" to get everything.

nielsm
Jun 1, 2009



BaseballPCHiker posted:

Do you have WinRM configured? Have you verified you have the rights for that PC?

PSExec doesn't use WinRM.

nielsm
Jun 1, 2009



Specifically, for PSExec to work, you must have a user account that can connect to \\COMPUTER\ADMIN$ and write files there.
You also need to be able to run "sc \\COMPUTER create <parameters>" to control the service control manager on it.
PSExec uses those remote management tools to do its task.

nielsm
Jun 1, 2009



I believe PowerShell loads the full script, then closes the script file, letting you delete the file without problems, even in the middle of execution.

DOS-derived batch files traditionally execute line by line, with the result that you could, at least under true DOS, have a batch file that modifies itself while running. I don't know how cmd.exe does things, but you should be able to delete a running batch file, just don't expect it to be able to continue running afterwards.

nielsm
Jun 1, 2009



If you wrap your task with a function, or even a module, and then have a small runner script that invokes it, you can use standard redirection on that.
Make sure to read Get-Help about_Redirection.

nielsm
Jun 1, 2009



22 Eargesplitten posted:

Now my question is how to find registry keys by value. I've got server entries in the registry, so I want to be able to search for a particular value and replace all instances of that value with the new server name. The Google searches I have done are only bringing up searches for the key name, not the value.

First, do you actually mean keys, or do you actually mean values?
Keep in mind that the registry has this odd structure where the "folders" are called keys, and each key has a number of values. A value has a name, a datatype, and data. The values are the "files".
(Historically, there was only one value per key, the one now represented with a blank/null name, shown as (Default) in regedit.)

Anyway, first figure out if you're looking for keys, or for key+value name. And whether you're searching by value data or something else.

When you use Get-ChildItem in a registry provider tree in PowerShell (e.g. HKLM:\ ) then you only get the keys out as objects, the values are accessed roundabout through the keys.

You can do something like this, at least:
code:
Get-ChildItem HKLM:\SOFTWARE\Classes\ |
  where { $_.GetValue("PerceivedType") -eq "video" }
If you don't know the value name it gets much more annoying. Microsoft.Win32.RegistryKey objects in PowerShell just get a NoteProperty called "Property" that contains a string array of value names, so you need to call GetValue for each value name you want to test something against.

nielsm
Jun 1, 2009



22 Eargesplitten posted:

I guess I'm looking for the value data then? My lead admin calls the values keys, if I'm understanding you right. He calls the individual items you edit keys. I'm not surprised, though. Everyone here calls display port "Dell DVI" and it drives me crazy.

It's 5PM on a Friday, so I'm not quite sure what you're saying at the end there. That stuff you wrote wouldn't find the data (in this case a server name that a program connects to) in a value, right?

My example searches for keys ("folders") that have a value named "PerceivedType" with a data contents of "video".
E.g. for this, it would find the "HKLM:\SOFTWARE\Classes\.MKV" item:

nielsm
Jun 1, 2009



beepsandboops posted:

I want to shoot off a command to our on-prem Exchange and Lync servers as part of our new user script.

Right now, I'm using Import-PSSession for each server then running the enable user command, but that seems to have a lot of overhead if I just want to run just the one command.

Invoke-Command doesn't seem to be able to use the product-specific commandlets. Is there a better way to run just one Exchange/Lync command remotely?

I can definitely use Invoke-Command to run individual cmdlets on Exchange, without importing the session or making a module from it. I just connect the session with New-PSSession, usually using -Name to set a name for the session so I can easily grab it later with Get-PSSession.
(Actually I have a custom Get-ExchangeSession function that tries to get the named session, and if that fails then it establishes one with that name and returns it.)

Then just store the session reference into a variable and pass that for -Session on Invoke-Command.

I can post an example tomorrow when I'm at work.

nielsm
Jun 1, 2009



Basic example of doing Exchange management via Invoke-Command instead of an imported PSSession:
code:
Function Get-ExchangeSession {
    $exchange = Get-PSSession -Name "Exchange" -ErrorAction SilentlyContinue
    if (-not $?) {
        Write-Host "Establishing Exchange session..."
        $exchange = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://exchange.contoso.com/PowerShell" -name "Exchange"
    }
    return $exchange
}

Function Set-MailboxFullAccess($sharedMailbox, $user) {
    $exchange = Get-ExchangeSession
    Invoke-Command -Session $exchange { param($id,$u) Add-MailboxPermission -Identity $id -User $u -AccessRights "FullAccess" } -ArgumentList $sharedMailbox,$user
}

nielsm
Jun 1, 2009



milk milk lemonade posted:

I'm writing a password reset script and I figured out how to get it to say "Sorry! That user doesn't exist" but I want it to return to the start of the script when that happens. Is there an easy way to do this? I've looked at a few things like erroraction but I don't think this applies when I do GetADUser and want it to either display the results or say "whoops!". If I need to start over and look at it from another angle I'd appreciate any tips on where to start!

Look into how classic text-mode menu systems have been written.

The typical approach is to have an "input loop" for each "menu" or other input-step, that will take input from user, validate it, and only exit the loop when a valid input has been received.

Roughly, untested and maybe incorrect:
code:
$inputValid = $false
while (-not $inputValid) {
  $input = Read-Host "Username to reset"
  $foundUser = Get-ADUser $input -ErrorAction SilentlyContinue
  if ($?) {
    $inputValid = $true
    Write-Host "Found user: $($foundUser.Name)"
  }
  else {
    Write-Host "No user found."
    $otherUsers = Get-ADUser -Filter { Name -like "*$($input)*" }
    if ($otherUsers) {
      Write-Host "Did you perhaps mean one of:"
      $otherUsers | Format-Table Name,samAccountName,DistinguishedName
    }
  }
}

nielsm
Jun 1, 2009



Frohike999 posted:

Ok, I'm hoping one of you can help with this one. I'm trying to write a script that will recursively go through a folder and rename all the files in it. That part I'm ok with. The problem I'm having is how to actually do this rename. The files all have the format of "abc1234.txt'. I want to keep the abc prefix and the .txt, but I want to add 100000 to the number in the middle. It's not necessarily always a 4 digit number in the middle, it could be abc1.txt or abc100001.txt. I'm going to keep working on this at work this morning, but if anyone has any insight I'd really appreciate it.

A regular expression may work to extract things.

code:
  if ($filename -match "(abc)(\d+)(\.txt)") {
    $newfilename = $Matches[1] + ([int]$Matches[2] + 1000000) + $Matches[3]
  }
$Matches is a magical variable that gets set when you use a regular expression operator such as -match, to contain the matched subexpressions.

pre:
PS> "abc123.txt" -match "(abc)(\d+)(\.txt)"
True

PS> $Matches

Name                           Value                                                                                                                          
----                           -----                                                                                                                          
3                              .txt                                                                                                                           
2                              123                                                                                                                            
1                              abc                                                                                                                            
0                              abc123.txt                                                                                                                     

PS> $Matches[1] + ([int]$Matches[2] + 1000000) + $Matches[3]
abc1000123.txt

nielsm
Jun 1, 2009



The Right thing to do would be wrapping the tasks you want to do in a Powershell module, then write a C# app calling that module through the Windows.Automation namespace.

nielsm
Jun 1, 2009



GPF posted:

You've got me all hot and bothered now. Could you drop an example? Here's a simple one: GUI is box with button and display area. Button fires off module that does a Get-Service on the local system. Returned list is dropped into the display box.

Daddy needs a good Xmas present.

Here's a stupidly simple WPF app that lists services

I don't know if the project files will work on any other machine, I picked a random System.Management.Automation.dll inside WinSxS for the reference.

nielsm
Jun 1, 2009



On the other hand, I think if you need to make pipelines, things become much more complex. Which is why you definitely want to pack all your logic into a module beforehand, so you only need a thin layer of glue in the GUI.

You can also set up "runspaces" in the programming model to do remoting directly. Unfortunately, it seems the security aspects of remoting can be difficult, at least if you want to target Exchange. I experimented with setting that up once, and never found something that worked.

nielsm
Jun 1, 2009



cheese-cube posted:

What security aspects of Exchange remoting did you find difficult?

I can make it work just fine against our on-prem Exchange through regular Powershell.

But when i tried establishing a runspace with the same parameters via the API in C#, it failed connecting in various ways. I can't remember the details, but there's no HTTPS configured or the cert is invalid, and it refuses to let me trust it regardless, giving various odd errors. Sometimes even claiming the server name not existing despite DNS working perfectly fine everywhere else.

nielsm
Jun 1, 2009



Likely since that allows them to guarantee the algorithmic complexity. When both inputs are sorted on the same criteria, the algorithm needs to do at most M+N comparisons, and the inputs can even be forward-only iterators or generators instead of being random access lists. So from a CS standpoint, it makes good sense.

nielsm
Jun 1, 2009



Avenging_Mikon posted:

:aaaaa:

Holy poo poo, how have I never made that connection?

And similar, HKLM = HKEY_LOCAL_MACHINE, HKCR = HKEY_CLASSES_ROOT (which it's important to remember is not a real key, but a combined view of HKLM and HKCU subkeys Software\Classes).

nielsm
Jun 1, 2009



No idea about that, but I've always just used the + operator to assemble strings when needed. Or string interpolation to glue contents of variables into strings.

code:
"abc" + "def"
"Free space: $spaceFree GB"

nielsm
Jun 1, 2009



I can't remember the terms to search for here, so can't find an answer...

I have a bunch of WMI objects I want to pass to Remove-WmiObject -Confirm, but the identity displayed for each is useless to the operator. Is there a way to override the "short display string" or whatever it's called for an object?

code:
$profiles = Get-WmiObject -Class Win32_UserProfile -ComputerName $target
# filter $profiles here
$profiles | Remove-WmiObject -Confirm
Gives a confirmation prompt like this:
pre:
Confirm
Are you sure you want to perform this action?
Performing the operation "Remove-WmiObject" on target "\\machine\root\cimv2:Win32_UserProfile.SID="S-1-5-18"".
Which is rather useless for identifying what is actually affected. (This example is the local system account, should probably not delete.) I'd rather display the LocalPath property or a custom NoteProperty of the object.

nielsm
Jun 1, 2009



PBS posted:

That output is the result of what is being passed to the method used for confirmation. Without changing the target yourself, I don't think you can change what's displayed for the confirmation without modifying those cmdlettes.

The easiest solution is probably to iterate though the objects and write information about the object before the confirmation dialog appears.

I think it uses obj.ToString() to get the identity shown, but really no idea. I'm not sure if it's even documented.
The problem with manually iterating over the objects is that you can very easily break "Yes to All" and "No to All" which are important to have in this use case.

But I think maybe writing a cmdlet of my own that just wraps Remove-WmiObject but somehow causes a more useful object identity to be printed, could work.

nielsm
Jun 1, 2009



Yes, Get-ADGroupMember returns you sparsely populated generic AD objects, since groups can contain just about any kind of object as member. If you know only users are members of the group, you can safely pass the result of that straight into Get-ADUser to get all the details, optionally passing -Properties to Get-ADUser to get more than the default property set. For example:
code:
Get-ADGroupMember -Recursive -Identity "VPN Users" | Get-ADUser -Properties Department,Title | Select Name, Department, Title, Enabled | Export-Csv "vpnusers.csv" -NoTypeInformation
Also please don't shout at PowerShell. It responds just as well to politely typed CamelCase. ;)

nielsm
Jun 1, 2009



Avenging_Mikon posted:

whoops, didn't even notice that part. Well, I'm taking what I've learned and am now making a script to disable AD accounts and update their description with the date they were disabled.

I've got everything set except I've forgotten how to pipe a file in to anything. And apparently get-file isn't recognized in the Active Directory Module. I'd really like to be able to point this at a file of user names and just have it go nuts, and then only need to change the file each time.

Plain text: Get-Content -Path "c:\some\file.txt" -Encoding UTF8

CSV: Import-Csv -Path "C:\some\file.csv" -Header @("UserName","FirstName","LastName","Email")

nielsm
Jun 1, 2009



Collateral Damage posted:

Dumb question time: Some times when concatenating variables I see people use $($variable) and some times just $variable. What's the difference?

The former lets you concatenate with variable-name-valid characters right of the name.

E.g.: "Free space: $(variable)MB"
If you wrote "$variableMB" it would look for a variable actually named variableMB instead.

Actually, what it really does is let you put entire expressions inside an interpolated string, so you can do things like:
"Free space: $($freeSpace/$totalSpace*100)%"

nielsm
Jun 1, 2009



I certainly think the parentheses makes it easier to pick out when reading code without syntax highlighting, too.

nielsm
Jun 1, 2009



You're not passing the credential you prompt for to the Exchange session, so it gets logged in with the account you're running PowerShell under (probably your regular domain account).
If you need to use a different login to get administrative access, make sure you're actually passing $credentials to New-PSSession.

nielsm
Jun 1, 2009



Can't test right now, but basically something like -Filter "accountExpires -lt '2017-07-05'" should work.

nielsm
Jun 1, 2009



cheese-cube posted:

Also what version of PS are you using? Can you just use the native Get-Acl and Set-Acl cmdlets?

I think the point is that constructing an ACL manually for use with Set-Acl is somewhat difficult.

nielsm
Jun 1, 2009



Avenging_Mikon posted:

2. I would like the script to ask me for the User's ID, and the ticket number.

The best approach (IMO) is to declare these as parameters, that way you can also call your script as part of another script, with the "host script" supplying the values instead of being locked in to interactive prompting.

I assume your script is a .ps1 file you run by itself. Put this block at the very top:
code:
param(
  $UserID = $null,
  $TicketID = $null
)

if (-not $UserID) {
  $userID = Read-Host "Enter user ID to disable"
}

if (-not $TicketID) {
  $ticketID = Read-Host "Enter work order ticket ID"
}
This declares that your script takes two parameters named -UserID and -TicketID (when you call it), and the code following the param() block checks if either value is falsy (e.g. null or empty) and prompts for a value from the user. The = $null part in the declaration says that if you don't specify the parameter, it gets a default value of $null.
You can do a lot more with parameters, but this is the most basic. Read up on the param() block as well as on the Read-Host cmdlet.

This way you can both call your script as "interactive" (e.g. right-click in File Explorer and choose Run, or click the Run button in the ISE), or you can call it with parameters from the prompt, like:
.\DisableUser.ps1 -UserID fred -TicketID SDE0023523

nielsm fucked around with this message at 21:22 on Aug 29, 2017

nielsm
Jun 1, 2009



I'm wondering if there are some idioms or syntaxes I'm missing, working with the regular MS ActiveDirectory module.

Say I have a list of user names, and a list of group names. I want to know what users are not in each group, and I want to simply add the missing users. Using Add-ADGroupMember with -Members @("user1", "user2") fails if even a single of the users is already member of the group, and making a loop (ForEach-Object) over the users is awkward and seems backward. Is there a better way?

Is there a good way to present a table (matrix) of AD user objects (with MemberOf property fetched) with some select groups as columns, containing member true/false flags? One that doesn't involve a loop and Add-Member (or creating PSCustomObjects).

nielsm
Jun 1, 2009



Or splat it:
code:
$taskArgs = @{
    TaskName = "TestRail Background Task"
    Trigger = (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-Timespan -Minutes 1) -RepetitionDuration ([System.TimeSpan]::MaxValue))
    Action = (New-ScheduledTaskAction -Execute "C:\Program Files (x86)\PHP\v5.6\php.exe" -Argument "E:\TestRail\task.php")
    Settings = (New-ScheduledTaskSettingsSet -AsJob -ExecutionTimeLimit 1h -MultipleInstances StopExisting)
}
Register-ScheduledTask @taskArgs

nielsm
Jun 1, 2009



I'm using New-ADGroup -WhatIf regularly as part of a script, works fine.

nielsm
Jun 1, 2009



Volume shadow copy on the file server gives you instant-to-take snapshots of the entire file system, and lets you/users restore files directly from File Explorer. It's the best thing ever for getting out of "oops data gone" situations.
It's called Previous Versions in File Explorer.

nielsm
Jun 1, 2009



https://mcpmag.com/articles/2013/08/20/powershell-name-duplicates.aspx talks about exactly that problem, you can specify a prefix in Import-Module that gets appended to the noun part:

Import-Module Hyper-V -Prefix Hv
Import-Module VMware.VimAutomation.Core -Prefix Vmw

That should get you commands named Get-HvVMHost and Get-VmwVMHost instead.

^^^ but that module "namespace" prefix sounds like a perhaps better solution.

nielsm
Jun 1, 2009



Implement your GUI as a .NET class library and import that into Powershell.

nielsm
Jun 1, 2009



Zaepho posted:

Powershell only allows you to do things you already have rights to do. Also, it essentially encapsulates .Net so to really block things you'd have to block .Net. Which of course you would never do because it's pretty much essential.

This. If PowerShell lets someone do a thing they should not have permissions to do, it's not PowerShell that's at fault. The permissions on the affected thing were set up were wrong to begin with.

(Also, fun fact: A base installation of the .NET framework includes a full functioning C# compiler, C:\Windows\Microsoft.NET\Framework\version\csc.exe. Anyone who can create files and run arbitrary programs can use that to compile their own code and do anything PS could be used for.)

nielsm
Jun 1, 2009



I'm not sure any filesystem supports prepending data to a file without rewriting the entire file.

What the above code does is turn this:
1234567890
into this:
x234567890

Same length, just the first byte replaced.

When you say "preappending" I think of getting this result instead:
x1234567890

That always requires reading the entire file. You won't need to read it all into memory at once, but you will need to read it all off disk and write it all back. (In Unix you can open() the original file, unlink() the name from the inode while keeping the file open, open() the filename again creating a new file, write the data to prepend, then read blocks of the original file and write those to the new file until EOF. When done, close both original files, and the original file really disappears because there's no more links to the inode in form of either names or file handles. I'm not sure if Windows supports something exactly equivalent.)

Adbot
ADBOT LOVES YOU

nielsm
Jun 1, 2009



Only &lt; &gt; &amp; are "core" to XML, anything else technically has to come from a DTD or other external source. There's nothing wrong with leaving a character unencoded if the meaning is unambigious.

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply