Windows Management Instrumentation msi install remote machine - c#

I'm trying to install a msi on a remote machine , i changed DCOM Config to get access from my machine when using methode install of win32_product it fails with return output 1601 ( that means that Windows Installer service could not be accessed )
i did some research in the net , and run the service manually using services.msc
and did msiexec / unreg and msiexec /register , still not working
ps: both machines a running on windows 10
here is my code
Ps : i used the same code for win32_Bios to get SerialNumber and it works
try
{
\\machine is the name of the computer
string PackageLocation = #"\\" + machine + msi;
ConnectionOptions connection = new ConnectionOptions();
connection.Username = username;
connection.Password = password;
connection.Impersonation = ImpersonationLevel.Impersonate;
connection.Authentication = AuthenticationLevel.Default;
connection.EnablePrivileges = true;
//define the WMI root name space
ManagementScope scope =
new ManagementScope(#"\\" + machine + #"\root\CIMV2", connection);
//define path for the WMI class
ManagementPath p =
new ManagementPath("Win32_Product");
//define new instance
ManagementClass classInstance = new ManagementClass(scope, p, null);
// Obtain in-parameters for the method
ManagementBaseObject inParams = classInstance.GetMethodParameters("Install");
// Add the input parameters.
inParams["AllUsers"] = true; //to install for all users
inParams["Options"] = ""; //paramters must be in the format “property=setting“
inParams["PackageLocation"] = #"c:\install\installer.msi";
//source file must be on the remote machine
// Execute the method and obtain the return values.
ManagementBaseObject outParams = classInstance.InvokeMethod("Install", inParams, null);
// List outParams
string retVal = outParams["ReturnValue"].ToString();
string msg = null;
switch (retVal)
{
case "0":
msg = "The installation completed successfully.";
break;
case "2":
msg = "The system cannot find the specified file. \n\r\n\r" + msi;
break;
case "3":
msg = "The system cannot find the path specified. \n\r\n\r" + msi;
break;
case "1619":
msg = "This installation package \n\r\n\r " + msi + "\n\r\n\rcould not be opened, please verify that it is accessible.";
break;
case "1620":
msg = "This installation package \n\r\n\r " + msi + "\n\r\n\rcould not be opened, please verify that it is a valid MSI package.";
break;
default:
msg = "Please see... \n\r\n\r http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/error_codes.asp \n\r\n\rError code: " + retVal;
break;
}
// Display outParams
Console.WriteLine(msg, "Installation report");
}
catch (ManagementException me)
{
Console.WriteLine(me.Message + "Management Exception");
}
catch (COMException ioe)
{
Console.WriteLine(ioe.Message, "COM Exception");
}
}

Related

C# SQL Server backup of a remote database to the remote default backup location without direct access to the remote location?

TL;DR - I want the server to take a backup not my application because the server is set up to do so and my application won't have access.
Background
My company created software for clients 20 years ago written in Delphi 7/Pascal. I am re-writing the software in C#. As part of the re-write I have created new Firebird, Oracle, and SQL Server databases. Federal regulation requires that all of the existing data is maintained so I have created a database modification / transformation tool in order to change from the old database structure to the new one.
Before I start making the change I need to backup the existing database. The technician who will run this tool has no access to their local file structure and no manual access to the remote server where the database is housed. The tool accesses an encrypted .ini-like file on the local system to parse out the components of the connection string and create a connection object. I then use that connection object to connect to the same database that the technicians computer is setup to connect to. This part all works
If I leave the default backup path alone it attempts to backup to default path but on the local machine(which technicians do not have access to create and we don't want a technician to have access to the .bak anyway) If I modify the default backup path to be a network path taken from the connection string, I get
SmoException: System.Data.SqlClient.SqlError: Cannot open backup device Operating system error 67(The network name cannot be found.).
because the filepath is not a network share (and won't be) and the database User credentials cannot access that path from outside of SQL Server.
So the question is: how do I have a backup taken to the remote default path as if I were on the server?
Here is the code that generates the error above (its the null case for remote).
public static void FullSqlBackup (Connection oldProactiveSql)
{
String sqlServerLogin = oldProactiveSql.UserName;
String password = oldProactiveSql.PassWord;
String instanceName = oldProactiveSql.InstanceName;
String remoteSvrName = oldProactiveSql.Ip + "," + oldProactiveSql.Port;
Server srv2;
Server srv3;
string device;
switch (oldProactiveSql.InstanceName)
{
case null:
ServerConnection srvConn2 = new ServerConnection(remoteSvrName);
srvConn2.LoginSecure = false;
srvConn2.Login = sqlServerLogin;
srvConn2.Password = password;
srv3 = new Server(srvConn2);
srv2 = null;
Console.WriteLine(srv3.Information.Version);
if (srv3.Settings.DefaultFile is null)
{
device = srv3.Information.RootDirectory + "\\DATA\\";
device = device.Substring(2);
device = oldProactiveSql.Ip + device;
}
else device = srv3.Settings.DefaultFile;
device = device.Substring(2);
device = string.Concat("\\\\", oldProactiveSql.Ip, device);
break;
default:
ServerConnection srvConn = new ServerConnection();
srvConn.ServerInstance = #".\" + instanceName;
srvConn.LoginSecure = false;
srvConn.Login = sqlServerLogin;
srvConn.Password = password;
srv2 = new Server(srvConn);
srv3 = null;
Console.WriteLine(srv2.Information.Version);
if (srv2.Settings.DefaultFile is null)
{
device = srv2.Information.RootDirectory + "\\DATA\\";
}
else device = srv2.Settings.DefaultFile;
break;
}
Backup bkpDbFull = new Backup();
bkpDbFull.Action = BackupActionType.Database;
bkpDbFull.Database = oldProactiveSql.DbName;
bkpDbFull.Devices.AddDevice(device, DeviceType.File);
bkpDbFull.BackupSetName = oldProactiveSql.DbName + " database Backup";
bkpDbFull.BackupSetDescription = oldProactiveSql.DbName + " database - Full Backup";
bkpDbFull.Initialize = true;
bkpDbFull.PercentComplete += CompletionStatusInPercent;
bkpDbFull.Complete += Backup_Completed;
switch (oldProactiveSql.InstanceName)
{
case null:
try
{
bkpDbFull.SqlBackup(srv3);
}
catch (Exception e)
{
Console.WriteLine (e);
Console.WriteLine(e.InnerException.Message);
throw;
}
break;
default:
try
{
bkpDbFull.SqlBackup(srv2);
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.InnerException.Message);
throw;
}
break;
}
}
Any help would be appreciated as I'm just running around in circles now.
From comments below I will try -
1. Dynamically create Stored Procedure [BackupToDefault] on database then run it.
2. If that fails link the database to itself.
3. Try - Exec [BackupToDefault] At [LinkedSelfSynonmym]
Wish me luck though it seems convoluted and the long way around I hope it works.
For inspiration... the backup is split into 3 files, each file residing in a different directory(sql instance backup dir & sql instance default dir & database primary dir)
//// compile with:
// /r:Microsoft.SqlServer.Smo.dll
// /r:Microsoft.SqlServer.SmoExtended.dll
// /r:Microsoft.SqlServer.ConnectionInfo.dll
using System;
using System.Data;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;
namespace SMObackup
{
class Program
{
static void Main()
{
// For remote connection, remote server name / ServerInstance needs to be specified
ServerConnection srvConn2 = new ServerConnection("machinename"/* <--default sql instance on machinename*/); // or (#"machinename\sqlinstance") for named instances
srvConn2.LoginSecure = false;
srvConn2.Login = "smologin";
srvConn2.Password = "SmoL#gin11";
srvConn2.DatabaseName = "msdb";
Server srv3 = new Server(srvConn2);
//server info
Console.WriteLine("servername:{0} ---- version:{1}", srv3.Name, srv3.Information.Version);
//server root directory
string serverRootDir = srv3.Information.RootDirectory;
//server backup directory
string serverBackupDir = srv3.Settings.BackupDirectory;
//database primary directory
string databasePrimaryFilepath = srv3.Databases[srvConn2.DatabaseName].PrimaryFilePath;
Console.WriteLine("server_root_dir:{0}\nserver_backup_dir:{1}\ndatabase_primary_dir{2}", serverRootDir, serverBackupDir, databasePrimaryFilepath);
Backup bkpDbFull = new Backup();
bkpDbFull.Action = BackupActionType.Database;
//comment out copyonly ....
bkpDbFull.CopyOnly = true; //copy only, just for testing....avoid messing up with existing backup processes
bkpDbFull.Database = srvConn2.DatabaseName;
//backup file name
string backupfile = $"\\backuptest_{DateTime.Now.ToString("dd/MM/yyyy/hh/mm/ss")}.bak";
//add multiple files, in each location
bkpDbFull.Devices.AddDevice(serverRootDir + backupfile, DeviceType.File);
bkpDbFull.Devices.AddDevice(serverBackupDir + backupfile, DeviceType.File);
bkpDbFull.Devices.AddDevice(databasePrimaryFilepath + backupfile, DeviceType.File);
bkpDbFull.Initialize = true;
foreach (BackupDeviceItem backupdevice in bkpDbFull.Devices)
{
Console.WriteLine("deviceitem:{0}", backupdevice.Name);
}
//backup is split/divided amongst the 3 devices
bkpDbFull.SqlBackup(srv3);
Restore restore = new Restore();
restore.Devices.AddRange(bkpDbFull.Devices);
DataTable backupHeader = restore.ReadBackupHeader(srv3);
//IsCopyOnly=True
for (int r = 0; r < backupHeader.Rows.Count; r++)
{
for (int c = 0; c < backupHeader.Columns.Count; c++)
{
Console.Write("{0}={1}\n", backupHeader.Columns[c].ColumnName, (string.IsNullOrEmpty(backupHeader.Rows[r].ItemArray[c].ToString())? "**": backupHeader.Rows[r].ItemArray[c].ToString()) );
}
}
srvConn2.Disconnect(); //redundant
}
}
}
Thank you #SeanLange
Pretty sure you would need to use dynamic sql in this case. Then the path will relative to where the sql is executed.
I changed my code to add:
private static void WriteBackupSp (Server remoteServer, string dbName, out string storedProcedure)
{
var s = "CREATE PROCEDURE [dbo].[ProactiveDBBackup]\n";
s += "AS\n";
s += "BEGIN\n";
s += "SET NOCOUNT ON\n";
s += "BACKUP DATABASE " + dbName + " TO DISK = \'" + string.Concat (remoteServer.BackupDirectory,#"\", dbName, ".bak") + "\'\n";
s += "END\n";
storedProcedure = s;
}
then changed the last switch to be:
switch (oldProactiveSql.InstanceName)
{
case null:
try
{
WriteBackupSp (srv3, oldProactiveSql.DbName, out var storedProcedure);
ConnectionToolsUtility.GenerateSqlConnectionString (oldProactiveSql, out var cs);
using (SqlConnection connection = new SqlConnection (cs))
{
using (SqlCommand command = new SqlCommand (storedProcedure, connection))
{
connection.Open ();
command.ExecuteNonQuery ();
connection.Close ();
}
}
var execBackup = "EXEC [dbo].[ProactiveDBBackup]\n";
using (SqlConnection connection = new SqlConnection (cs))
{
using (SqlCommand command = new SqlCommand (execBackup, connection))
{
connection.Open ();
command.ExecuteNonQuery ();
connection.Close ();
}
}
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.InnerException.Message);
throw;
}
break;
default:
try { bkpDbFull.SqlBackup(srv2); }
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.InnerException.Message);
throw;
}
break;
}
And that allowed me to take a backup of the database through the connection string to the default backup location without having credentials with access to the network path location.

Remote installation msi returns error 1619

Remote install msi returns error 1619 if package location on network drive or on a shared folder. If package is locate on remote machine, installation happens successfully.
static void Main(string[] args)
{
string[] app = { "1.msi", "2.msi", "3.msi" };
string retVal = "";
ConnectionOptions options = new ConnectionOptions();
string targetIpAddress = "targetPC"; // you can change this target IP address
options.Username = #"domain\username";
options.Password = "password";
ManagementScope scope = new ManagementScope(("\\\\" + targetIpAddress + "\\root\\cimv2"), options);
scope.Connect();
ManagementPath p = new ManagementPath("Win32_Product");
ManagementClass classInstance = new ManagementClass(scope, p, null);
ManagementBaseObject inParams = classInstance.GetMethodParameters("Install");
ManagementBaseObject outParams;
inParams["AllUsers"] = true;
inParams["Options"] = string.Empty;
foreach (var item in app)
{
inParams["PackageLocation"] = #"\\network_drive\app_location" + item;
outParams = classInstance.InvokeMethod("Install", inParams, null);
retVal = outParams["ReturnValue"].ToString();
retVal_install(app, inParams, retVal, item);
}
Console.ReadKey();
}
public static void retVal_install(string[] _app, ManagementBaseObject _inParams, string _retVal, string _item)
{
switch (_retVal)
{
case "0":
Console.WriteLine("The installation " + _item + " completed successfully.");
break;
case "1619":
Console.WriteLine("This installation package \n\r\n\r " + _inParams["PackageLocation"] + "\n\r\n\rcould not be opened, please verify that it is accessible.");
break;
Console.WriteLine("This installation package \n\r\n\r " + _inParams["PackageLocation"] + "\n\r\n\rcould not be opened, please verify that it is a valid MSI package.");
break;
default:
Console.WriteLine(_retVal);
break;
}
}
}
It is necessary that the remote installation returns value 0 (The installation completed successfully.)

Need help converting PowerShell to C# code

So i have this powershell code:
try{
$ComputerWMIObject = Get-WmiObject Win32_ComputerSystem -ComputerName "$oldComputerName" -Authentication 6
if ( $ComputerWMIObject ){
$result = $ComputerWMIObject.Rename("$newComputerName", $ADUserPassword , $ADUserName )
switch($result.ReturnValue)
{
0 {
if ( $Restart.IsChecked ) {
Get-WmiObject Win32_OperatingSystem -ComputerName "$oldComputerName" | ForEach-Object {$restart = $_.Win32Shutdown(6)}
$ResultText.Text = "Computer $oldComputerName was renamed to $newComputerName and restarted"
} else {
$ResultText.Text = "Computer $oldComputerName was renamed to $newComputerName restart computer to finish"
}
}
5 { $ResultText.Text = "Computer was not renamed. Please check if you have admin permissions (ReturnCode 5)" }
default { $ResultText.Text = "ReturnCode $($result.ReturnValue)"}
}
}else{
$ResultText.Text = "Couldn't create WMI Object on $oldComputerName"
}
}catch{
$ResultText.Text = $_
}
I'm trying to convert this to C# and can't find a way to do this. I just don't understand how to create WMI object.
It would be very helpful if you can post and example on how to do this.
I've read this Remotely change computer name for a Windows Server 2008 machine using C#? topic. And it throws an exception may be its because of this line:
Authentication = AuthenticationLevel.PacketPrivacy
I'm using System.Net.Security namespace and as it's stated in comment PacketPrivacy exists only there.
Since I can't ask there because I have low rating I've asked again.
Would be grateful if any one can help me.
PS: I know this can be done using NETDOM but I would prefer using WMI object.
ADDED:
I'm trying to use this:
var remoteControlObject = new ManagementPath
{
ClassName = "Win32_ComputerSystem",
Server = oldName,
Path = oldName + "\\root\\cimv2:Win32_ComputerSystem.Name='" + oldName + "'",
NamespacePath = "\\\\" + oldName + "\\root\\cimv2"
};
var conn = new ConnectionOptions
{
Authentication = AuthenticationLevel.PacketPrivacy,
Username = accountWithPermissions.Domain + "\\" + accountWithPermissions.UserName,
Password = accountWithPermissions.Password
};
var remoteScope = new ManagementScope(remoteControlObject, conn);
var remoteSystem = new ManagementObject(remoteScope, remoteControlObject, null);
ManagementBaseObject newRemoteSystemName = remoteSystem.GetMethodParameters("Rename");
var methodOptions = new InvokeMethodOptions();
newRemoteSystemName.SetPropertyValue("Name", newName);
newRemoteSystemName.SetPropertyValue("UserName", accountWithPermissions.UserName);
newRemoteSystemName.SetPropertyValue("Password", accountWithPermissions.Password);
ManagementBaseObject outParams = remoteSystem.InvokeMethod("Rename", newRemoteSystemName, null);
And I get this error Server RPC is unavailable. (Exception HRESULT: 0x800706BA) here:
ManagementBaseObject newRemoteSystemName = remoteSystem.GetMethodParameters("Rename");
ADDED2:
Ok, I guess I found out what causes an error.
i've changed original conn User name from
Username = oldName + "\\" + accountWithPermissions.UserName,
to
Username = accountWithPermissions.Domain + "\\" + accountWithPermissions.UserName,
and error happens, if I use old code I get ACCESS_IS_DENIED and that's correct because that user doesn't have rights.
So what's wrong if I use Domain\User may be I should change NamespacePath in remoteControlObject to be able to work with domain user authentication?
I was unable to find a way resolve this issue and no one seems to have an answer.
So I've solved this by using NETDOM when Server RPC is unavailable. (Exception HRESULT: 0x800706BA) actually any error happens I will add access denied check so it won't try NETDOM when this happens;
var remoteControlObject = new ManagementPath
{
ClassName = "Win32_ComputerSystem",
Server = oldName,
Path = oldName + "\\root\\cimv2:Win32_ComputerSystem.Name='" + oldName + "'",
NamespacePath = "\\\\" + oldName + "\\root\\cimv2"
};
string domain = accountWithPermissions.Domain;
string user = accountWithPermissions.UserName;
var conn = new ConnectionOptions
{
Authentication = AuthenticationLevel.PacketPrivacy,
Username = domain + "\\" + accountWithPermissions.UserName,
Password = accountWithPermissions.Password
};
var remoteScope = new ManagementScope(remoteControlObject, conn);
var remoteSystem = new ManagementObject(remoteScope, remoteControlObject, null);
try
{
ManagementBaseObject newRemoteSystemName = remoteSystem.GetMethodParameters("Rename");
newRemoteSystemName.SetPropertyValue("Name", newName);
newRemoteSystemName.SetPropertyValue("UserName", accountWithPermissions.UserName);
newRemoteSystemName.SetPropertyValue("Password", accountWithPermissions.Password);
ManagementBaseObject outParams = remoteSystem.InvokeMethod("Rename", newRemoteSystemName, null);
}
catch (Exception e)
{
this.Res.Inlines.Add(string.Format("Ошибка:\n" + e.Message + "\n"));
this.Res.Inlines.Add(string.Format("Пробуем переименовать используя NETDOM\n"));
bool restart = false;
PowerNETDOM(oldName, newName, accountWithPermissions, restart);
}
Server RPC is unavailable. (Exception HRESULT: 0x800706BA) is happening not because my changes to the script. This error if fired only by few PC's as I found out it can happen in many cases more about this here: Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA) . And changing settings on a remote PC makes me go and check the settings directly from that PC. In that case I can just rename PC not even trying to find out what this happening. That's why I do use NETDOM because for some reason it doesn't have problems renaming that PC remotely.
private static void PowerNETDOM(String oldName, String newName, NetworkCredential accountWithPermissions, bool restart)
{
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript
(
"param($Restart,$oldComputerName,$newComputerName,$ADUserPassword,$ADUserName);" +
"function ConvertTo-Encoding ([string]$From, [string]$To){" +
" Begin{ $encFrom = [System.Text.Encoding]::GetEncoding($from);$encTo = [System.Text.Encoding]::GetEncoding($to); }" +
" Process{ $bytes = $encTo.GetBytes($_);$bytes = [System.Text.Encoding]::Convert($encFrom, $encTo, $bytes);$encTo.GetString($bytes)}" +
"}" +
"$tmp = NETDOM RENAMECOMPUTER $oldComputerName /NewName:$newComputerName /ud:$ADUserName /pd:$ADUserPassword /Force $res_text | ConvertTo-Encoding \"cp866\" \"windows-1251\";" +
"$tmp > C:\\Temp\\rename.txt;$tmp;"
);
PowerShellInstance.AddParameter("restart", restart);
PowerShellInstance.AddParameter("newComputerName", newName);
PowerShellInstance.AddParameter("oldComputerName", oldName.ToString());
PowerShellInstance.AddParameter("ADUserPassword", accountWithPermissions.Password);
PowerShellInstance.AddParameter("ADUserName", accountWithPermissions.Domain + "\\" + accountWithPermissions.UserName);
PowerShellInstance.Invoke();
}
}

Creating and deleting files on remote machine

I want to create and delete a file on a remote machine of which i have admin username and password.
I am using this code
ConnectionOptions options = new ConnectionOptions();
options.Username = "admin";
options.Password = "12345";
ManagementScope scope = null;
ObjectQuery query = null;
ManagementObjectSearcher searcher = null;
try
{
scope = new ManagementScope(#"\\192.168.3.125\root\CIMV2", options);
scope.Connect();
query = new ObjectQuery(#"SELECT * FROM CIM_Datafile WHERE name = 'c:\\c$\\Testing\\Test.txt'");
searcher = new ManagementObjectSearcher(scope, query); // EDIT forgot to include 'scope' previously
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
foreach(ManagementObject mo in searcher.Get())
{
uint returnCode = (uint)mo.InvokeMethod("Delete", null);
if (returnCode == 0)
Console.WriteLine("File was successfully deleted");
else
Console.WriteLine("Deletion failed due to return code " + returnCode);
}
But it is giving me invalid query error and also i want to know how to Create a file on Remote machine.
and i even cant access the path \\192.168.3.125\C$\Testing\Test.txt
My file location is c:\Testing\Test.txt
Firstly can you access the file via the windows explorer from the machine (start -> run -> \192.168.3.125\C$\Testing\Test.txt)
If so what's wrong with
File.Delete(#"\\192.168.3.125\C$\Testing\Test.txt");
In my case, when I was connecting two windows based computers, you could just put:
#"\\PC-NAME\NEXT-FOLDER\NEXT-FOLDER\test.txt"

Remotely Install an Application from a FileShare using WMI

So far my code will start a process (Install an application) with command line arguments on a target computer and wait for the process to finish, IF I copy the install files to that computer.
My goal now is to:
Start a process with command line arguments (Install an application) on the remote computer.
NOT copy the files to the remote computer. The installer files will be located on a network share that both the sender computer and the remote computer have access to.
Wait for the process to finish.
Any help is much appreciated!
private void StartAppAction(string PCName, string Params)
{
//Example of Params \\Server\Folder\Application.EXE /s
ConnectionOptions conn = new ConnectionOptions();
conn.Impersonation = ImpersonationLevel.Impersonate;
conn.Authentication = AuthenticationLevel.Default;
conn.EnablePrivileges = true;
ManagementScope manScope = new ManagementScope(String.Format(#"\\{0}\ROOT\CIMV2", PCName), conn);
try
{
manScope.Connect();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
ObjectGetOptions objOpt = new ObjectGetOptions();
ManagementPath manPath = new ManagementPath("Win32_Process");
ManagementClass manClass = new ManagementClass(manScope, manPath, objOpt);
ManagementBaseObject inParams = manClass.GetMethodParameters("Create");
inParams["CommandLine"] = Params;
ManagementBaseObject outParams = manClass.InvokeMethod("Create", inParams, null);
string query = String.Format("SELECT * FROM __InstanceDeletionEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.ProcessID = '{0}'", outParams["ProcessId"].ToString());
string scope = #"\\" + PCName + #"\root\CIMV2";
EventWatcherOptions evOp = new EventWatcherOptions(null, new TimeSpan(1, 0, 0), 1);
ManagementEventWatcher manWatch = new ManagementEventWatcher(scope, query, evOp);
try
{
ManagementBaseObject watcher = manWatch.WaitForNextEvent();
var ID = ((ManagementBaseObject)watcher["TargetInstance"]);
//Process Ended
}
catch
{
MessageBox.Show("Unable to watch for the remote process to finish");
}
}

Categories