I am writing this function to ask a user what VM they want to use, they will have to choose from their own Hyper-V list. After that, it will then ask if I want it on or off. I can get the selected VM to turn on or off when I select it. However, when I run the function it will try to go through all the VM's in Hyper-V. How can I get it so it can just turn on or off the VM I want?
(I am also going to go back and add an option to save the VM if it is on.)
Function get-ORS {
param ($R, $O, $S, $name, $off, $on)
$name= Read-Host "What VM would you like to turn on use?"
$IO= Read-Host "Would you like to turn on/off your VM?: [On] [Off]"
$R= Get-VM | where {$_.State -eq 'Running'}
$O= Get-VM | where {$_.State -eq 'Off'}
$S= Get-VM | Where {$_.State -eq 'saved'}
$Off= Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on= Get-VM | where {$_.State -eq 'Off'} | Start-VM
if ($IO -eq $on) {
try
{
Start-VM -Name $name
}
catch
{
echo ("Could not start" + $name)
}
}
elseif ($O){
try
{
Start-VM -Name $name
echo ("Starting "+ $name)
}
catch
{
echo "Could not start the VM."
}
}
elseif ($R){
try
{
echo "Your VM is currently running."
}
catch
{
echo "Something went wrong."
}
}
elseif ($S) {
try
{
Start-VM -Name $name
echo ("Starting your from a saved state " + $name)
}
catch
{
echo "Could not start the saved VM."
}
}
if ($IO -eq $off){
try
{
Stop-VM -Name $name
}
catch
{
echo ("Could not turn off " + $name)
}
}
}
There is a lot wrong with this function / script. Especially regarding the logic (use case) and construct side of this effort. It's really a Frankenstein this, meaning, maybe, you are copying and pasting from different sources. The formatting makes this hard to follow as well.
You really need to step back an approach this one use case at a time to make sure you are going down the right path.
You have params, then more prompts for the same information. You have a variable in an if statement that is not in your param or prompt section so it will always be empty.
You are specifically asking for all VMs, ...
$R = Get-VM | where {$_.State -eq 'Running'}
$O = Get-VM | where {$_.State -eq 'Off'}
$S = Get-VM | Where {$_.State -eq 'saved'}
$Off = Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on = Get-VM | where {$_.State -eq 'Off'} | Start-VM
... but you are saying you only want one, but in your prompts you are asking for two.
$name = Read-Host "What VM would you like to turn on use?"
$IO = Read-Host "Would you like to turn on/off your VM?: [On] [Off]"
You also have these as params, so that's just odd. Only put in the param block what you want to pass or prompt for.
Why do you have a pram block, then have a bunch of Read-Host blocks?
That is redundant. You can do the Read-Host in the Param block as well. So, that you can pass the params all at once, or if one forgets to, prompts occur.
param
(
$name = (Read-Host "What VM would you like to turn on use?"),
$IO = (Read-Host "Would you like to turn on/off your VM?: [On] [Off]")
)
$R = Get-VM | where {$_.State -eq 'Running'}
$O = Get-VM | where {$_.State -eq 'Off'}
$S = Get-VM | Where {$_.State -eq 'saved'}
$Off = Get-VM | where {$_.state -eq 'Running'} | Stop-VM
$on = Get-VM | where {$_.State -eq 'Off'} | Start-VM
You can also, use a validate set to ensure the user only can select an option vs having then type it in.
You can also just show a list of VM's for the use to select from, by using dynamic parameter set or using Out-GridView for the user to select from.
You have a very long if statement which just a candidate for a switch statement to make it more concise.
You are passing all your guest names in your if statements. Why, when you say you only want one.
This comes off as this is all new to you. Which is fine, but t is worth taking a few quick free online session before jumping in. If that is the case then, see these discussions and Q & A's.
See these resources.
PowerShell Resources
Helping out someone with PowerShell
And this...
MS PowerShell Docs
IMHO, you should approach this a different way to make it more concise, easier to understand, extend, maintain and troubleshoot.
What you have could really be reduced to just this, from what you show now. Again, there are many ways to do X or Y, this is just one way.
Function Get-Ors
{
[cmdletbinding()]
[Alias('ors')]
param
(
)
$VMName = ( Get-VM |
Select-Object -Property Name, State, Status |
Out-GridView -OutputMode Single -Title 'Select a target VM you want to use.')
$IO = ( 'On','Off'|
Out-GridView -OutputMode Single -Title 'Select On or Off to start or stop the target VM.')
switch ($IO)
{
On {
"Attempting to start $($VMName.Name)"
Start-VM -Name $VMName
}
Off {
"Attempting to stop $($VMName.Name)"
Stop-VM -Name $VMName.Name
}
default {Write-Warning -Message "$($VMName.Name) state could not be determined"}
}
}
PowerShell will auto generate a response based on current state if not valid for the request.
If you don't care for the built-in Out-GridView, you could construct and use messagebox or WinForm or WPF or use PowerShell other PoorMan's GUI approach, using the Show-Command cmdlet. The PoorMan's GUI approach is not customizable.
Related
I created an advanced function to get the mac address from a VM running on VMware ESXi.
function Get-MacFromVm {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
# The name of the VM of which we want to obtain the mac address.
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[string[]]
$Name
)
Begin {}
Process {
foreach ($item in $Name) {
if ($PSCmdlet.ShouldProcess($item, "Getting the mac address")) {
Get-VM $item -PipelineVariable vm |
Get-NetworkAdapter |
Select-Object #{n="Name"; e={$vm.Name}},
#{n="ClientId"; e={$_.MacAddress -replace ":","-"}}
}
}
}
End {}
}
So far everything works perfect.
I can use it in any of the following ways and get results back.
It accepts either a single or array of string via the named parameter or as pipeline input.
Get-MacFromVm -Name "playground"
Get-MacFromVm -Name "playground", "DC01"
"playground", "DC01" | Get-MacFromVm
The output is a [PSCustomObject] with 2 properties, a Name and the ClientId.
Now the problem starts when I want to chain the result to multiple other cmdlets by using the -PipelineVariable parameter.
Normally I should be able to use it like this:
Get-MacFromVm -Name "playground" -PipelineVariable pv | % {$pv}
But it doesn't show me any results back. If i substitute the $pv with $_ it does show the correct result, but I cannot use that automatic variable 2 or 3 cmdlets farther in the pipeline chain.
Although I can solve this by using the -OutVariable and/or split it into multiple lines.
I want to know why this doesn't work, I want to know what I'm missing here.
I have no experience with -PipelineVariable parameter. So, I took this as an opportunity to learn about the -PipelineVariable parameter:
Because I do not have easy access to VMs either, I simulated the function as follows:
Function Get-MacFromVm {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[string[]]
$Name
)
Function MacAddress {(1..4 | ForEach {'{0:x2}' -f (Get-Random -Min 0 -Max 255)}) -Join ":"}
Function Get-VM {[cmdletbinding()] Param ($Name) [pscustomobject]#{Name = $Name; MacAddress = (MacAddress)}}
ForEach ($Item in $Name) {
If ($PSCmdlet.ShouldProcess($Item, "Getting the mac address")) {
Get-VM $item -PipelineVariable vm |
Select-Object #{n="Name"; e={$vm.Name}}, #{n="ClientId"; e={$_.MacAddress -replace ":","-"}}
}
}
}
But I am not able to reproduce the issue:
PS C:\> Get-MacFromVm -Name "playground", "DC01" -PipelineVariable pv | % {$pv}
Name ClientId
---- --------
playground 3c-23-55-c4
DC01 4f-38-42-a7
So I wonder if this simulated function also works for you.
If yes, maybe you can find the differences with a real VM object.
Knowing PowerShell, I would question whether there is nothing outputted or nothing displayed. So what happens if you just output a single property:
Get-MacFromVm -Name "playground" -PipelineVariable pv | % {$pv.Name}
Or:
Get-MacFromVm -Name "playground" -PipelineVariable pv | % {$pv.GetType()}
Does this return a "You cannot call a method on a null-valued expression." error?
Update 30 October 2019
As per mklement0 comment:
The reason you're not seeing the bug is that your function, due to not using begin / process / end blocks, implicitly runs in the end block, and the very first invocation of any of these script blocks is captured in the pipeline variable, but not the output from subsequent script blocks calls, which would happen with a function that has a process block.
Meaning the correct simulation should be:
Function Get-MacFromVm {
[CmdletBinding(SupportsShouldProcess=$true)]
Param(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[string[]]
$Name
)
Begin {
Function MacAddress {(1..4 | ForEach {'{0:x2}' -f (Get-Random -Min 0 -Max 255)}) -Join ":"}
Function Get-VM {[cmdletbinding()] Param ($Name) [pscustomobject]#{Name = $Name; MacAddress = (MacAddress)}}
}
Process {
ForEach ($Item in $Name) {
If ($PSCmdlet.ShouldProcess($Item, "Getting the mac address")) {
Get-VM $item -PipelineVariable vm |
Select-Object #{n="Name"; e={$vm.Name}}, #{n="ClientId"; e={$_.MacAddress -replace ":","-"}}
}
}
}
}
Which indeed doesn't give any result for the PipelineVariable:
PS C:\> Get-MacFromVm -Name "playground", "DC01" -PipelineVariable pv | % {$pv}
PS C:\>
While the current item ($_) does:
PS C:\> Get-MacFromVm -Name "playground", "DC01" -PipelineVariable pv | % {$_}
Name ClientId
---- --------
playground e7-b5-a0-31
DC01 0f-ed-94-cc
I am new to powershell, Need to write a script to pull security logs and then place the data in a time stamped csv file in a location but I am getting I get stuck in a loop. My desired outcome is a CSV file in my directory with the security logs and time stamp for file name here is what I got for the part it is flagging.
Get-EventLog "Security" -After $Date `
| Where -FilterScript {$_.EventID -eq 4624 -and $_.ReplacementStrings[4].Length -gt 10 -and $_.ReplacementStrings[5] -notlike "*$"} `
| foreach-Object {
$row = "" | Select UserName, LoginTime
$row.UserName = $_.ReplacementStrings[5]
$row.LoginTime = $_.TimeGenerated
$eventList += $row
$variable | Export-Csv c:\output.csv
}
$eventList
Export-Csv : Cannot bind argument to parameter 'InputObject' because it is null.
At line:12 char:21
I am guessing the issue is here? Again my apologize very little scripting background, just trying to make a process better.
$variable | Export-Csv c:\output.csv
Yes, you don't define $variable within your script, that is why you getting this exception. It looks like you trying to export a bunch of EventLog entries as csv, so the Export-Csv cmdlet probably should belong outside the Foreach-Objectcmdlet. Maybe you want to do something like this:
Get-EventLog "Security" -After $Date `
| Where -FilterScript {$_.EventID -eq 4624 -and $_.ReplacementStrings[4].Length -gt 10 -and $_.ReplacementStrings[5] -notlike "*$"} `
| foreach-Object {
$row = "" | Select UserName, LoginTime
$row.UserName = $_.ReplacementStrings[5]
$row.LoginTime = $_.TimeGenerated
$row # this will put the current row to the pipeline
} | Export-Csv 'c:\output.csv'
I would like to be able to find all blanks from a CSV file and if a blank character is found on a line then should appear on the screen and I should be asked if I want to keep the entire line which contains that white space or remove it.
Let's say the directory is C:\Cr\Powershell\test. In there there is one CSV file abc.csv.
Tried doing it like this but in PowerShell ISE the $_.PSObject.Properties isn't recognized.
$csv = Import-Csv C:\Cr\Powershell\test\*.csv | Foreach-Object {
$_.PSObject.Properties | Foreach-Object {$_.Value = $_.Value.Trim()}
}
I apologize for not includding more code and what I tried more so far but they were silly attempts since I just begun.
This looks helpful but I don't know exactly how to adapt it for my problem.
Ok man here you go:
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Retain line."
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Delete line."
$n = #()
$f = Get-Content .\test.csv
foreach($item in $f) {
if($item -like "* *"){
$res = $host.ui.PromptForChoice("Title", "want to keep this line? `n $item", [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no), 0)
switch ($res)
{
0 {$n+=$item}
1 {}
}
} else {
$n+=$item
}
}
$n | Set-Content .\test.csv
if you have questions please post in the comments and i will explain
Get-Content is probably a better approach than Import-Csv, because that'll allow you to check an entire line for spaces instead of having to check each individual field. For fully automated processing you'd just use a Where-Object filter to remove non-matching lines from the output:
Get-Content 'C:\CrPowershell\test\input.csv' |
Where-Object { $_ -notlike '* *' } |
Set-Content 'C:\CrPowershell\test\output.csv'
However, since you want to prompt for each individual line that contains spaces you need a ForEach-Object (or a similiar construct) and a nested conditional, like this:
Get-Content 'C:\CrPowershell\test\input.csv' | ForEach-Object {
if ($_ -notlike '* *') { $_ }
} | Set-Content 'C:\CrPowershell\test\output.csv'
The simplest way to prompt a user for input is Read-Host:
$answer = Read-Host -Prompt 'Message'
if ($answer -eq 'y') {
# do one thing
} else {
# do another
}
In your particular case you'd probably do something like this for any matching line:
$anwser = Read-Host "$_`nKeep the line? [y/n] "
if ($answer -ne 'n') { $_ }
The above checks if the answer is not n to make removal of the line a conscious decision.
Other ways to prompt for user input are choice.exe (which has the additional advantage of allowing a timeout and a default answer):
choice.exe /c YN /d N /t 10 /m "$_`nKeep the line"
if ($LastExitCode -ne 2) { $_ }
or the host UI:
$title = $_
$message = 'Keep the line?'
$yes = New-Object Management.Automation.Host.ChoiceDescription '&Yes'
$no = New-Object Management.Automation.Host.ChoiceDescription '&No'
$options = [Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$answer = $Host.UI.PromptForChoice($title, $message, $options, 1)
if ($answer -ne 1) { $_ }
I'm leaving it as an exercise for you to integrate whichever prompting routine you chose with the rest of the code.
I have the following:
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Monday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Tuesday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Wednesday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Thursday.csv) +
#(Import-Csv C:\Users\Administrator\Desktop\dbs\Friday.csv) |
sort first_name,last_name,phone1 -Unique |
Export-Csv C:\Users\Administrator\Desktop\dbs\joined.csv
Import-Module ActiveDirectory
#EDIT PATH SO IT POINTS TO DB FILE \/
$newUserList = Import-Csv C:\Users\Administrator\Desktop\dbs\joined.csv
ForEach ($item in $newUserList){
$fname = $($item.first_name)
$lname = $($item.last_name)
$phone = $($item.phone1)
$username=$fname+$lname.substring(0,1)
# Puts Domain name into a Placeholder.
$domain='#csilab.local'
# Build the User Principal Name Username with Domain added to it
$UPN=$username+$domain
# Create the Displayname
$Name=$fname+" "+$lname
$newusers1 = (New-ADUser -GivenName $fname -Surname $lname -HomePhone $phone -Name $Name -DisplayName $Name -SamAccountName $username -AccountPassword (ConvertTo-SecureString "1NewPassword" -asplaintext -force) -ChangePasswordAtLogon $true -UserPrincipalName $UPN -Path "ou=test,dc=csi,dc=lab" -Enabled $true -PassThru) |
# I need this block to check for duplicates missed by the csv sort & merge
# as well as any in the destination OU itself as the script will run daily
# with inevitable possibility that user is unique to the csv but not the AD.
$newusers1 | Get-ADUser -Filter * -SearchBase "OU=Active Users,DC=csilab,DC=local" |
Sort-Object -Unique |
Remove-ADUser -confirm:$false
However I run it and get:
Get-ADUser : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
At C:\Users\Administrator\Desktop\Team2.ps1:40 char:14
+ $newusers1 | Get-ADUser -Filter * -SearchBase "OU=Active Users,DC=csilab,DC=loca ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (CN=Bethanie Cut...csilab,dc=local:PSObject) [Get-ADUser], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Microsoft.ActiveDirectory.Management.Commands.GetADUser
I also worry even if it did work that it'd delete the unique users instead of duplicates.
get-AdUser $username | Move-ADObject -TargetPath 'OU=Active Users,dc=csilab,dc=local'
}
What can I do to ensure all users are there without any originals getting deleted, just the duplicates?
You still have an empty pipe at the end of your New-ADUser statement, which would cause your script to fail with an "empty pipe element is not allowed" error, but oh well ...
To avoid collisions just check if an account already exists before you try to create it, and create it only if it doesn't:
$username = $fname + $lname.substring(0,1)
...
if (Get-ADUser -Filter "SamAccountName -eq '$username'") {
Write-Host "Account $username already exists."
} else {
New-ADUser -SamAccountName $username -Name $Name ... -PassThru
}
Also, you're overcomplicating the CSV handling. Simply process a list of the files via ForEach-Object:
$domain = '#csilab.local'
Set-Location 'C:\Users\Administrator\Desktop\dbs'
'Monday.csv', 'Tuesday.csv', 'Wednesday.csv', 'Thursday.csv', 'Friday.csv' |
ForEach-Object { Import-Csv $_ } |
Sort-Object first_name, last_name, phone1 -Unique |
ForEach-Object {
$fname = $_.first_name
$lname = $_.last_name
$phone = $_.phone1
...
}
You might want to use Try/Catch:
Try {$newusers1=(New-ADUser–GivenName$fname–Surname$lname-HomePhone$phone–Name$Name-DisplayName $Name –SamAccountName$username–AccountPassword(ConvertTo-SecureString"1NewPassword"-asplaintext-force) -ChangePasswordAtLogon$true–UserPrincipalName$UPN–Path"dc=csi,dc=lab"-Enabled$true)}
Catch {
If ($_.Exception.Message -match "account already exists")
{
#do whatever here, eg $NewUsers1 = Get-ADUser $Name
}
}
Also, if you can't see the user when browsing via ADUC it could be that you are connected to a different DC.
As mentioned above, the newuser1 variable will be null if the command failed. It will not load with the other user automatically, and it would be scary bad if it did. You need to decide what to do if the account already exist, that may simply be loading the variable with the other account, or doing something like appending a "1" to $name and retrying the command.
I am running validations based on details about the machine the script is running on. I currently have an API that will return 1 of the following
Name1
Name1, Name2
Name1, Name3
Name1, Name2, Name3
Name2, Name3
Name2
Name3
What would be the most efficient way to run a different set of validations (functions) based upon one of those 7 results?
EDIT: Here is some pseudo code to represent what I am trying to accomplish. I am hoping to make the switch and values in the where-object of each switch cleaner
function returnRelevantBlankValues {
$instance = $args[0] #could be any 7 of the strings above
$inputFile = "C:\path\input.txt"
$fileResults = "C:\path\output.txt"
switch ($instance){
"name1" {
Get-Content $inputFile | where-object {
$_ -like "*name1*" -and $_ -notlike "*name2*" -and $_ -notlike "*name3"} > $fileResults
}
"name1, name2" {
Get-Content $inputFile | where-object {
$_ -like "*name1*" -and $_ -like "*name1*" -and $_ -notlike "*name3"} > $fileResults
}
}
}
I validate based on a server's role, and a server can have more than one.
First I create server objects:
$servers = #()
$servers += New-Object -Type PSObject -Property #{
Name = "Server1"
Role = "DB"
}
$servers += New-Object -Type PSObject -Property #{
Name = "Server2"
Role = "DB","Application"
}
(In actuality I store them in XML, but this works)
Then, you can use the $_.Role -contains "Application" to determine if you want to run a check. This also makes it easier to eventually add additional servers of similar roles. YMMV depending on the details of what you're trying to check.