I am looking to replace a Virtual Box solution with MS Hyper-V since I have had many problems with non-Reproducible issues in my automated test-suite using Virtual Box. I have a Windows 8.1 computer I will be using to run the tests on.
The current Virtual Box flow:
Start a VM
Reset snapshot
Use C# to transfer files to Guest OS through the network
Use Virtual Box to trigger the transferred .exe file to start automated tests.
I see people using Powershell Scripts and WMI to start and stop their Hyper-V VMs, but I don't see any way to trigger the transferred files on the Guest OS.
Am I missing an API that I can use? Otherwise how could I trigger the EXE on the guest OS programmatically?
I ended up using System.Management.Automation.PowerShell. I will share the main code chunk I used to do each step so future users can get help.
The Main Code Chunk
var ps = PowerShell.Create();
//Restore Snapshots
ps.AddCommand("Restore-VMSnapshot");
ps.AddParameter("Name", snapshot);
ps.AddParameter("VMName", vmName);
ps.AddParameter("Confirm", false);
ps.Invoke();
ps.Commands.Clear();
//Start VM
ps.AddCommand("Start-VM");
ps.AddParameter("Name", vmName);
ps.Invoke();
ps.Commands.Clear();
//Get IP
string[] ipValues = null;
do
{
ps.AddCommand("Get-VMNetworkAdapter");
ps.AddParameter("VMName", vmName);
var ips = ps.Invoke();
ps.Commands.Clear();
if (ips.Count > 0)
{
ipValues = (string[])ips[0].Members["IPAddresses"].Value;
}
} while (ipValues.Length ==0);
string ip = ipValues[0];
//Move Exe to VM
File.Copy(#"...", "\\\\" + ip + "\\Users\\Public\\Documents\\...", true);
//Run Program
ps.AddScript("$Username = '...'; $Password = '...' ;$ComputerName = '"+ip+"' ;"+
"$Script = {Start-Process C:\\Users\\Public\\Documents\\....exe} ;$secpasswd = ConvertTo-SecureString $Password -AsPlainText -Force ;"+
"$mycreds = New-Object System.Management.Automation.PSCredential ($Username, $secpasswd) ;"+
" $Session = New-PSSession -ComputerName $ComputerName -credential $mycreds ; Invoke-Command -Session $Session -Scriptblock $Script");
var passwords = ps.Invoke();
ps.Commands.Clear();
Notes
The //GetIP section is a do{}while() cause the IP takes a while to be query-able.
There is alot of pre-work required with the host computer and VMs to make this system function, which I will not get into here as google explains those parts better than me.
The flow is designed to match another system which uses Virtual Box, so it may seems a bit inefficient.
This obviously needs to be modified to fit each situation, but should be a good starting point for Hyper-V Automation.
A very usefull PowerShell CmdLet to transfert files to VM is Copy-VMFile.
Syntax is explained here :
http://technet.microsoft.com/en-us/library/dn464282.aspx
Hope this helps !
Related
I have been working on this for days now and didn´t find a solution.
I´m trying to write a little programm to create some AD-User with attributes provided by a csv file.
Creating the AD-User itself works fine, but when it comes to the activation of the exchange mailbox, it fails.
The mailbox should be enabled with the samaaccountname. The database should be found and set by the manager mailadress. (The new mailbox schould be placed in the same database as the one from the managers).
When I run the script from the Powershell itself, it runs without any errors and does exactly what it is supposed to do. But wen called from my c# program, nothing happens.
My Powershellscript looks like this:
Param(
[string] $manager,
[string] $sam
)
$session = New-Pssession -ConfigurationName Microsoft.Exchange -ConnectionURI http://<FQDN>/Powershell/ -Authentication Kerberos
Import-Pssession $session -WarningAction:Ignore -disablenamechecking |out-null
$mb = get-mailbox $manager
Enable-Mailbox $sam -Database $mb.Database
and its called by this c# method:
public void mail_Box(string sam, string manager)
{
try
{
RunspaceConfiguration runconf = RunspaceConfiguration.Create();
Runspace run = RunspaceFactory.CreateRunspace(runconf);
run.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = run;
ps.AddScript("<pathToSkript>").AddParameter("manager", manager).AddParameter("sam", sam);
ps.Invoke();
}
catch (Exception e)
{
popup(e.Message);
}
}
And at this point I´m totaly stuck.
I tried to "build" the script within c# with preloaded exchange-powershell-module
PSSnapInException PSException = null;
PSSnapInInfo info = runconf.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.SnapIn", out PSException);
with the AddCommand and AddParameter methodes but this gave me snapin registration errors, or in case of using
AddScript(". $env:ExchangeInstallPath\\bin\\RemoteExchange.ps1")
i got errors like "Enable-Mailbox" is not a Cmdlet.
As you may see I´m not a developer, just a sysadmin trying to get things automated.
Our software needs to map a network drive depending on which database the User logs in to.
The software first checks that the drive isn't already mapped, if it is then it skips the mapping step.
If the drive isn't mapped, or it is mapped to a different share (i.e. the User was previously logged in to a different database), then it clears any existing drive mapping, and maps the required drive.
It does this by generating and then running a PowerShell script.
Remove-SmbMapping -LocalPath "R:" -Force -UpdateProfile;
Remove-PSDrive -Name "R" -Force;
net use "R" /delete /y;
$password = ConvertTo-SecureString -String "Password" -AsPlainText -Force;
$credential = New-Object System.Management.Automation.PSCredential -ArgumentList "Username", $password;
New-PSDrive -Name "R" -PSProvider "FileSystem" -Root "\\server\share" -Credential $credential -Persist;
$a = New-Object -ComObject shell.application;
$a.NameSpace( "R:" ).self.name = "FriendlyName";
The first three lines remove any existing mapping on that drive letter. They all theoretically do the same thing, however thanks to Microsoft it's entirely random which line will actually work. It only consistently works if all three lines are run.
The middle three lines map the new drive.
The last two lines change the drive label of the new drive to something more user-friendly than the default \\server\share label
The first time someone logs in after a reboot the above script works perfectly. The new drive is mapped, and the label is changed.
However, if the User then logs out and logs into a different database, the label will not change.
For example, the User first logs in to 'Database A', and the drive is mapped with the label 'DatabaseAFiles'. All well and good.
But if the User then logs out, and logs in to 'Database B', the drive is correctly mapped and points to the correct share, but the label still says 'DatabaseAFiles' and not 'DatabaseBFiles'.
If the User reboots their PC, however, and logs in to 'Database B', then the label will correctly say 'DatabaseBFiles', but any subsequent log ins to other databases again won't change the label.
Reboot
Log in to Database A, label is DatabaseAFiles
Log out and into Database B, label is still DatabaseAFiles
Reboot
Log in to Database B, label is now DatabaseBFiles
This is not dependent on the last two script lines being present (the two that set the label), I actually added those to try to fix this issue. If those two lines are removed, the label is the default \\server\share label, and still doesn't change correctly, i.e.
Reboot
Log in to Database A, label is \\servera\sharea
Log out and into Database B, label is still \\servera\sharea
Reboot
Log in to Database B, label is now \\serverb\shareb
Regardless of the label, the drive is always correctly mapped to the correct share, and using it has all the correct directories and files.
Everything works correctly, it's just the label that is incorrect after the first login per reboot.
The script is run from within a C# program in a created PowerShell instance
using (PowerShell PowerShellInstance = PowerShell.Create())
{
PowerShellInstance.AddScript(script);
IAsyncResult result = PowerShellInstance.BeginInvoke();
while (result.IsCompleted == false)
{
Thread.Sleep(1000);
}
}
As it maps a drive, it cannot be run in Adminstrator mode (the drive won't be mapped for the actual User), it has to be run in normal mode, so there is a check earlier up for that.
If I take a copy of the script and run it in a PowerShell session outside the C# program, I get exactly the same results (everything works but the label is wrong after the first login), so it's not that it's being run from within the C# program.
It's entirely possible that the issue is with either File Explorer or with Windows, either caching the label somewhere and reusing it could be the problem, of course.
Anyone have any suggestions of things I can try please?
A time ago, I have had to rename file shares and therefor I wrote this function. Maybe this is helpful for you.
#--------------------------------------
function Rename-NetworkShare {
#--------------------------------------
param(
[string]$sharePattern,
[string]$value
)
$regPath = Get-ChildItem 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2'
$propertyName = '_LabelFromReg'
foreach( $child in $regPath ) {
if( $child.PSChildName -like ('*' + $sharePattern + '*') ) {
if( !$child.Property.Contains( $propertyName ) ) {
New-ItemProperty $child.PSPath -Name $propertyName -PropertyType String | Out-Null
}
Set-ItemProperty -Path $child.PSPath -Name $propertyName -Value $value | Out-Null
}
}
}
Rename-NetworkShare -sharePattern 'patternOldName' -value 'NewFriendlyName'
It's not ideal, there's one bit I'm not happy about, but this is the best I've been able to come up with so far. If I come up with something better I'll post that instead.
Firstly, I check if there is already a drive mapped to the letter I want to use:-
// Test if mapping already exists for this database
var wrongMapping = false;
var drives = DriveInfo.GetDrives();
foreach (var drive in drives)
{
var driveLetter = drive.RootDirectory.ToString().Substring(0, 1);
if (driveLetter == mappingDetails.DriveLetter && Directory.Exists(drive.Name))
{
wrongMapping = true; // Assume this is the wrong drive, if not we'll return from the method before it's used anyway
var unc = "Unknown";
using (RegistryKey key = Registry.CurrentUser.OpenSubKey("Network\\" + driveLetter))
{
if (key != null)
{
unc = key.GetValue("RemotePath").ToString();
}
}
if (unc == mappingDetails.Root)
{
View.Status = #"Drive already mapped to " + mappingDetails.DriveLetter + ":";
ASyncDelay(2000, () => View.Close());
return; // Already mapped, carry on with login
}
}
}
If we already have the correct path mapped to the correct drive letter, then we return and skip the rest of the mapping code.
If not, we'll have the variable wrongMapping, which will be true if we have a different path mapped to the drive letter we want. This means that we'll need to unmap that drive first.
This is done via a Powershell script run the by C# program, and contains the bit I'm not happy about:-
Remove-PSDrive mappingDetails.DriveLetter;
Remove-SmbMapping -LocalPath "mappingDetails.DriveLetter:" -Force -UpdateProfile;
Remove-PSDrive -Name "mappingDetails.DriveLetter" -Force;
net use mappingDetails.DriveLetter /delete /y;
Stop-Process -ProcessName explorer;
The first four lines are different ways to unmap a drive, and at least one of them will work. Which one does work seems to be random, but between all four the drives (so far) always get unmapped.
Then we get this bit:
Stop-Process -ProcessName explorer;
This will close and restart the Explorer process, thus forcing Windows to admit that the drive we just unmapped is really gone. Without this, Windows won't fully release the drive, and most annoyingly it will remember the drive label and apply it to the next drive mapped (thus making a mapping to CompanyBShare still say CompanyAShare).
However, in so doing it will close any open File Explorer windows, and also briefly blank the taskbar, which is not good.
But, given that currently no Company sites have more than one share, and it's only the Developers and Support that need to remove existing drives and map new ones, for now we'll put up with it.
Once any old drive is unmapped, we then carry on and map the new drive, which again is done via a PowerShell script run from the C# code.
$password = ConvertTo-SecureString -String "mappingDetails.Password" -AsPlainText -Force;
$credential = New-Object System.Management.Automation.PSCredential -ArgumentList "mappingDetails.Username", $password;
New-PSDrive -Name "mappingDetails.DriveLetter" -PSProvider "FileSystem" -Root "mappingDetails.Root" -Credential $credential -Persist;
$sh=New_Object -com Shell.Application;
$sh.NameSpace('mappingDetails.DriveLetter:').Self.Name = 'friendlyName';
New-Item –Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\" –Name "foldername";
Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\foldername" -Name "_LabelFromReg";
New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\foldername" -Name "_LabelFromReg" -Value "friendlyName" -PropertyType "String\";
The first part maps the drive:
$password = ConvertTo-SecureString -String "mappingDetails.Password" -AsPlainText -Force;
$credential = New-Object System.Management.Automation.PSCredential -ArgumentList "mappingDetails.Username", $password;
New-PSDrive -Name "mappingDetails.DriveLetter" -PSProvider "FileSystem" -Root "mappingDetails.Root" -Credential $credential -Persist;
The middle part changes the name directly:
$sh=New_Object -com Shell.Application;
$sh.NameSpace('mappingDetails.DriveLetter:').Self.Name = 'friendlyName';
And the end part changes the name in the Registry:
New-Item –Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\" –Name "foldername";
Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\foldername" -Name "_LabelFromReg";
New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2\foldername" -Name "_LabelFromReg" -Value "friendlyName" -PropertyType "String\";
Firstly, it creates a key for this path (it the key already exists it'll fail but the script will carry on)
Then it removes the existing property _LabelFromReg (if it doesn't exist it'll fail but the script will carry on)
Then it (re)creates the property _LabelFromReg with the new friendlyname.
So, again doing the same thing two ways, but between the two it works.
I'd like to find some alternative to having to kill and restart the Explorer process, it's really tacky, but it seems to be the only way to get Windows to acknowledge the changes.
And at least I now get the correct labels on the drives when mapped.
So I try to invoke a PS script from a ASPX/C# application. When I run the PS script from commandline (PS C:\scripts> ./do-barrelRoll.ps1 -s remoteComputer) it works as expected. But when I use C# it cannot connect to the remote computer (wrong user/password). Fun fact: The user and password to use are inside a config section in the PS script itself (see $SETUP variable)! What am I doing wrong?
Exact error message from ASPX (the server name is correct!):
[remoteComputer] Connecting to remote server remoteComputer failed with the following error message : The user name or password is incorrect
Relevant parts of the PS Script:
set-alias new New-Object
$passwd = $SETUP["password"] | ConvertTo-SecureString -asplaintext -force
$session = new -TypeName System.Management.Automation.PSCredential -argumentlist $SETUP["user"], $passwd
Invoke-Command –ComputerName $PARAM["s"] -Credential $session –ScriptBlock {
# Do a barrel roll
}
Relevant parts of ASPX/C# application:
using System.Management.Automation;
public ActionResult doAction(string server, string action)
{
var split = server.Split(';');
//...
var ps = PowerShell.Create();
ps.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -f; C:\\scripts\\do-barrelRoll.ps1 -s " + split[1]);
Collection<PSObject> results = ps.Invoke();
// Here I retrive the results and the content of the error stream and display them
}
It works as expected at the command line most likely because your account has full script access and can run everything. The account that .NET runs under should be more constrained. You'll have to check that the account has rights to run unrestricted scripts. Check local policies and then go up from there. You most likely are not the same account that the .NET application is running as.
The following command works when called directly from powershell, but not when called within an ASP.NET application.
Invoke-Command -ComputerName remotesrv -ScriptBlock { 5 }
I suppose, there is some kind of user rights problem, but at this point I am stuck and don't know how to solve this.
The ASP.NET code looks as follows:
System.Threading.Tasks.Task.Factory.StartNew(() =>
{
using (PowerShell powershell = PowerShell.Create())
{
var rs = "-ComputerName remotesrv";
powershell.AddScript("Set-ExecutionPolicy RemoteSigned");
var script = String.Format("Invoke-Command {0} -scriptblock {{ 5 }}", rs);
powershell.AddScript(script);
powershell.InvocationStateChanged += delegate(object sender, PSInvocationStateChangedEventArgs e)
{
if (e.InvocationStateInfo.State == PSInvocationState.Completed)
{
// Clean up
}
};
var output = new PSDataCollection<PSObject>();
output.DataAdded += delegate(object sender, DataAddedEventArgs e)
{
PSDataCollection<PSObject> myp = (PSDataCollection<PSObject>)sender;
Collection<PSObject> results = myp.ReadAll();
foreach (PSObject result in results)
{
if (result.BaseObject is int)
{
// Do something in the database
}
}
};
IAsyncResult asyncResult = powershell.BeginInvoke<PSObject, PSObject>(null, output);
asyncResult.AsyncWaitHandle.WaitOne();
}
}
);
If I don't add -ComputerName remotesrv, the script is executed.
What do you need to do, to be able to call a powershell script remotely from an ASP.NET application?
I would recommend you read through the Powershell remoting requirements specifically the User Permission portion. By default, the account creating the connection must be an administrator on the remote machine. If your web application is running under an identity that is not an administrator on the remote machine, the connection will fail. Note that this is the default behaviour and can be changed by tweaking the session configurations on the remote machine.
I had the same problem, my solution was change the user who execute IIS's application pool.
After set Enable-PsRemoting -Force and Set-ExecutionPolicy Unrestricted, (perhaps is better left ExecutionPolicy RemoteSigned), invoke-command still doesn't work.
Then I went to Task Manager, Process tab and look by w3wp.exe and the User that was executing that process was "DefaultAppPool", I guess that this user have not rights to remote access or execution on PowerShell. (Image shows Administrador because I already change it)
To change the user go to IIS Manager, in application groups, my site's application groups shows in Identity: ApplicationPoolIdentity, Select the application pool and click Advanced Settings, in Process Model, select Identity and click the ellipsis (the button with the three dots). In Personal Account sets an Administrator account with username and password, reboot IIS.
If I look by Task Manager, iis user has changed
Problem Summary
When run in batch mode, my user identity is lost but (like a really good cliffhanger) not until the very last moment.
Problem Details
I have a PowerShell script running on WinXpSp3 that runs a script block (via Invoke-Command) on a remote machine as a particular test user (via -Session parameter) in the background (via -AsJob parameter). The session is created with this:
New-PSSession -computername $myServer -credential $myCredential
The script block performs a number of actions, culminating in running the NUnit test framework. The C# code under test records the "myTestUser" username (via Environment.UserName) so the credentials provided by PowerShell are properly received that far. This is further confirmed by Process Explorer: examining properties of nunit-console running the batch tests shows it is owned by myTestUser.
The test includes accessing a Sql Server 2008 R2 database; the connection string is set via a new SqlConnection(connectionString) call. The connection string is set up for Windows Authentication and uses this form:
Data Source=<my_db_server_name>;Initial Catalog=<my_db_name>;Integrated Security=True;Persist Security Info=True
Even though I have conclusively pushed the myTestUser credentials all the way to the C# code under test, the DB access attempt is not seeing these credentials, resulting in this error: Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'
Some supplemental info:
I have confirmed that the test user (myTestUser) has DB permissions and the NUnit test is capable of accessing the DB: When I run the NUnit test manually (via NUnit GUI) logged in as myTestUser, the test works properly and SqlProfiler clearly shows this activity with myTestUser appearing in the NTUserName column.
The same error occurs if I run locally rather than on a remote machine.
The same error occurs if I run as myself on my local machine (i.e. omitting the -credential parameter).
Question
How can I rescue myTestUser from the brink of doom and get him DB access?
2011.05.16 Update
Here is a simplified example exhibiting the same problem.
First, my test program DBFetchVersion that prints the name of the current user and the results of a simple query:
class Program
{
const string connString = ...your connection string here... ;
const string query = "SELECT getdate() [Date], substring(##version,1,charindex('-',##version)-1) +convert(varchar(100),SERVERPROPERTY('edition'))+ ' ' +convert(varchar(100),SERVERPROPERTY('productlevel')) [SQL Server Version], ##servicename [Service Name], ##servername [Server Host], db_name() [Database], user_name() [User], host_name() [Client]";
static void Main(string[] args)
{
DataView dataView;
using (var connection = new SqlConnection(connString))
{
Console.WriteLine("user = " + Environment.UserName);
using (var dataAdapter = new SqlDataAdapter(query, connection))
{
var dataSet = new DataSet();
try
{
connection.Open();
dataAdapter.SelectCommand.CommandType = CommandType.Text;
dataAdapter.Fill(dataSet, query);
}
finally { if (connection.State == ConnectionState.Open) connection.Close(); }
dataView = dataSet.Tables[0].DefaultView;
}
foreach (var item in dataView.Table.Rows[0].ItemArray)
Console.WriteLine(item);
}
}
}
And here is the Powershell script that calls the above program.
$scriptBlock = {
& "...path to my executable...\DBFetchVersion\bin\Debug\DBFetchVersion.exe"
}
$serverName = ... my server name ...
$username = "testuser"
$password = ... my user password ...
$adjPwd = $password | ConvertTo-SecureString -asPlainText -Force
$testCred = (New-Object System.Management.Automation.PSCredential($username,$adjPwd))
$mySession = New-PSSession -computername $serverName -credential $testCred
# Test Scenarios:
Invoke-Command $scriptBlock
#Invoke-Command $scriptBlock -computername $serverName
#Invoke-Command $scriptBlock -computername $serverName -credential $testCred
#Invoke-Command $scriptBlock -Session $mySession
In the list of four test scenarios at the end, the uncommented one works, printing my user name and the results of the query.
DBFetchVersion still reports I am the user with the second line, but the DB connection fails with the " Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON' " error.
The remaining two lines report the "testuser" user name, but both report the same login failure for the DB connection.
What this isolated example tells me is not that I think there is anything buggy about Powershell, .NET, or my code, but there is something with the authentication mechanism that I do not yet understand, since specifying another computer or a session both involve a path that should, in some sense, have stronger protection.
2011.08.03 Update - Eureka!
Well, Matt was correct in identifying the double-hop issue as the culprit and CredSSP authentication as the solution. Unfortunately, as I quickly found out, CredSSP requires Windows 7, so I went about setting up a couple VMs as a sandbox. CredSSP, however, was not one to easily relinquish its secrets (at least to me) as I detailed in this post on ServerFault: Cannot get CredSSP authentication to work in PowerShell
I finally was able to get CredSSP authentication to work so I could then come back to the problem I posed here in this thread. As a test, I used these 3 script blocks plugged into the PowerShell script I provided above:
$scriptBlockA = {
Write-Host ("hello, world: {0}, {1}" -f $env:USERNAME, (hostname))
}
# Simple DB test, but requires SqlServer installed!
$scriptBlockB = {
if (! (Get-PSSnapin | ? { $_.name -eq "SqlServerCmdletSnapin100" } ) )
{ Add-PSSnapin SqlServerCmdletSnapin100; }
Invoke-Sqlcmd -Query "SELECT getdate() as [Now]" -ServerInstance CinDevDB5
}
# Indirect DB test; requires .NET but not SqlServer,
# plus DBFetchVersion in home dir for targeted user.
$scriptBlockC = {
& ".\DBFetchVersion.exe"
}
Block A worked with or without CredSSP, since there is no double-hop. Blocks B and C would only work with CredSSP because they both attempt to access a remote database. QED.
Initially i read this and thought of the "double hop" issue, but the supplemental info maybe me question that though.
When you run it locally (as yourself or the testuser) what commands do you use? this:
& "...path to my executable...\DBFetchVersion\bin\Debug\DBFetchVersion.exe"
also does this work from your local machine (as either yourself or the user):
Add-PSSnapin SqlServerCmdletSnapin100;
Invoke-Sqlcmd -Query "SELECT getdate()" -ServerInstance Server
Also what OS are you using? If it is Windows 2008 and the issue is double hop you may be able to us CredSSP to avoid it.