Install software from a list using Powershell - powershell

I am building a script to automate computer build and configuration: The idea is that from WDS it comes as clean as possible, automatically runs this script which will check the serial number, query our Workday database of assets and configure the OS according to what the user assigned to that system needs.
Right now I am focusing on 3 big groups: Laptop, Desktop, and Lab. All 3 will have some SW that will be the same and some that will be specific for each. My issue is with msiexec: Initially, I hard-coded all the installations for each group. but this means that I will have to change the script each time something is updated (say a new app is rolled out as default). which is not ideal.
function Install-Desktop {
#Write-Output "Here will be the install Desktop computer script"
$IPATH="<Path To root sw folder>"
#Software List
<# SOFTWARE LIST #>
$office="$IPATH\script\o365"
$webex="$IPATH\script\webex"
$chrome="$IPATH\script\chrome"
#install Ofice:
Invoke-Expression "$office\setup.exe /configure $office\O365.xml"
$params = '/i', "$webex\webexapp.msi",'/qb!','/norestart'
Start-Process msiexec -ArgumentList "$params" -Wait -PassThru
$params = '/i', "$chrome\GoogleChromeStandaloneEnterprise64.msi",'/qb!','/norestart'
Start-Process msiexec -ArgumentList $params -Wait -PassThru
}
This piece of code works well.
Now my idea was to import from a list the software to be installed (it is easier to maintain a list than to modify the script every time). something like:
function install-software {
param (
[String]$Type
)
$IPATH=<ROOT SW Folder>
$SoftWares=Import-Csv -Path "$IPath\script\$Type`.csv" #there will be a Laptop.csv in that path
foreach ($Software in $SoftWares) {
#detect if it is msiexect or other:
# (this has to do with how the csv is built, the first parameter is '/i' if it is an msi installer)
if ($Software.param1 -eq "'/i'") {
Start-Process msiexec -ArgumentList $Software -Wait -PassThru
}
else {
$Params=[string]::Join(" ",$Software.param1,$Software.param2,$Software.param3,$Software.param4)
Invoke-Expression "$Params"
}
}
}
This only works on the else part. However on the msiexec side of the if, the MSI opens as without arguments. I tried a lot of ways to pass the args, none worked. I am not a PowerShell guru in any way, so there is probably something that I am missing to see here.

Well, it looks like you have to pass the full path, it doesn't even let you use mounted net drive: so the answer was on the csv. instead of S:\<path to installer> it had to be \<Full path to installer> and i had to get rid of all the quotes and double quotes as well.

Related

How do I add a powershell script to be run post setup during a windows 10 unattended install?

I may be going about this the wrong/more difficult way. I am open to suggestions.
I am running NTLite v2.3.8.8920 [HOME] ((c) NTlitesoft d.o.o) to create unattended Windows 10 discs. After years of doing unattended discs and realizing the ever expanding size of the disc, (Latest disc was 32.73GB!), I found WinGet, the absolutely amazing repository, and have even gone as far as to create my own installer!
The issue for today is: how do I access WinGet during an unattended installation? I have compiled a list of applications that I use frequently; most that I have been hard coding to the disc and thus this incredible size; and I would love to be able to run this script post-setup and save the time and space. Here is my code:
#The first batch here is a function I created for notification purposes. Not sure how to do timed popups in Powershell yet.
#Get Words
function GW($myinput){
$WS = New-Object -ComObject "Wscript.Shell"
$ws.popup($myinput,3,'TK Installer',64)|SET-CLIPBOARD}
SET-CLIPBOARD to offload the popup response code. Need to find a better output or a way to prevent printing this response.
function install-myapps(){
Clear-Host
#Variable to hold the application list
$myapps = (
'Microsoft.PowerShell',
'Microsoft-Windows.Terminal',
'Microsoft.DotNet.SDK.3_1',
'Microsoft.DotNet.SDK.5',
'Microsoft.DotNet.SDK.6',
'Microsoft.MSIXCore',
'Microsoft.msmpisdk',
'Microsoft.ADKPEAddon',
'Microsoft.WebDeploy',
'9N5LW3JBCXKF',
'Nlitesoft.NTLite',
'Libretro.RetroArch',
'Notepad++.Notepad++',
'CodecGuide.K-LiteCodecPack.Full',
'Foxit.FoxitReader',
'7zip.7zip',
'OBSProject.OBSStudio',
'XnSoft.XnConvert',
'XnView.Classic',
'XnSoft.XnViewMP',
'corel.winzip',
'XP8K0J757HHRDW')
#Parser
ForEach-Object($aa in $myapps.Split(',')){
#Notification
GW "Installing $aa`nPlease wait..."
#Installer
WinGet install $aa --silent --accept-package-agreements --accept-source-agreements --force}
}
This code works perfectly in both command line and exe format; the latter using PS2EXE or IExpress. I just cannot figure out how to instantiate it post-setup from the unattended Win1021H2 side. Any help or insight would be greatly appreciated!
I was unable to figure out the process for this so I worked it around differently.
Below is how I fixed this situation:
# The first section opens and names function and
# declares variable $Hopeful applied to the full URI for the application we are installing
# !Considering using get-input but for now we will just use a replaceable variable!
# The second section begins the downloading and saving process
# Begins by separating the application from the URI assuming the format is as www.domain.com/application.exe
# Note now that the variable $JustApp will pull the just the last portion of the URI which is the application name
# Also, we'll make sure that what we're trying to do is possible by checking the extension of the last object
# Because wget needs 2 things; the URI and a place for the download to go; I am creating a directory to put these
# downloads in. Thus, $MyDir\$JustApp is now the default file point.
cls
Clear-Host
$MyDir = "d:\TKDI\"
# Create directory
if($mydir|Test-Path){
"My Directory Already Exists!"
}else{
md $MyDir -Force
}
# Section 1
Function TKDI($Hopeful,$MyArgs){
$_|select
# Section 2
$JustApp = $hopeful -split('/')|select -last 1
if($justapp -match "exe")
{
switch($MyArgs)
{
inno{$x ='/sp- /silent /forcecloseapplications /restartapplications /norestart'}
S{$x ='/S'}
silent{$x ='/silent'}
quiet{$x ='/quiet'}
passive{$x ='-passive'}
default{$x =$myargs}
un{$x ='-uninstall'}
$null{$x='/?'}
}
cls
echo "Processing $justapp"
if(Test-Path $mydir$justapp -PathType Leaf){echo 'File Downloaded Already!'}else{wget -Uri $hopeful -OutFile $MyDir$justapp}
$noteit = 'Installing $justapp in 5 seconds...'
$x=6;while($x-- -ge 1){cls;Write-host $x;sleep 1}
start -verb runas -wait -FilePath $mydir$justapp -ArgumentList $x
}elseif($justapp -match "msi")
{
cls
echo "You're file will be downloaded and installed!"
wget -Uri $hopeful -OutFile $MyDir$justapp
start -wait -Verb runas msiexec.exe -ArgumentList "-i $mydir$justapp /passive /norestart"
}else{
echo "This URI does not result in an application!"
}
}
tkdi www.example.com/index.exe inno #Installs beautifully```

ps1 to exe deployment of Powershell Script, Cannot find all the information required

I'm trying to create a .exe file that will run a powershell script that I have to install various components of an application. I'm working on step 1 of 4 to get this entire application installed and want to get it into one package. Things are not going well with it. I am using Ps1 to Exe by f2ko. There is no documentation that I can find on the software, but it does what I want it to. The issue is in the packaged files and how it is run. I am running into a couple issues. The primary issue seems to be with Start-Process, When the exe is running I get an error that states
Start-Process : This command cannot be run completely because the system cannot find all the information required.
At C:\Users\adminjp\AppData\Local\Temp\2605.tml\2606.tmp\2607.ps1:9 char:16
The remainder of the error is obscured by a status bar for the installer. The status bar runs, but the process does not. I initially had an issue with ExecutionPolicy but that is rectified by manually changing the value in order to run the script. I have been having multiple issues getting this script to run and am running out of articles online that I haven't read. Here is my powershell script:
Set-ExecutionPolicy -Force remotesigned
$NETfile = "env:p2eincfilepath\1 - NDP471-KB4033342-x86-x64-AllOS-ENU.exe"
$NETargs = "/q"
$SQLfile = "env:p2eincfilepath\setup.exe"
$SQLargs = "/ConfigurationFile=`".\ConfigurationFile_SQLExpress.ini`""
function Show-Progress ($file, $arguments, $component){
$process = Start-Process $file $arguments -PassThru
for($i = 0; $i -le 100; $i = ($i + 1) % 100)
{
Write-Progress -Activity "Installer" -PercentComplete $i -Status "Installing $component"
Start-Sleep -Milliseconds 100
if ($process.HasExited) {
Write-Progress -Activity "Installer" -Completed
break
}
}
}
Show-Progress $NETfile $NETargs ".NET 4.7.2"
Show-Progress $SQLfile $SQLargs "SQL Express"
The program that converts this script to an exe is what sets the extraction location and, to the best of my knowledge, I have no way to set that. Attached is a screenshot of their explanation of how to reference the embeded files:
I am using env:p2eincfilepath because that is the powershell method of accessing environment variables and the application appears to create or use an environment variable. Please let me know if there is any other information you might need. The Ps1 appears to work correctly if I run it directly from powershell and put the files in the root directory, calling them with ./ I would really like to keep this function in use since this is what gives me my status bar letting the user know what is being installed:
I think you need to have a $ sign in front of the env variable for it to fill in. e.g.
$NETfile = "$env:p2eincfilepath\1 - NDP471-KB4033342-x86-x64-AllOS-ENU.exe"
$NETargs = "/q"
$SQLfile = "$env:p2eincfilepath\setup.exe"
$SQLargs = "/ConfigurationFile=`".\ConfigurationFile_SQLExpress.ini`""

Start-Process, mklink and encrypted/stored credentials, oh my

I am working on a way to create a Symlink as a standard user, to address the situation outlined here.
I have created a password file and an AES key as shown here.
And I have this code, which without the credential stuff, but run from an elevated ISE, works as intended, creating a symlink in the root of C that points to the created folder in root of C.
But, when run unelevated it doesn't create the symlink, nor does it throw an error of any kind. It acts the same as if there was no credentials in use.
$passwordFile = "\\Mac\Support\Px Tools\x_PS Dev\SymLink_password.txt"
$keyFile = "\\Mac\Support\Px Tools\x_PS Dev\SymLink_AES.key"
$user = 'Px_Install'
$key = Get-Content $keyFile
$credential = New-Object -typeName:System.Management.Automation.PSCredential -argumentList:#($user, (Get-Content $passwordFile | ConvertTo-SecureString -key:$key))
if (-not (Test-Path 'C:\_Real')) {
New-Item 'C:\_Real' -itemType:directory > $null
}
if (-not (Test-Path 'C:\_Real\real.txt')) {
New-Item 'C:\_Real\real.txt' -itemType:file > $null
}
try {
Start-Process -filePath:cmd.exe -windowStyle:hidden -argumentList:('/c', 'mklink', '/d', 'C:\C4RLink', "`"C:\_Real`"") -credential:$credential -errorAction:stop
} catch {
Write-Host "Error"
}
So, three questions I guess.
1: Is there any way to test the validity of the created credential? I used $credential.GetType and it returns
OverloadDefinitions
-------------------
type GetType()
Which may or may not be correct, not sure.
2: Is there something wrong with my use of Start-Process?
3: Is there a way to actually trap meaningful errors or is cmd.exe so primitive I am stuck checking to see if the link exists post Start-Process and throwing my own error?
I tried
$results = Start-Process -filePath:cmd.exe -windowStyle:hidden -argumentList:('/c', 'mklink', '/d', 'C:\C4RLink', "`"C:\_Real`"") -credential:$credential -errorAction:stop -passThru
Write-Host "$results"
and it produces System.Diagnostics.Process (cmd) which isn't so helpful.
Speaking of Windows 7, I just tested it in Windows 7/PS2.0, and it DOES throw an error, but in Windows 10 it doesn't. Gawd Micros0ft, can't you get your shit together, EVER? but, maybe a thread to follow. Also going to try getting credentials another way, to eliminate that variable.
FWIW, I tried NOT wrapping the argument list in an array, in fact I started with that. But it didn't work so I tried the array on a lark.
EDIT: So, trying it in Windows 7 does produce an error, which is Parameter set cannot be resolved using the specified named parameters. I also realized I needed -verb:Runas in there. Added that, and switched my credentials to use Get-Credential for now. But still getting parameter set issues. Sigh.
Edit2: Seems to not like -verb or -windowsStyle in Windows 7/PS2.0. The latter is no big deal I guess, but -verb is pretty much required to get this to work methinks.
Edit3: nope, seems not to like -verb in Windows 10 either. But I have it reporting exceptions now, so thats a form of progress.
EDIT4: getting closer. I now have this
Start-Process powershell -credential (Get-Credential 'Px_Install') -argumentList "-noprofile -command &{Start-Process -filePath cmd.exe -argumentList '/c', 'mklink', '/d', 'C:\C4RLink', 'C:\_Real' -verb runas}"
And it works, but it raises a UAC dialog, which pretty much makes it useless.

Powershell - Copying File to Remote Host and Executing Install exe using WMI

EDITED: Here is my code now. The install file does copy to the remote host. However, the WMI portion does not install the .exe file, and no errors are returned. Perhaps this is a syntax error with WMI? Is there a way to just run the installer silently with PsExec? Thanks again for all the help sorry for the confusion:
#declare params
param (
[string]$finalCountdownPath = "",
[string]$slashes = "\\",
[string]$pathOnRemoteHost = "c:\temp\",
[string]$targetJavaComputer = "",
[string]$compname = "",
[string]$tempPathTarget = "\C$\temp\"
)
# user enters target host/computer
$targetJavaComputer = Read-Host "Enter the name of the computer on which you wish to install Java:"
[string]$compname = $slashes + $targetJavaComputer
[string]$finalCountdownPath = $compname + $tempPathTarget
#[string]$tempPathTarget2 =
#[string]$finalCountdownPath2 = $compname + $
# say copy install media to remote host
echo "Copying install file and running installer silently please wait..."
# create temp dir if does not exist, if exist copy install media
# if does not exist create dir, copy dummy file, copy install media
# either case will execute install of .exe via WMII
#[string]$finalCountdownPath = $compname + $tempPathTarget;
if ((Test-Path -Path $finalCountdownPath) )
{
copy c:\hdatools\java\jre-7u60-windows-i586.exe $finalCountdownPath
([WMICLASS]"\\$targetJavaComputer\ROOT\CIMV2:win32_process").Create("cmd.exe /c c:\temp\java\jre-7u60-windows-i586.exe /s /v`" /qn")
}
else {
New-Item -Path $finalCountdownPath -type directory -Force
copy c:\hdatools\dummy.txt $finalCountdownPath
copy "c:\hdatools\java\jre-7u60-windows-i586.exe" $finalCountdownPath
([WMICLASS]"\\$targetJavaComputer\ROOT\CIMV2:win32_process").Create("cmd.exe /c c:\temp\java\jre-7u60-windows-i586.exe /s /v`" /qn")
}
I was trying to get $Job = Invoke-Command -Session $Session -Scriptblock $Script to allow me to copy files on a different server, because I needed to off load it from the server it was running from. I was using the PowerShell Copy-Item to do it. But the running PowerShell script waits until the file is done copying to return.
I want it to take as little resources as possible on the server that the powershell is running to spawn off the process on another server to copy the file. I tried to user various other schemes out there, but they didn't work or the way I needed them to work. (Seemed kind of kludgey or too complex to me.) Maybe some of them could have worked? But I found a solution that I like that works best for me, which is pretty easy. (Except for some of the back end configuration that may be needed if it is is not already setup.)
Background:
I am running a SQLServer Job which invokes Powershell to run a script which backups databases, copies backup files, and deletes older backup files, with parameters passed into it. Our server is configured to allow PowerShell to run and under the pre-setup User account with SQL Server Admin and dbo privileges in an Active Directory account to allow it to see various places on our Network as well.
But we don't want it to take the resources away from the main server. The PowerShell script that was to be run would backup the database Log file and then use the another server to asynchronously copy the file itself and not make the SQL Server Job/PowerShell wait for it. We wanted it to happen right after the backup.
Here is my new way, using WMI, using Windows Integrate Security:
$ComputerName = "kithhelpdesk"
([Wmiclass]'Win32_Process').GetMethodParameters('Create')
Invoke-WmiMethod -ComputerName RemoteServerToRunOn -Path win32_process -Name create -ArgumentList 'powershell.exe -Command "Copy-Item -Path \\YourShareSource\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak -Destination \\YourShareDestination\YourDatabase_2018-08-07_11-45.log.bak"'
Here is my new way using passed in Credentials, and building arg list variable:
$Username = "YouDomain\YourDomainUser"
$Password = "P#ssw0rd27"
$ComputerName = "RemoteServerToRunOn"
$FromFile = "\\YourShareSource\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak"
$ToFile = "\\YourShareDestination\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak"
$ArgumentList = 'powershell.exe -Command "Copy-Item -Path ' + $FromFile + ' -Destination ' + $ToFile + '"'
$SecurePassWord = ConvertTo-SecureString -AsPlainText $Password -Force
$Cred = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $Username, $SecurePassWord
([Wmiclass]'Win32_Process').GetMethodParameters('Create')
Invoke-WmiMethod -ComputerName $ComputerName -Path win32_process -Name create -ArgumentList $ArgumentList -Credential $Cred
We think that this above one is the preferred one to use.
You can also run a specific powershell that will do what you want it to do (even passing in parameters to it):
Invoke-WmiMethod -ComputerName RemoteServerToRunOn -Path win32_process -Name create -ArgumentList 'powershell.exe -file "C:\PS\Test1.ps1"'
This example could be changed to pass in parameters to the Test1.ps1 PowerShell script to make it more flexible and reusable. And you may also want to pass in a Credential like we used in a previous example above.
Help configuring WMI:
I got the main gist of this working from: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/invoke-wmimethod?view=powershell-5.1
But it may have also needed WMI configuration using:
https://helpcenter.gsx.com/hc/en-us/articles/202447926-How-to-Configure-Windows-Remote-PowerShell-Access-for-Non-Privileged-User-Accounts?flash_digest=bec1f6a29327161f08e1f2db77e64856b433cb5a
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/enable-psremoting?view=powershell-5.1
Powershell New-PSSession Access Denied - Administrator Account
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/invoke-wmimethod?view=powershell-5.1 (I used to get how to call Invoke-WmiMethod).
https://learn.microsoft.com/en-us/powershell/scripting/core-powershell/console/powershell.exe-command-line-help?view=powershell-6 (I used to get syntax of command line)
I didn't use this one, but could have: How to execute a command in a remote computer?
I don't know for sure if all of the steps in the web articles above are needed, I suspect not. But I thought I was going to be using the Invoke-Command PowerShell statement to copy the files on a remote server, but left my changes from the articles above that I did intact mostly I believe.
You will need a dedicated User setup in Active Directory, and to configure the user accounts that SQL Server and SQL Server Agent are running under to give the main calling PowerShell the privileges needed to access the network and other things to, and can be used to run the PowerShell on the remote server as well. And you may need to configure SQLServer to allow SQL Server Jobs or Stored Procedures to be able to call PowerShell scripts like I did. But this is outside the scope of this post. You Google other places on the internet to show you how to do that.

Remove non-windows MSP packages with powershell

I'm trying to remove a Non-Microsoft MSP from a server that is running Win2k3 or Win2k8. The Update does show up in the Add/Remove programs when selecting "View Installed Updates". However I can't seem to find a way to get the MSP GUID.
I planned on using
msiexec /i {GUID-OF-PRODUCT} MSIPATCHREMOVE={GUID_OF_PATCH} /qb
that was found in this article: how to remove the Patch from console
However, I don't have a way to get the Patch GUID from the command line. Has anyone else been able to do something like this? There's plenty of ways to do this for Microsoft Patches, but since this is non-Microsoft, I'm hoping it's still possible.
Thanks,
Greg
You can use the Windows Installer com object to enumerate the patches.
Check out this article. It doesn't do exactly what you need, but it provides the comObject.types.ps1xml file you will need:
http://www.snowland.se/2010/02/21/read-msi-information-with-powershell/
Then you can do this to get the patch:
$installer_obj = New-Object -com WindowsInstaller.Installer;
$patches = $installer_obj.InvokeParamProperty("PatchesEx", "Product-Code-GUID", "s-1-1-0", 7, 15);
Product-Code-GUID is the GUID for the product you are interested in. I prefer to enumerate a list of products as well, and get the GUID programmatically based on the human readable name (i.e. the one that is displayed in Add/Remove Programs).
$installer_obj = New-Object -com WindowsInstaller.Installer;
$all_products = $installer_obj.GetProperty("Products");
foreach($product_code in $all_products) {
$product_name = $installer_obj.InvokeParamProperty("ProductInfo", $product_code, "ProductName")
if($product_name -eq "MySQL Server 5.1") {
$interesting_product_code = $product_code;
}
}
$patches = $installer_obj.InvokeParamProperty("PatchesEx", $interesting_product_code, "s-1-1-0", 7, 15);
Either route you take, now you just need to loop through the $patches and call msiexec from the command line with the proper arguments (if you are opting to use a literal string for the $interesting_product_code, just replace the variable and concatenation with the literal string GUID.):
foreach($patch in $patches) {
$patch_code = $patch.GetProperty("PatchCode");
$argument_list = "/I" + $interesting_product_code + " MSIPATCHREMOVE=$patch_code /qb /norestart";
Start-Process -FilePath "msiexec.exe" -ArgumentList $argument_list -Wait;
}
Here is a reference to the Windows Installer com object. You can do some other fun stuff with it too:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa369432%28v=vs.85%29.aspx
Hope that helps,
Aaron