diff --git a/contrib/windows-agent/ExecutionPlanGenerator/App.config b/contrib/windows-agent/ExecutionPlanGenerator/App.config new file mode 100644 index 00000000..8e156463 --- /dev/null +++ b/contrib/windows-agent/ExecutionPlanGenerator/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj b/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj new file mode 100644 index 00000000..5c63e534 --- /dev/null +++ b/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj @@ -0,0 +1,62 @@ + + + + + Debug + AnyCPU + {501BE151-4B8C-4355-88DC-3AEF1921B2D7} + Exe + Properties + Mirantis.Murano + ExecutionPlanGenerator + v4.5 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/contrib/windows-agent/ExecutionPlanGenerator/Program.cs b/contrib/windows-agent/ExecutionPlanGenerator/Program.cs new file mode 100644 index 00000000..798c2ae5 --- /dev/null +++ b/contrib/windows-agent/ExecutionPlanGenerator/Program.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Mirantis.Murano +{ + class Command + { + public string Name { get; set; } + public Dictionary Arguments { get; set; } + } + class ExecutionPlan + { + public List Scripts { get; set; } + public List Commands { get; set; } + public int RebootOnCompletion { get; set; } + } + + + class Program + { + static void Main(string[] args) + { + if (args.Length < 1 || args.Length > 2) + { + Console.WriteLine("Usage: ExecutionPlanGenerator inputfile [outputfile]"); + return; + } + + var outFile = args.Length == 2 ? args[1] : null; + + var plan = new ExecutionPlan { + Scripts = new List(), + Commands = new List() + }; + + + + var lines = File.ReadAllLines(args[0]); + + + foreach (var statement in lines + .Select(t => t.Split(new[] { ' ', '\t' }, 2)) + .Where(t => t.Length == 2) + .Select(t => new Tuple(t[0].Trim().ToLower(), t[1].Trim()))) + { + switch (statement.Item1) + { + case "include": + Include(statement.Item2, plan, args[0]); + break; + case "call": + Call(statement.Item2, plan); + break; + case "reboot": + plan.RebootOnCompletion = int.Parse(statement.Item2); + break; + case "out": + if (args.Length < 2) + { + var path = statement.Item2; + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(Path.GetDirectoryName(args[0]), path); + } + outFile = path; + } + break; + } + } + + var data = JsonConvert.SerializeObject(plan, Formatting.Indented); + if (outFile == null) + { + Console.WriteLine(data); + } + else + { + File.WriteAllText(outFile, data); + } + } + + private static void Call(string line, ExecutionPlan plan) + { + var parts = line.Split(new[] { ' ', '\t'}, 2); + var command = new Command() { + Name = parts[0].Trim(), + Arguments = new Dictionary() + }; + + + if (parts.Length == 2) + { + foreach (var x in parts[1] + .Split(',') + .Select(t => t.Split('=')) + .Where(t => t.Length == 2) + .Select(t => new KeyValuePair(t[0].Trim(), t[1].Trim()))) + { + object value = null; + long num; + bool boolean; + if (x.Value.StartsWith("\"")) + { + value = x.Value.Substring(1, x.Value.Length - 2); + } + else if (long.TryParse(x.Value, out num)) + { + value = num; + } + else if (bool.TryParse(x.Value, out boolean)) + { + value = boolean; + } + else + { + continue; + } + command.Arguments.Add(x.Key, value); + } + } + plan.Commands.Add(command); + } + + private static void Include(string file, ExecutionPlan plan, string dslPath) + { + var path = file; + if (!Path.IsPathRooted(file)) + { + path = Path.Combine(Path.GetDirectoryName(dslPath), path); + } + + var text = File.ReadAllText(path, Encoding.UTF8); + plan.Scripts.Add(Convert.ToBase64String(Encoding.UTF8.GetBytes(text))); + } + } +} diff --git a/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs b/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..54a148f4 --- /dev/null +++ b/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ConsoleApplication1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ConsoleApplication1")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9aab688a-ce5f-402e-8891-2d7b4ae85ea3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/contrib/windows-agent/ExecutionPlanGenerator/packages.config b/contrib/windows-agent/ExecutionPlanGenerator/packages.config new file mode 100644 index 00000000..b82a8b0d --- /dev/null +++ b/contrib/windows-agent/ExecutionPlanGenerator/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/contrib/windows-agent/README.rst b/contrib/windows-agent/README.rst new file mode 100644 index 00000000..743eabab --- /dev/null +++ b/contrib/windows-agent/README.rst @@ -0,0 +1,8 @@ +Murano Windows Agent +==================== + +Murano Windows Agent is an initial version of Murano Agent. +Currently, it's outdated and not supported. + +The main difference with the new Python agent is support of running Powershell. +After this support will be added to Python Agent, Windows Agent will be dropped. diff --git a/contrib/windows-agent/Tools/NuGet.exe b/contrib/windows-agent/Tools/NuGet.exe new file mode 100644 index 00000000..4645f4b3 Binary files /dev/null and b/contrib/windows-agent/Tools/NuGet.exe differ diff --git a/contrib/windows-agent/WindowsAgent.sln b/contrib/windows-agent/WindowsAgent.sln new file mode 100644 index 00000000..b77d57cb --- /dev/null +++ b/contrib/windows-agent/WindowsAgent.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsAgent", "WindowsAgent\WindowsAgent.csproj", "{F7E2A8D5-6D24-4651-A4BC-1024D59F4903}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExecutionPlanGenerator", "ExecutionPlanGenerator\ExecutionPlanGenerator.csproj", "{501BE151-4B8C-4355-88DC-3AEF1921B2D7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903}.Release|Any CPU.Build.0 = Release|Any CPU + {501BE151-4B8C-4355-88DC-3AEF1921B2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {501BE151-4B8C-4355-88DC-3AEF1921B2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {501BE151-4B8C-4355-88DC-3AEF1921B2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {501BE151-4B8C-4355-88DC-3AEF1921B2D7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/contrib/windows-agent/WindowsAgent/App.config b/contrib/windows-agent/WindowsAgent/App.config new file mode 100644 index 00000000..85d96b37 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/App.config @@ -0,0 +1,28 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs b/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs new file mode 100644 index 00000000..9761d36a --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Mirantis.Murano.WindowsAgent +{ + class ExecutionPlan + { + public class Command + { + public string Name { get; set; } + public Dictionary Arguments { get; set; } + } + + public string[] Scripts { get; set; } + public LinkedList Commands { get; set; } + public int RebootOnCompletion { get; set; } + } +} diff --git a/contrib/windows-agent/WindowsAgent/MqMessage.cs b/contrib/windows-agent/WindowsAgent/MqMessage.cs new file mode 100644 index 00000000..76e2faab --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/MqMessage.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Mirantis.Murano.WindowsAgent +{ + class MqMessage + { + private readonly Action ackFunc; + + public MqMessage(Action ackFunc) + { + this.ackFunc = ackFunc; + } + + public MqMessage() + { + } + + public string Body { get; set; } + public string Id { get; set; } + + public void Ack() + { + ackFunc(); + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/PlanExecutor.cs b/contrib/windows-agent/WindowsAgent/PlanExecutor.cs new file mode 100644 index 00000000..628eeb4d --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/PlanExecutor.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Text; +using Newtonsoft.Json.Linq; +using NLog; +using Newtonsoft.Json; + +namespace Mirantis.Murano.WindowsAgent +{ + class PlanExecutor + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + class ExecutionResult + { + public bool IsException { get; set; } + public object Result { get; set; } + } + + private readonly string path; + + public PlanExecutor(string path) + { + this.path = path; + } + + public bool RebootNeeded { get; set; } + + public void Execute() + { + RebootNeeded = false; + var resultPath = this.path + ".result"; + Runspace runSpace = null; + try + { + var plan = JsonConvert.DeserializeObject(File.ReadAllText(this.path)); + List currentResults = null; + try + { + currentResults = File.Exists(resultPath) ? + JsonConvert.DeserializeObject>(File.ReadAllText(resultPath)) : + new List(); + } + catch(Exception exception) + { + Log.WarnException("Cannot deserialize previous execution result", exception); + currentResults = new List(); + } + + runSpace = RunspaceFactory.CreateRunspace(); + runSpace.Open(); + + var runSpaceInvoker = new RunspaceInvoke(runSpace); + runSpaceInvoker.Invoke("Set-ExecutionPolicy Unrestricted"); + if (plan.Scripts != null) + { + var index = 0; + foreach (var script in plan.Scripts) + { + runSpaceInvoker.Invoke(Encoding.UTF8.GetString(Convert.FromBase64String(script))); + Log.Debug("Loaded script #{0}", ++index); + } + } + + while (plan.Commands != null && plan.Commands.Any()) + { + var command = plan.Commands.First(); + Log.Debug("Preparing to execute command {0}", command.Name); + + var pipeline = runSpace.CreatePipeline(); + var psCommand = new Command(command.Name); + if (command.Arguments != null) + { + foreach (var kvp in command.Arguments) + { + var value = ConvertArgument(kvp.Value); + psCommand.Parameters.Add(kvp.Key, value); + } + } + + Log.Info("Executing {0} {1}", command.Name, string.Join(" ", + (command.Arguments ?? new Dictionary()).Select( + t => string.Format("{0}={1}", t.Key, t.Value == null ? "null" : t.Value.ToString())))); + + pipeline.Commands.Add(psCommand); + + try + { + var result = pipeline.Invoke(); + Log.Debug("Command {0} executed", command.Name); + if (result != null) + { + currentResults.Add(new ExecutionResult { + IsException = false, + Result = result.Select(SerializePsObject).Where(obj => obj != null).ToList() + }); + } + } + catch (Exception exception) + { + object additionInfo = null; + if (exception is ActionPreferenceStopException) + { + var apse = exception as ActionPreferenceStopException; + if (apse.ErrorRecord != null) + { + additionInfo = new { + ScriptStackTrace = apse.ErrorRecord.ScriptStackTrace, + PositionMessage = apse.ErrorRecord.InvocationInfo.PositionMessage + }; + exception = apse.ErrorRecord.Exception; + } + } + + + Log.WarnException("Exception while executing command " + command.Name, exception); + currentResults.Add(new ExecutionResult + { + IsException = true, + Result = new[] { + exception.GetType().FullName, exception.Message, command.Name, additionInfo + } + }); + break; + } + finally + { + plan.Commands.RemoveFirst(); + File.WriteAllText(path, JsonConvert.SerializeObject(plan)); + File.WriteAllText(resultPath, JsonConvert.SerializeObject(currentResults)); + } + } + runSpace.Close(); + var executionResult = JsonConvert.SerializeObject(new ExecutionResult { + IsException = false, + Result = currentResults + }, Formatting.Indented); + + if (plan.RebootOnCompletion > 0) + { + if (plan.RebootOnCompletion == 1) + { + RebootNeeded = !currentResults.Any(t => t.IsException); + } + else + { + RebootNeeded = true; + } + } + File.WriteAllText(resultPath, executionResult); + } + catch (Exception exception) + { + Log.WarnException("Exception while processing execution plan", exception); + File.WriteAllText(resultPath, JsonConvert.SerializeObject(new ExecutionResult { + IsException = true, + Result = exception.Message + }, Formatting.Indented)); + } + finally + { + if (runSpace != null) + { + try + { + runSpace.Close(); + } + catch + {} + } + Log.Debug("Finished processing of execution plan"); + } + } + + private static object ConvertArgument(object arg) + { + if (arg is JArray) + { + var array = arg as JArray; + return array.Select(ConvertArgument).ToArray(); + } + else if (arg is JValue) + { + var value = (JValue) arg; + return value.Value; + } + else if (arg is JObject) + { + var dict = (JObject)arg; + var result = new Hashtable(); + foreach (var item in dict) + { + result.Add(item.Key, ConvertArgument(item.Value)); + } + return result; + } + return arg; + } + + private static object SerializePsObject(PSObject obj) + { + if (obj.BaseObject is PSCustomObject) + { + var result = new Dictionary(); + foreach (var property in obj.Properties.Where(p => p.IsGettable)) + { + try + { + result[property.Name] = property.Value.ToString(); + } + catch + { + } + } + return result; + } + else if (obj.BaseObject is IEnumerable) + { + return ((IEnumerable) obj.BaseObject).Select(SerializePsObject).ToArray(); + } + else + { + return obj.BaseObject; + } + } + } + +} diff --git a/contrib/windows-agent/WindowsAgent/Program.cs b/contrib/windows-agent/WindowsAgent/Program.cs new file mode 100644 index 00000000..5aeba8a8 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/Program.cs @@ -0,0 +1,168 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Net; +using System.Text; +using System.Threading; +using NLog; + +namespace Mirantis.Murano.WindowsAgent +{ + [DisplayName("Murano Agent")] + sealed public class Program : WindowsService + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private volatile bool stop; + private Thread thread; + private RabbitMqClient rabbitMqClient; + private int delayFactor = 1; + private string plansDir; + + static void Main(string[] args) + { + Start(new Program(), args); + } + + protected override void OnStart(string[] args) + { + base.OnStart(args); + + Log.Info("Version 0.5.3"); + + this.rabbitMqClient = new RabbitMqClient(); + + var basePath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + this.plansDir = Path.Combine(basePath, "plans"); + + + if (!Directory.Exists(plansDir)) + { + Directory.CreateDirectory(plansDir); + } + + this.thread = new Thread(Loop); + this.thread.Start(); + } + + void Loop() + { + const string unknownName = "unknown"; + while (!stop) + { + try + { + foreach (var file in Directory.GetFiles(this.plansDir, "*.json.result") + .Where(file => !File.Exists(Path.Combine(this.plansDir, Path.GetFileNameWithoutExtension(file))))) + { + var id = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file)) ?? unknownName; + if (id.Equals(unknownName, StringComparison.InvariantCultureIgnoreCase)) + { + id = ""; + } + + var result = File.ReadAllText(file); + Log.Info("Sending results for {0}", id ?? unknownName); + rabbitMqClient.SendResult(new MqMessage { Body = result, Id = id }); + File.Delete(file); + } + + var path = Directory.EnumerateFiles(this.plansDir, "*.json").FirstOrDefault(); + if (path == null) + { + var message = rabbitMqClient.GetMessage(); + var id = message.Id; + if(string.IsNullOrEmpty(id)) + { + id = unknownName; + } + + path = Path.Combine(this.plansDir, string.Format("{0}.json", id)); + File.WriteAllText(path, message.Body); + Log.Info("Received new execution plan {0}", id); + message.Ack(); + } + else + { + var id = Path.GetFileNameWithoutExtension(path); + Log.Info("Executing exising plan {0}", id); + } + var executor = new PlanExecutor(path); + executor.Execute(); + File.Delete(path); + delayFactor = 1; + + if (stop) break; + if (executor.RebootNeeded) + { + Reboot(); + } + } + catch (Exception exception) + { + WaitOnException(exception); + } + + } + + } + + private void Reboot() + { + Log.Info("Going for reboot!!"); + LogManager.Flush(); + /*try + { + System.Diagnostics.Process.Start("shutdown.exe", "-r -t 0"); + } + catch (Exception ex) + { + Log.ErrorException("Cannot execute shutdown.exe", ex); + }*/ + + + try + { + PowerShell.Create().AddCommand("Restart-Computer").AddParameter("Force").Invoke(); + } + catch (Exception exception) + { + + Log.FatalException("Reboot exception", exception); + } + finally + { + Log.Info("Waiting for reboot"); + for (var i = 0; i < 10 * 60 * 5 && !stop; i++) + { + Thread.Sleep(100); + } + Log.Info("Done waiting for reboot"); + } + + } + + private void WaitOnException(Exception exception) + { + if (stop) return; + Log.WarnException("Exception in main loop", exception); + var i = 0; + while (!stop && i < 10 * (delayFactor * delayFactor)) + { + Thread.Sleep(100); + i++; + } + delayFactor = Math.Min(delayFactor + 1, 6); + } + + protected override void OnStop() + { + stop = true; + this.rabbitMqClient.Dispose(); + base.OnStop(); + } + + } +} diff --git a/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs b/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..f7d169bb --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WindowsAgent")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsAgent")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9591bf2c-f38b-47e0-a39d-ea9849356371")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/contrib/windows-agent/WindowsAgent/RabbitMqClient.cs b/contrib/windows-agent/WindowsAgent/RabbitMqClient.cs new file mode 100644 index 00000000..6d42b51e --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/RabbitMqClient.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using NLog; +using RabbitMQ.Client; + +namespace Mirantis.Murano.WindowsAgent +{ + class RabbitMqClient : IDisposable + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly ConnectionFactory connectionFactory; + private IConnection currentConnecton; + + static RabbitMqClient() + { + var ssl = new SslOption { + Enabled = bool.Parse(ConfigurationManager.AppSettings["rabbitmq.ssl"] ?? "false"), + Version = SslProtocols.Default, + AcceptablePolicyErrors = bool.Parse(ConfigurationManager.AppSettings["rabbitmq.allowInvalidCA"] ?? "true") ? + SslPolicyErrors.RemoteCertificateChainErrors : SslPolicyErrors.None + }; + + var sslServerName = ConfigurationManager.AppSettings["rabbitmq.sslServerName"] ?? ""; + ssl.ServerName = sslServerName; + if (String.IsNullOrWhiteSpace(sslServerName)) + { + ssl.AcceptablePolicyErrors |= SslPolicyErrors.RemoteCertificateNameMismatch; + } + + connectionFactory = new ConnectionFactory { + HostName = ConfigurationManager.AppSettings["rabbitmq.host"] ?? "localhost", + UserName = ConfigurationManager.AppSettings["rabbitmq.user"] ?? "guest", + Password = ConfigurationManager.AppSettings["rabbitmq.password"] ??"guest", + Protocol = Protocols.DefaultProtocol, + VirtualHost = ConfigurationManager.AppSettings["rabbitmq.vhost"] ?? "/", + Port = int.Parse(ConfigurationManager.AppSettings["rabbitmq.port"] ?? "5672"), + RequestedHeartbeat = 10, + Ssl = ssl + }; + } + + public RabbitMqClient() + { + + } + + public MqMessage GetMessage() + { + var queueName = ConfigurationManager.AppSettings["rabbitmq.inputQueue"] ?? Dns.GetHostName().ToLower(); + try + { + IConnection connection = null; + lock (this) + { + connection = this.currentConnecton = this.currentConnecton ?? connectionFactory.CreateConnection(); + } + var session = connection.CreateModel(); + session.BasicQos(0, 1, false); + //session.QueueDeclare(queueName, true, false, false, null); + var consumer = new QueueingBasicConsumer(session); + var consumeTag = session.BasicConsume(queueName, false, consumer); + var e = (RabbitMQ.Client.Events.BasicDeliverEventArgs) consumer.Queue.Dequeue(); + Action ackFunc = delegate { + session.BasicAck(e.DeliveryTag, false); + session.BasicCancel(consumeTag); + session.Close(); + }; + + return new MqMessage(ackFunc) { + Body = Encoding.UTF8.GetString(e.Body), + Id = e.BasicProperties.MessageId + }; + } + catch (Exception exception) + { + + Dispose(); + throw; + } + } + + public void SendResult(MqMessage message) + { + var exchangeName = ConfigurationManager.AppSettings["rabbitmq.resultExchange"] ?? ""; + var resultRoutingKey = ConfigurationManager.AppSettings["rabbitmq.resultRoutingKey"] ?? "-execution-results"; + bool durable = bool.Parse(ConfigurationManager.AppSettings["rabbitmq.durableMessages"] ?? "true"); + + try + { + IConnection connection = null; + lock (this) + { + connection = this.currentConnecton = this.currentConnecton ?? connectionFactory.CreateConnection(); + } + var session = connection.CreateModel(); + /*if (!string.IsNullOrEmpty(resultQueue)) + { + //session.QueueDeclare(resultQueue, true, false, false, null); + if (!string.IsNullOrEmpty(exchangeName)) + { + session.ExchangeBind(exchangeName, resultQueue, resultQueue); + } + }*/ + var basicProperties = session.CreateBasicProperties(); + basicProperties.SetPersistent(durable); + basicProperties.MessageId = message.Id; + basicProperties.ContentType = "application/json"; + session.BasicPublish(exchangeName, resultRoutingKey, basicProperties, Encoding.UTF8.GetBytes(message.Body)); + session.Close(); + } + catch (Exception) + { + Dispose(); + throw; + } + } + + public void Dispose() + { + lock (this) + { + try + { + if (this.currentConnecton != null) + { + this.currentConnecton.Close(); + } + } + catch + { + } + finally + { + this.currentConnecton = null; + } + } + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/SampleExecutionPlan.json b/contrib/windows-agent/WindowsAgent/SampleExecutionPlan.json new file mode 100644 index 00000000..9522b70a --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/SampleExecutionPlan.json @@ -0,0 +1,37 @@ +{ + "Scripts": + [ + "ZnVuY3Rpb24gdDMgeyAxMjsgcmV0dXJuICJ0ZXN0IiB9", + "ZnVuY3Rpb24gTmV3LVBlcnNvbigpDQp7DQogIHBhcmFtICgkRmlyc3ROYW1lLCAkTGFzdE5hbWUsICRQaG9uZSkNCg0KICAkcGVyc29uID0gbmV3LW9iamVjdCBQU09iamVjdA0KDQogICRwZXJzb24gfCBhZGQtbWVtYmVyIC10eXBlIE5vdGVQcm9wZXJ0eSAtTmFtZSBGaXJzdCAtVmFsdWUgJEZpcnN0TmFtZQ0KICAkcGVyc29uIHwgYWRkLW1lbWJlciAtdHlwZSBOb3RlUHJvcGVydHkgLU5hbWUgTGFzdCAtVmFsdWUgJExhc3ROYW1lDQogICRwZXJzb24gfCBhZGQtbWVtYmVyIC10eXBlIE5vdGVQcm9wZXJ0eSAtTmFtZSBQaG9uZSAtVmFsdWUgJFBob25lDQoNCiAgcmV0dXJuICRwZXJzb24NCn0=", + "ZnVuY3Rpb24gVGVzdFRocm93KCkNCnsNCglUaHJvdyBbc3lzdGVtLkluZGV4T3V0T2ZSYW5nZUV4Y2VwdGlvbl0gDQp9" + ], + "Commands" : + [ + { + "Name": "New-Person", + "Arguments" : + { + "FirstName": "MyFirstName", + "LastName": "MyLastName", + "Phone": "123-456" + } + + }, + { + "Name": "t3", + "Arguments" : + { + } + + }, + { + "Name": "Get-Date", + + }, + { + "Name": "TestThrow", + + } + ], + "RebootOnCompletion": 0 +} \ No newline at end of file diff --git a/contrib/windows-agent/WindowsAgent/ServiceManager.cs b/contrib/windows-agent/WindowsAgent/ServiceManager.cs new file mode 100644 index 00000000..6bf4d494 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/ServiceManager.cs @@ -0,0 +1,111 @@ +using System; +using System.Configuration.Install; +using System.Reflection; +using System.ServiceProcess; +using NLog; + +namespace Mirantis.Murano.WindowsAgent +{ + public class ServiceManager + { + private readonly string serviceName; + + public ServiceManager(string serviceName) + { + this.serviceName = serviceName; + } + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public bool Restart(string[] args, TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + var millisec1 = TimeSpan.FromMilliseconds(Environment.TickCount); + + service.Stop(); + service.WaitForStatus(ServiceControllerStatus.Stopped, timeout); + Log.Info("Service is stopped"); + + // count the rest of the timeout + var millisec2 = TimeSpan.FromMilliseconds(Environment.TickCount); + timeout = timeout - (millisec2 - millisec1); + + service.Start(args); + service.WaitForStatus(ServiceControllerStatus.Running, timeout); + Log.Info("Service has started"); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot restart service " + serviceName, ex); + return false; + } + } + + public bool Stop(TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + service.Stop(); + service.WaitForStatus(ServiceControllerStatus.Stopped, timeout); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot stop service " + serviceName, ex); + return false; + } + } + + public bool Start(string[] args, TimeSpan timeout) + { + var service = new ServiceController(serviceName); + try + { + service.Start(args); + service.WaitForStatus(ServiceControllerStatus.Running, timeout); + return true; + } + catch (Exception ex) + { + Log.ErrorException("Cannot start service " + serviceName, ex); + return false; + } + } + + public bool Install() + { + try + { + ManagedInstallerClass.InstallHelper( + new string[] { Assembly.GetEntryAssembly().Location }); + } + catch(Exception ex) + { + Log.ErrorException("Cannot install service " + serviceName, ex); + return false; + } + return true; + } + + public bool Uninstall() + { + try + { + ManagedInstallerClass.InstallHelper( + new string[] { "/u", Assembly.GetEntryAssembly().Location }); + } + catch (Exception ex) + { + Log.ErrorException("Cannot uninstall service " + serviceName, ex); + return false; + } + return true; + } + + } + +} diff --git a/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj b/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj new file mode 100644 index 00000000..bf9aba9a --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj @@ -0,0 +1,95 @@ + + + + + Debug + AnyCPU + {F7E2A8D5-6D24-4651-A4BC-1024D59F4903} + Exe + Properties + Mirantis.Murano.WindowsAgent + WindowsAgent + v4.0 + 512 + Client + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\packages\RabbitMQ.Client.3.0.2\lib\net30\RabbitMQ.Client.dll + + + + + + + False + C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll + + + + + + + + + + + + + + Component + + + + + + Component + + + Component + + + + + + + + + + $(SolutionDir)Tools\nuget install $(ProjectDir)packages.config -o $(SolutionDir)Packages + + + \ No newline at end of file diff --git a/contrib/windows-agent/WindowsAgent/WindowsService.cs b/contrib/windows-agent/WindowsAgent/WindowsService.cs new file mode 100644 index 00000000..1748e047 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/WindowsService.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.ServiceProcess; +using NLog; + +namespace Mirantis.Murano.WindowsAgent +{ + public abstract class WindowsService : ServiceBase + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + public bool RunningAsService { get; private set; } + + protected static void Start(WindowsService service, string[] arguments) + { + Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)); + + if (arguments.Contains("/install", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Install(); + } + else if (arguments.Contains("/uninstall", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Uninstall(); + } + else if (arguments.Contains("/start", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Start(Environment.GetCommandLineArgs(), TimeSpan.FromMinutes(1)); + } + else if (arguments.Contains("/stop", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Stop(TimeSpan.FromMinutes(1)); + } + else if (arguments.Contains("/restart", StringComparer.OrdinalIgnoreCase)) + { + new ServiceManager(service.ServiceName).Restart(Environment.GetCommandLineArgs(), TimeSpan.FromMinutes(1)); + } + else if (!arguments.Contains("/console", StringComparer.OrdinalIgnoreCase)) + { + service.RunningAsService = true; + Run(service); + } + else + { + try + { + service.RunningAsService = false; + Console.Title = service.ServiceName; + service.OnStart(Environment.GetCommandLineArgs()); + service.WaitForExitSignal(); + } + finally + { + service.OnStop(); + service.Dispose(); + } + } + } + + protected WindowsService() + { + var displayNameAttribute = + this.GetType().GetCustomAttributes(typeof (DisplayNameAttribute), false).Cast(). + FirstOrDefault(); + if(displayNameAttribute != null) + { + ServiceName = displayNameAttribute.DisplayName; + } + } + + + protected virtual void WaitForExitSignal() + { + Console.WriteLine("Press ESC to exit"); + while (Console.ReadKey(true).Key != ConsoleKey.Escape) + { + } + } + + protected override void OnStart(string[] args) + { + Log.Info("Service {0} started", ServiceName); + + base.OnStart(args); + } + + protected override void OnStop() + { + Log.Info("Service {0} exited", ServiceName); + base.OnStop(); + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs b/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs new file mode 100644 index 00000000..c737b28c --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; +using System.Reflection; +using System.ServiceProcess; + +namespace Mirantis.Murano.WindowsAgent +{ + [RunInstaller(true)] + public class WindowsServiceInstaller : Installer + { + public WindowsServiceInstaller() + { + var processInstaller = new ServiceProcessInstaller { Account = ServiceAccount.LocalSystem }; + foreach (var type in Assembly.GetEntryAssembly().GetExportedTypes().Where(t => t.IsSubclassOf(typeof(ServiceBase)))) + { + var nameAttribute = type.GetCustomAttributes(typeof (DisplayNameAttribute), false) + .Cast().FirstOrDefault(); + if(nameAttribute == null) continue; + var serviceInstaller = new ServiceInstaller { + StartType = ServiceStartMode.Automatic, + ServiceName = nameAttribute.DisplayName, + DisplayName = nameAttribute.DisplayName + }; + var descriptionAttribute = type.GetCustomAttributes(typeof(DescriptionAttribute), false) + .Cast().FirstOrDefault(); + if(descriptionAttribute != null) + { + serviceInstaller.Description = descriptionAttribute.Description; + } + + Installers.Add(serviceInstaller); + } + + Installers.Add(processInstaller); + + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/packages.config b/contrib/windows-agent/WindowsAgent/packages.config new file mode 100644 index 00000000..7aabef8e --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/contrib/windows-agent/packages/repositories.config b/contrib/windows-agent/packages/repositories.config new file mode 100644 index 00000000..7753eee4 --- /dev/null +++ b/contrib/windows-agent/packages/repositories.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file