0

So this is what I have (full add-in code):

using Microsoft.SystemCenter.VirtualMachineManager;
using Microsoft.SystemCenter.VirtualMachineManager.UIAddIns;
using Microsoft.SystemCenter.VirtualMachineManager.UIAddIns.ContextTypes;
using Microsoft.VirtualManager.Remoting;
using System;
using System.AddIn;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Forms;

namespace Microsoft.VirtualManager.UI.AddIns.BackupAddIn
{
[AddIn("Make Backup")]
public class BackupAddIn : ActionAddInBase
{
    protected const string PowershellPath = "%WINDIR%\\System32\\WindowsPowershell\\v1.0\\powershell.exe";
    protected const string DefaultDirectory = "C:\\ClusterStorage\\Volume2";
    protected const string WildcardVMName = "{{VMName}}";
    protected const string WildcardError = "{{Error}}";
    protected const string BackupDirectoryBase = "{{Base}}";
    protected const string BackupDirectoryName = "{{Name}}";
    protected const string BackupDirectoryDate = "{{Date}}";

    public enum JobState { Initialized, Success, Fail };

    protected const string BackupDirectoryTemplate = BackupDirectoryBase + "\\" + BackupDirectoryName + "\\" + BackupDirectoryDate + "\\";
    protected static readonly ReadOnlyCollection<string> AllowedBackupStates = new ReadOnlyCollection<string>(new string[] { "PowerOff", "Paused"/*, "Saved"*/});

    public override bool CheckIfEnabledFor(IList<ContextObject> contextObjects)
    {
        if (contextObjects != null && contextObjects.Count > 0)
        {
            foreach (var host in contextObjects.OfType<HostContext>())
            {
                if (host.ComputerState != ComputerState.Responding)
                {
                    return false;
                }
            }
            return true;
        }

        return false;
    }

    public override void PerformAction(IList<ContextObject> contextObjects)
    {
        if (contextObjects != null)
        {
            // check if we have VMs selected
            var VMs = contextObjects.OfType<VMContext>();
            if (VMs != null && VMs.Count() > 0)
            {
                // check if VMs are in a good state
                var badVMs = VMs.Where(vm => AllowedBackupStates.Contains(vm.Status.ToString()) == false).ToArray();
                if (badVMs != null && badVMs.Length > 0)
                {
                    MessageBox.Show("Backup not possible!\r\nThe following VMs are still running:\r\n\r\n" + string.Join(", ", badVMs.Select(vm => vm.Name)));
                }
                else
                {
                    // ask for backup directory
                    string backupDir = Microsoft.VisualBasic.Interaction.InputBox("Enter a path on the host to export the selected virtual machine(s) to.", "Export path", DefaultDirectory);
                    if (string.IsNullOrEmpty(backupDir) == false)
                    {
                        if (backupDir.EndsWith("\\"))
                        {
                            backupDir = backupDir.Substring(0, backupDir.Length - 1);
                        }

                        // go
                        /*foreach (var vm in VMs)
                        {
                            exportVM(vm, backupDir);
                        }*/

                        // testing to export multiple vms in one invoke
                        exportVMs(VMs, backupDir);
                    }
                }
            }
        }
    }

    public string getDate()
    {
        var date = DateTime.Now;
        return date.Year.ToString()
            + (date.Month < 10 ? "0" : "") + date.Month.ToString()
            + (date.Day < 10 ? "0" : "") + date.Day.ToString()
            + "_"
            + (date.Hour < 10 ? "0" : "") + date.Hour.ToString()
            + (date.Minute < 10 ? "0" : "") + date.Minute.ToString();
    }

    public void ManageJob(string name, JobState state, string message = null)
    {
        string command;
        if (state == JobState.Initialized)
        {
            command = string.Format("New-SCExternalJob -Name \"{0}\"", name);
        }
        else if (state == JobState.Success)
        {
            command = string.Format("Set-SCExternalJob -Job (Get-SCJob -Name \"{0}\")[0] -Complete -InfoMessage \"" + (string.IsNullOrEmpty(message) ? "Backup successfully started." : message.Replace("\"", "'")) + "\"", name);
        }
        else
        {
            command = string.Format("Set-SCExternalJob -Job (Get-SCJob -Name \"{0}\")[0] -Failed -InfoMessage \"" + (string.IsNullOrEmpty(message) ? "Backup FAILED." : message.Replace("\"", "'")) + "\"", name);
        }

        //MessageBox.Show(command);

        PowerShellContext.ExecuteScript<Host>(
            command,
            (profiles, error) =>
            {
                if (error != null)
                {
                    MessageBox.Show("Cannot modify job state\r\nError: " + error.Problem);
                }
            }
        );
    }

    public void exportVMs(IEnumerable<VMContext> VMs, string backupDir)
    {
        string date = getDate();
        string VMS = "";
        string fullBackupDirS = BackupDirectoryTemplate.Replace(BackupDirectoryBase, backupDir).Replace(BackupDirectoryName, "_VMBackups").Replace(BackupDirectoryDate, date);
        VMS = "'" + string.Join("', '", VMs.Select(vm => vm.Name).ToArray()) + "'";
        string command = string.Format("Export-VM -Name {0} -Path '{1}'", VMS, fullBackupDirS);
        MessageBox.Show(command);

        // We need to manager jobs in another thread probably --------------------------------------------------------------!!!
        string jobname = "Starting_backup_of_multiple_machines";

        mkShortcuts(backupDir, date, VMs.Select(vm => vm.Name).ToArray(), VMs.First());
        //!            execPSScript(jobname, scvmmPsCommand(command, VMs.First()), VMs.First(), WildcardVMName + ": Backup successful.", WildcardVMName + ": Backup FAILED!\r\nError: " + WildcardError, backupDir, date, VMs.Select(vm => vm.Name).ToArray());
    }

    public String scvmmPsCommand(string command, VMContext vm, string appPath = PowershellPath)
    {
        return string.Format("Invoke-SCScriptCommand -Executable {0} -VMHost (Get-SCVMHost -ID \"{1}\") -CommandParameters \"{2}\" -RunAsynchronous -TimeoutSeconds 360000", appPath, vm.VMHostId.ToString(), command);
    }

    // Make a shortcut from the machines backup directory to the backup in the "_VMBackups"-folder
    public void mkShortcuts(string path, string date, string[] names, VMContext vm)
    {
        string command = "$shell = New-Object -ComObject WScript.Shell;";
        foreach (var n in names)
        {
            command = command + string.Format(" $shortc = $shell.CreateShortcut('{0}\\{1}\\{2}.lnk'); $shortc.TargetPath = '{0}\\_VMBackup\\{2}\\{1}'; $shortc.Save();", path, n, date);
        }
        string fullCommand = scvmmPsCommand(command, vm);
        MessageBox.Show(fullCommand);
        execPSScript("Create_ShortcutS", fullCommand, vm, "Shortcut(s) created.", "FAILED to create Shortcut(s)!");
    }

    public void execPSScript(string jobname, string command, VMContext vm, string successMessage, string errorMessage, string path = "", string date = "", string[] names = null)
    {
        ManageJob(jobname, JobState.Initialized);
        PowerShellContext.ExecuteScript<Host>(
            command,
            (vms, error) =>
            {
                if (error != null)
                {
                    ManageJob(jobname, JobState.Fail, errorMessage.Replace(WildcardVMName, vm.Name).Replace(WildcardError, error.Problem));
                }
                else
                {
                    ManageJob(jobname, JobState.Success, successMessage.Replace(WildcardVMName, vm.Name));
                    if (string.IsNullOrEmpty(path) == false)
                    {
                        //mkShortcuts(path, date, names, vm);
                    }
                }
            }
        );
    }
}
}

When I run the plugin I get an error Box that says sth like: "Unknown script-error. Expression not closed - ")" missing.

+ ... andard'; .Save();
+                    ~
An expression was expected after '('.
+ CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordEx 
   ception
+ FullyQualifiedErrorId : ExpectedExpression". Weitere Informationen finden Sie im Standardfehlerprotokoll "C:\Windows\TEMP\gce_stderrord07b04547c74493caa6bdba9087df444.log".

But I can't find the error in my code since it worked when typed manually into the powershell on the host and .Save() takes no arguments. Do you have any idea?

000000000000000000000
  • 1,467
  • 1
  • 19
  • 38
  • 2
    What, **exactly** does the error message state? Saying just *"error Box that says sth like"* is **not** helpful. Error messages usually contain a reference to the line number on which error was triggered. – vonPryz Jan 13 '15 at 11:41
  • "Error: Unknown error while running the PS-Script: Closing expression ")" missing" (Translation from German) This really is everything it says – 000000000000000000000 Jan 13 '15 at 12:03
  • Error says you're missing a bracket, then you're missing a bracket. Or comma, or quote, or whatever else. Is this PowerShell? What extensions are you using? What is your code supposed to do? OK it's C# calling PowerShell calling vb script but why? – Jan Chrbolka Jan 13 '15 at 12:50
  • If the error message isn't helpful, then you got to write all the script commands into a console window or file instead of executing them.Copy-and-paste (do not type, in order to avoid accidentally fixing a mistake) those to a Powershell prompt to see on which command fails. – vonPryz Jan 13 '15 at 13:10
  • I know this is stupid, but so is my job... This is the C# code of an SCVMM add-in wich creates a backup of my vms on the host. Now I want to create a shortcut on the host to the backup dir. So I invoke a ps script over the ps script on the system center called by the add-in to execute a vb function. I know this is stupid, I can't help it. :/ – 000000000000000000000 Jan 13 '15 at 13:12
  • `.Save()` seems to be getting parsed on its own as a statement. That won't work, since that ends up trying to call a `Save` command or cmdlet or function with an empty expression in a new context within the parentheses, and in the local scope (`.`). PowerShell does not allow dangling references like that, unlike, say, VB's `With` statements. The exact root cause is not at all clear, though, so you'll have to dig some more to figure out why your code is trying to do that. – Nathan Tuggy Jan 15 '15 at 11:10
  • Then why does it work like this ´$shortcut.Save()´ in Powershell on the host, but not when invoked from my c# vmm plugin? Basically how do I get this (http://pctools.com/guides/scripting/detail/141/?act=reference) to work with this (http://technet.microsoft.com/en-us/library/hh801727.aspx)? – 000000000000000000000 Jan 15 '15 at 12:48

1 Answers1

0

OK, Instead of

command2 = command2 + string.Format(" ${1} = $shell.CreateShortcut(\"C:\\ClusterStorage\\Volume2\\test.lnk\"); ${1}.TargetPath = \"{0}\\_VMBackup\\{2}\\{1}\"; ${1}.Save();", backupDir, vm.Name, date);

Try

command2 = command2 + string.Format(" $shortc = $shell.CreateShortcut(\"C:\\ClusterStorage\\Volume2\\test.lnk\"); $shortc.TargetPath = \"{0}\\_VMBackup\\{2}\\{1}\"; $shortc.Save();", backupDir, vm.Name, date);
command2 = "'" + command2 + "'"

This did not work and neither did a lot of other things. Basically, the problem is the way the command string is passed to PowerShell to execute. PowerShell will examine the sting and attempt to enumerate all variables. This is not the desired behaviour. Tried to enclose the entire string in single quotes, however this did not help. In the end the solution was to use the PowerShell escape character “`” to mask all variable names.

command2 = command2 + string.Format(" `$shortc = `$shell.CreateShortcut(\"C:\\ClusterStorage\\Volume2\\test.lnk\"); `$shortc.TargetPath = \"{0}\\_VMBackup\\{2}\\{1}\"; `$shortc.Save();", backupDir, vm.Name, date);
Jan Chrbolka
  • 4,184
  • 2
  • 29
  • 38
  • While it's a good idea to reuse an allready declared variable.. it didn't solve my initial problem. Unexpected token ")". Why? Argh!! – 000000000000000000000 Jan 14 '15 at 07:57
  • As I said, a stab in the dark, sorry... There is just not enough information to start troubleshooting. Can you get the contents of your command2 variable, or if the code won't execute at all, post the whole code? – Jan Chrbolka Jan 14 '15 at 22:49
  • Thanks for that. So to start with, your code does generate the appropriate PowerShell command to create a string. So I'm as stumped as you are. Then I have noticed your error. "... andard'; .Save();" this looks like the end of your PS command "{1}\"; $shortc.Save();" with the variable blanked out. I have seen this before where PowerShell will replace variables with values (blank) in a command that is passed in as a string. Try adding another set of single quotes around your execution statement. I have updated my answer above. – Jan Chrbolka Jan 16 '15 at 05:30
  • Sry - didn't help either. They are still blank and now it complains about the first character '. >:[ My guess is that since these are objects, powershell can not display them as strings in the error message, but I have no idea if that is true. Still the same problem as the last few thousand hours I guess. ^~^ – 000000000000000000000 Jan 16 '15 at 07:16
  • One more thing to try is to add PowerShell escape character before each variable name. So PS escape ' just before each $... – Jan Chrbolka Jan 16 '15 at 08:12
  • Just before every $? Because '$xyz didn't do it ^.^ – 000000000000000000000 Jan 16 '15 at 08:36
  • The error you're getting looks like it's comming from the PowerShell where it checks integrity of the script before execution. So the script fails and does not get executed. Based on the error output something is stripping out variable names. This can happen when passing out PS scripts in a string. To prevent that there are a few things you can do. Enclose whole string in single quotes. Enclose whole string in {}, or put escape character before all meaningful characters in the string. I.e. '$name will be parsed as $name and not evaluated, which is what is happening, I think. I do have an evalu – Jan Chrbolka Jan 16 '15 at 08:46
  • I have an evaluation copy of VMMC, so might have a play over the weekend. – Jan Chrbolka Jan 16 '15 at 08:48
  • OK - the character I have to use in front of $ is this: `. Now I get "... andard'; $Shortcut.Save();" with the same error (calling "Save" with "0" arguments) This is kinda freeking me out :3 – 000000000000000000000 Jan 16 '15 at 10:19
  • Now we're getting somewhere! :P The PS script is executing and the error is a PS runtime error. Not very helpfull but this exact error can mean that you saving your shortcut to a path that does not exist. Try putting some extra quotes around your path statements and make sure they exist. Maybe start with a known hardcoded path. – Jan Chrbolka Jan 16 '15 at 10:30
  • Wow...it works! :D Apparently the path has to exist first. {caugh} I hate powershell {caugh} But at least now my boss won't have to spend the weekend hating me xD – 000000000000000000000 Jan 16 '15 at 10:49
  • Good stuff! Yep, PowerShell can be a real pain in the a#@#$ sometimes, I love it. When I get a chance I will update the answer. Maybe it can save someone else a few hours. – Jan Chrbolka Jan 16 '15 at 10:59