I hope someone can help me with the following script:
The script I built works for 1 computer. It asks the user to enter the computer name, and It will check if the computer is ONLINE, if it is it will copy and install the application.
Now, I want to add another functionality. I want users to create a TXT file with all the computer names (line by line) and save them somewhere in the computer. The user will run the script, and a Windows Explorer would open requesting the location of the TXT file. So, the script will collect that data (I might be wrong, but should I use arrays for that?) From there, I am assuming a ForEach statement should do it right?
The most difficult (for me) part is... if the computer is OFFLINE, it will skip that computer, and go for the next one... when it reaches the end of the lines, it should go back to the first line and check if the computer (that was offline) is online. The tricky part is... if the computer already has the app installed, it should skip it, and continue to keep pinging computers and so on. I want the script to run until someone manually stops it, installed the app to all the pcs, or runs for 3 days.
This might be too much to ask, but... it would also be nice to see a report of all the computers where the app was successfully installed (I am sure this should be on the ForEACH portion).
Thank you all
function HarvesterMenu {
$PCName = read-host -Prompt "Enter the computer name"
$PCName = $PCName -replace '(^\s+|\s+$)','' -replace '\s+',' '
$pingPC = Test-Connection -ComputerName $PCName -Quiet -Count 1 -ErrorAction SilentlyContinue
if ($pingPC -eq $True) {
Write-host "ONLINE"
harvesterInstall($PCName)
}
else {
Write-host "OFFLINE"
}
}
function HarvesterInstall($InstallOnPC) {
$Sourcefile = "\\SERVER1\discovery03$\HarvesterRemote.msi"
Copy-Item -Path $Sourcefile -Destination "\\$InstallOnPC\c$\windows\temp\HarvesterRemote.msi"
Invoke-Command -ComputerName $InstallOnPC -ScriptBlock {
Start-Process c:\windows\temp\HarvesterRemote.msi -ArgumentList "/quiet" -Wait
}
Start-Sleep 3
$InstCompleted = Test-Path -Path '\\$InstallOnPC\C$\Program Files (x86)\Labs\Harvester Remote\Harvester\Harvester.exe'
Write-Host $InstCompleted
if ($InstCompleted -eq $True) {
Write-host "Harvester was successfuly installed on $InstallOnPC"
}
else {
Write-host "Something went wrong. Ping the server and try again"
}
$BacktoMenu = read-host -Prompt "Press Y to go back to the Main Menu or N to exist"
if ($backtoMenu -eq "y") {
harvesterMenu
}
else {
Exit
}
}
HarvesterMenu
You can do that easily with a Foreach-Loop.
U need a File with the pc's u wanna install the application. maybe .txt or .csv
Then u can just address it in a foreach.
something like that:
$InputFile = 'C:\Temp\lIST.csv'
$addresses = get-content $InputFile
foreach($address in $addresses) {
if (test-Connection -ComputerName $address -Count 1 -Quiet ) {
write-Host "$address Online"
#ur harvesterinstall
}
else
{
Write-Warning "$address Offline"
}
}
Related
New to Powershell, My goal is to go through a list of remote Computers and check to see if certain services are running on them and starting the services if they are not. what would be the best approach in creating a variable for the services on said servers?
Server1.txt - 'ServiceA ServiceB ServiceC'
Server2.txt - 'ServiceD ServiceE ServiceF'
Server3.txt - 'ServiceG ServiceH'
$services = get-content .\Server1.txt
$services | ForEach {
try {
Write-Host "Attempting to start '$($.DisplayName)'"
Start-Service -Name $.Name -ErrorAction STOP
Write-Host "SUCCESS: '$($.DisplayName)' has been started"
} catch {
Write-output "FAILED to start $($.DisplayName)"
}
}
Thank you.
In your input, you have mentioned one text file for each server which is not advisable. Also there is no computer name in your Start-service Command. Please find my input sample below.
server1-serviceA,ServiceB,ServiceC
server2-serviceD,ServiceE,ServiceF
server3-serviceG,ServiceH,ServiceI
And here is the powershell script, since you have mentioned different services for each server there is a need for using split function.
$textFile = Get-Content C:\temp\servers.txt
foreach ($line in $textFile) {
$computerName = $line.split("-")[0] #Getting computername by using Split
$serviceNames = $line.split("-")[1] #Getting Service names by using split
foreach ($serviceName in $serviceNames.split(",")) {
# Again using split to handle multiple service names
try {
Write-Host " Trying to start $serviceName in $computerName"
Get-Service -ComputerName $computerName -Name $serviceName | Start-Service -ErrorAction Stop
Write-Host "SUCCESS: $serviceName has been started"
}
catch {
Write-Host "Failed to start $serviceName in $computerName"
}
}
}
I haven't tested the script for starting the service, but the loop works properly for multiple servers and their respective services. Thanks!
I have an application that seems to have left a mess behind. I've uninstalled the app and it seems to have progressed normally.
No longer shows in Programs & Features. Nothing in the registry locations:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer
Nothing even when searching.
When I try to install a newer version I get an error that the previous version needs to be removed first.
I finally found something when I looked in Win32_Product using PS:
Get-WmiObject Win32_Product | Sort-Object Name | Format-Table IdentifyingNumber, Name, LocalPackage
Question is, how can I get it out of here with a script? I can't run an uninstall, get errors. I have been able to use MSI Cleanup Utility to remove but I'd like to be able to do something more automated. Estimating that there's about 200 machines in this state.
There are some very good reasons to never use Win32_Product. If you Google, there's lots of explanations, but here's one of the first hits, Please Stop Using Win32_Product To Find Installed Software. Alternatives Inside!
Of course, that's not really your question, Win32_Product was just how you located it. It's possible the program installation data is in a different location.
Try looking through:
'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\'
'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\'
'Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\'
There's an awesome script on the Gallery called Get-RemoteProgram.ps1 it packages a function by the same name so dot source it into your session like:
. <Path>\Get-RemoteProgram.ps1
Once you are in you can search for the program and include the registry path in the output. There're plenty of examples in the help file but something like:
Get-RemoteProgram -ComputerName $env:computername -IncludeProgram ^Office -ProgramRegExMatch -DisplayRegPath
Once you know the location I would look for an UninstallString value. If yes, I'd then think about how to get it to run silently, which if it's an MSI package should be pretty straight forward. Once you've got it worked out simply wrap some PowerShell code around it to invoke and monitor it through to completion.
Update from Comments:
Obviously I'd have trouble figuring this out from a afar. I posted above because it would find something in the registry. Partly because you hadn't listed the Wow6432... location.
Given my earlier statements I'm not going to try testing Win32_Product on my own. However, my next step would be to figure out what Win32_Product is finding. In that case I would start with Process Monitor. It will take some work, but may illuminate what Win32_Process is finding.
The other thing I can suggest is to observe a fresh installation of the software on another system. By snapshotting the registry, and perhaps a directory listing before and after you may find additional bread crumbs.
You can also use a secondary instance of the program to harvest the uninstall string, then try running it on the concerned system to see what happens.
Piggy backing off Steven's answer, you can use something along the lines of this:
Function Uninstall-Software {
[CmdletBinding()]
[CmdletBinding()]
param(
[Parameter(Mandatory=$false,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias('cn','name')]
[string[]]$ComputerName,
[Parameter(Mandatory=$false)]
[switch]$Quiet
)
Begin{
$Date_Now = Get-Date
}
Process{
if($Quiet){
foreach($Computer in $ComputerName){
try{
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
[array]$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"} -Session $PSSession
[array]$Software_List = $Software_List | Sort-Object -Property DisplayName -Unique
$(for($i=0; $i -lt $Software_List.Count; $i++){
[PSCustomObject]#{
'Display Name' ="${i}.) $($Software_List[$i].DisplayName)"
' Version ' = $Software_List[$i].DisplayVersion
}
} ) | Out-Host
$QS = Read-Host -Prompt "Enter Number of software to uninstall"
$QS = $QS -split ','
if([string]::IsNullOrEmpty("$QS") -eq $true){"Null string"; break}
foreach ($Q in $QS) {
if($Software_List[$Q].QuietUninstallString -eq $null){
$Quiet_Switch = '/quiet'
$Uninstall_String = $Software_List[$Q].UninstallString
Invoke-Command -ScriptBlock { & cmd /c $Using:Uninstall_String $Using:Quiet_Switch /norestart } -Session $PSSession -EnableNetworkAccess
}
else{
$Uninstall_String = $Software_List[$Q].QuietUninstallString
Invoke-Command -ScriptBlock { & cmd /c $Using:Uninstall_String /norestart } -Session $PSSession -EnableNetworkAccess
}
#& cmd /c $Uninstall_String $Quiet_Switch /norestart
}
} Catch [System.Management.Automation.RuntimeException] {
$Error[0].Message.Split('.')[1].Trim()
}
}
}
Else{
foreach($Computer in $ComputerName){
try{
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
[array]$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"} -Session $PSSession
[array]$Software_List = $Software_List | Sort-Object -Property DisplayName -Unique
$(for($i=0; $i -lt $Software_List.Count; $i++){
[PSCustomObject]#{
'Display Name' ="${i}.) $($Software_List[$i].DisplayName)"
' Version ' = $Software_List[$i].DisplayVersion
}
} ) | Out-Host
$QS = Read-Host -Prompt "Enter Number of software to uninstall"
$QS = $QS -split ','
if([string]::IsNullOrEmpty("$QS") -eq $true){"Null string"; break}
foreach($Q in $QS){
$Uninstall_String = $Software_List[$Q].UninstallString
& cmd /c $Uninstall_String /norestart
}
} Catch [System.Management.Automation.RuntimeException] {
$Error[0].Message.Split('.')[1].Trim()
}
}
}
}
}
Calling the function gives you 2 options.
Interactive: Uninstall-Software
Non-Interactive: Uninstall-Software -Quiet
As you can imagine, the interactive choice doesn't work for remote computers, but using -Quiet will.
Uninstall-Software -ComputerName RemoteComputer -Quiet
This is a script I made a while ago and never finished it so it's all over the place. Works for the most part but, can use some serious work. I just dont care for it anymore.
Running the function will make a number selection out of your list of installed software pulled from the registry. So, all you have to do is select the number of software to uninstall, or select multiple.
Hopefully this gets you on the right track following Stevens comments in regards to using the uninstall string to uninstall software, instead of using CIM methods.
Please don't mark this as the answer, thought i'd share an unfinished script that works.
I am working on developing PowerShell script to automate a task on a remote server by using Invoke-Command with WinRM.
The script will take the server IP, test WinRM and "Get-Credential" cmdlet to establish session and use Invoke-Command to run another script on remote server. I have made significant progress of what I want to achieve, however, I am having trouble on how to setup the code so that when I press the "Cancel" or "X" button on Get-Credential prompt it should abort the script and return to the regular PowerShell command line prompt.
Below is what I have so far, I have erased the comments and description of the code to keep the number of words less in here.
function SS
{
Add-Type -AssemblyName System.Windows.Forms
$BInput = [System.Windows.Forms.MessageBox]::Show('Do you want to proceed?', 'Confirmation',[System.Windows.Forms.MessageBoxButtons]::YesNo)
switch ($BInput)
{
"Yes" {
while ($true)
{
$server=Read-Host "Enter Server IP Address"
set-item -Path WSMan:\localhost\Client\TrustedHosts -Value "$server" -Force
if(Test-WSMan -ComputerName $server -ErrorAction SilentlyContinue)
{
Write-Host "$server is accessible, enter credentials to connect"
while ($true)
{
$creden=Get-Credential -Message "Please enter the server credentials that you want to connect"
$serversession = New-Pssession -ComputerName $server -Credential $creden -ErrorAction SilentlyContinue
if(-not($serversession))
{
write-warning "Credentials are not valild, please try again"
}
else
{
write-host "$server is connected, starting the workflow ......"
Invoke-Command -Session $serversession -FilePath "C:\Temp\XXX.ps1"
}
}
Break
}
else
{
write-host "Windows Remote Management (WinRM) protocol is not running, please check service and confirm."
}
}
Get-Pssession | Remove-PSSession
}
"No" {
Break
}
}
}
I understand I have to apply the changes / logic after this line
$creden=Get-Credential -Message "Please enter the server credentials that you want to connect"
But can't seem to find it yet. I looked online and have taken different approaches but no success so far. I would like to have opinions or recommendations on how to tackle this, appreciate your help.
Thanks
What i'm seeing is that you may be thinking too much into it. A simple if statement should do the trick, try:
$creden=Get-Credential -Message "Please enter the server credentials that you want to connect"
if(!$creden){break}
Continuing from my comment.
Try this refactor of your use case.
Point of note: Note fully tested since I do not have an environment at this time to test.
Function Start-WorkFlow
{
<#
.Synopsis
Execute a workflow
.DESCRIPTION
Sets up a WinRM session to a remote host to execute the defined workflow
.EXAMPLE
Start-WorkFlow
.EXAMPLE
swf
.INPUTS
Remote host IPAddress
Remove host credentials
.OUTPUTS
Resutls of teh workflow
.NOTES
V 0.0.1 - Prototype script. Clean-Up before production use
.COMPONENT
Stand-alone script
.ROLE
Administrative actions
.FUNCTIONALITY
Implemetned error logic for each code block
Restrict the user input to only be a proper IPAddress
Validate TCPIP state
Validate WSman state
Establish a new session
Process workflow
Exit session
#>
[cmdletbinding(SupportsShouldProcess)]
[Alias('swf')]
Param
(
)
If ((Read-Host -Prompt 'Do you want to proceed: [Yes/No]') -eq 'No' )
{Break}
Else
{
Do {$RemoteServerIPAddress = (Read-Host -Prompt 'Enter Server IP Address')}
Until ($RemoteServerIPAddress -match "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
Get-ChildItem -Path 'WSMan:\localhost\Client\TrustedHosts'
Try
{
(Test-Connection -ComputerName $RemoteServerIPAddress -Count 1 -ErrorAction Stop).IPV4Address
# Set-Item -Path 'WSMan:\localhost\Client\TrustedHosts' -Value $RemoteServerIPAddress -Force
Get-ChildItem -Path 'WSMan:\localhost\Client\TrustedHosts'
Try
{
Test-WSMan -ComputerName $RemoteServerIPAddress -ErrorAction Stop
"$RemoteServerIPAddress is accessible, enter credentials to connect"
Do
{
$Creds = $null
$CredMesssage = 'Please enter the remote server credentials that you want to connect.'
$CredMesssage = "$CredMesssage If credentials are not valid, you will be prompted to re-enter them."
$Creds = Get-Credential -Message $CredMesssage
if(-Not $creds)
{
Write-Warning -Message 'Credential request cancelled.'
Start-Sleep -Seconds 3
Exit
}
$NewPSSessionSplat = #{
ComputerName = $RemoteServerIPAddress
Credential = $Creds
Name = 'RemoteSessionName'
ErrorAction = 'Stop'
}
New-PSSession $NewPSSessionSplat
}
Until (Get-PSSession -Name 'RemoteSessionName')
"$RemoteServerIPAddress is connected, starting the workflow ......"
Invoke-Command -Session $RemoteServerSession -FilePath 'C:\Temp\XXX.ps1'
}
Catch
{
Write-Warning -Message 'Session connection results:'
$PSitem.Exception.Message
}
Finally
{
Get-PSSession |
Remove-PSSession -ErrorAction SilentlyContinue
}
}
Catch
{
Write-Warning -Message "
The remote server $RemoteServerIPAddress is not available
Exiting the session."
Start-Sleep -Seconds 3
Exit
}
}
}
Start-WorkFlow
I work in a large company and that is global and I would like to be able to run a script that will allow me to clean up old profile remotely. I found this script but it doesn't seem to do anything. Does anyone have thought to why. I am a novice in powershell so any help would be appreciated. Thanks in advance!
function Remove-ProfileWD {
<#
.SYNOPSIS
Interactive menu that allows a user to connect to a local or remote computer and remove a local profile.
.DESCRIPTION
Presents an interactive menu for user to first make a connection to a remote or local machine. After making connection to the machine,
the user is presented with all of the local profiles and then is asked to make a selection of which profile to delete. This is only valid
on Windows Vista OS and above for clients and Windows 2008 and above for server OS.
.EXAMPLE
Remove-ProfileWF
Description
-----------
Presents a text based menu for the user to interactively remove a local profile on local or remote machine.
#>
#Prompt for a computer to connect to
(
[Parameter(Mandatory=$true)]
$Computer = Read-Host "Please enter a computer name"
)
#Test network connection before making connection
If ($computer -ne $Env:Computername) {
If (!(Test-Connection -comp $computer -count 1 -quiet)) {
Write-Warning "$computer is not accessible, please try a different computer or verify it is powered on."
Break
}
}
Do {
#Gather all of the user profiles on computer
Try {
[array]$users = Get-WmiObject -ComputerName $computer Win32_UserProfile -filter "LocalPath Like 'C:\\Users\\%'" -ea stop
}
Catch {
Write-Warning "$($error[0]) "
Break
}
#Cache the number of users
$num_users = $users.count
Write-Host -ForegroundColor Green "User profiles on $($computer):"
Write-Host -ForegroundColor Green "User profile Last Use ate"
#Begin iterating through all of the accounts to display
For ($i=0;$i -lt $num_users; $i++) {
Write-Host -ForegroundColor Green "$($i): $(($users[$i].localpath).replace('C:\Users\','')) $(get-item \\$computer\C`$\users\$(($users[$i].localpath).replace('C:\Users\',''))| Foreach {$_.LastWriteTime}) "
}
Write-Host -ForegroundColor Green "q: Quit"
#Prompt for user to select a profile to remove from computer
Do {
$account = Read-Host "Select a number to delete local profile or 'q' to quit"
#Find out if user selected to quit, otherwise answer is an integer
If ($account -NotLike "q*") {
$account = $account -as [int]
}
}
#Ensure that the selection is a number and within the valid range
Until (($account -lt $num_users -AND $account -match "\d") -OR $account -Like "q*")
If ($account -Like "q*") {
Break
}
Write-Host -ForegroundColor Yellow "Deleting profile: $(($users[$account].localpath).replace('C:\Users\',''))"
#Remove the local profile
($users[$account]).Delete()
Write-Host -ForegroundColor Green "Profile: $(($users[$account].localpath).replace('C:\Users\','')) has been deleted"
#Configure yes choice
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Remove another profile."
#Configure no choice
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Quit profile removal"
#Determine Values for Choice
$choice = [System.Management.Automation.Host.ChoiceDescription[]] #($yes,$no)
#Determine Default Selection
[int]$default = 0
#Present choice option to user
$userchoice = $host.ui.PromptforChoice("","Remove Another Profile?",$choice,$default)
}
#If user selects No, then quit the script
Until ($userchoice -eq 1)
}
I am trying to write something that will prompt for a PC name and then remotely perform a folder permission change. Everything is chaotic looking but I am struggling with changing the execution policy after the Enter-PSSession - it seems it just gets stuck and won't proceed further to my function.
This is the rough draft of what I have thus far. Any help is appreciated.
$PromptedPC = Read-Host -Prompt "`n `n Enter PC Number"
Enter-PSSession -ComputerName $PromptedPC
Write-Host `n"Do you want to change folder permissions?" -ForegroundColor Green
$ReadAnswer = Read-Host " ( y / n ) "
PowerShell.exe -noprofile -ExecutionPolicy Bypass
switch ($ReadAnswer)
{
Y {
function Grant-userFullRights {
[cmdletbinding()]
param(
[Parameter(Mandatory=$true)]
[string[]]$Files,
[Parameter(Mandatory=$true)]
[string]$UserName
)
$rule=new-object System.Security.AccessControl.FileSystemAccessRule ($UserName,"FullControl","Allow")
foreach($File in $Files) {
if(Test-Path $File) {
try {
$acl = Get-ACL -Path $File -ErrorAction stop
$acl.SetAccessRule($rule)
Set-ACL -Path $File -ACLObject $acl -ErrorAction stop
Write-Host "Successfully set permissions on $File"
} catch {
Write-Warning "$File : Failed to set perms. Details : $_"
Continue
}
} else {
Write-Warning "$File : No such file found"
Continue
}
}
}
}
}
Grant-userFullRights -Files 'C:\ProgramData\New World Systems\' -UserName "BUILTIN\Users"
I did get the function information from: http://techibee.com/powershell/grant-fullcontrol-permission-to-usergroup-on-filefolder-using-powershell/2158
I am just trying to put something together that will work for a programdata folder our users need full control of that keeps reverting permissions to something lower than full access.
You start a new PowerShell with the ExecutionPolicy Bypass.
Sine you are already in PowerShell, the proper way to configure the Execution Polcy is Set-ExecutionPolicy.
However, since you don't invoke any script file, there is no need to change the Execution Policy.