From 6ea51f46786f4c407b6c356bb805328349bc2c04 Mon Sep 17 00:00:00 2001 From: Stan Lagun Date: Wed, 3 Jan 2018 00:56:33 -0800 Subject: [PATCH] Message signing implementation for legacy Windows agent Also does a minor code refactoring to address R# warnings + add Apache 2.0 headers Change-Id: I1486909950828993400a75aa320c73adba4c086f Partial-Blueprint: message-signing --- .../ExecutionPlanGenerator.csproj | 6 +- .../ExecutionPlanGenerator/Program.cs | 27 ++- .../Properties/AssemblyInfo.cs | 17 +- .../ExecutionPlanGenerator/packages.config | 2 +- contrib/windows-agent/WindowsAgent/App.config | 10 +- .../WindowsAgent/ExecutionPlan.cs | 23 +- contrib/windows-agent/WindowsAgent/Message.cs | 41 ++++ .../{RabbitMqClient.cs => MessageSource.cs} | 114 +++++---- .../windows-agent/WindowsAgent/MqMessage.cs | 30 --- .../WindowsAgent/PlanExecutor.cs | 222 ++++++++++++------ contrib/windows-agent/WindowsAgent/Program.cs | 109 +++++---- .../WindowsAgent/Properties/AssemblyInfo.cs | 17 +- .../WindowsAgent/ServiceManager.cs | 37 ++- .../WindowsAgent/SignatureVerifier.cs | 60 +++++ .../WindowsAgent/WindowsAgent.csproj | 40 ++-- .../WindowsAgent/WindowsService.cs | 26 +- .../WindowsAgent/WindowsServiceInstaller.cs | 17 +- .../WindowsAgent/packages.config | 7 +- 18 files changed, 538 insertions(+), 267 deletions(-) create mode 100644 contrib/windows-agent/WindowsAgent/Message.cs rename contrib/windows-agent/WindowsAgent/{RabbitMqClient.cs => MessageSource.cs} (53%) delete mode 100644 contrib/windows-agent/WindowsAgent/MqMessage.cs create mode 100644 contrib/windows-agent/WindowsAgent/SignatureVerifier.cs diff --git a/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj b/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj index 5c63e534..8ec2009e 100644 --- a/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj +++ b/contrib/windows-agent/ExecutionPlanGenerator/ExecutionPlanGenerator.csproj @@ -32,14 +32,12 @@ 4 - - ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - diff --git a/contrib/windows-agent/ExecutionPlanGenerator/Program.cs b/contrib/windows-agent/ExecutionPlanGenerator/Program.cs index 798c2ae5..82e6b645 100644 --- a/contrib/windows-agent/ExecutionPlanGenerator/Program.cs +++ b/contrib/windows-agent/ExecutionPlanGenerator/Program.cs @@ -1,19 +1,34 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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 + internal class Command { public string Name { get; set; } public Dictionary Arguments { get; set; } } - class ExecutionPlan + + internal class ExecutionPlan { public List Scripts { get; set; } public List Commands { get; set; } @@ -21,9 +36,9 @@ namespace Mirantis.Murano } - class Program + static class Program { - static void Main(string[] args) + public static void Main(string[] args) { if (args.Length < 1 || args.Length > 2) { diff --git a/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs b/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs index 54a148f4..49749bb6 100644 --- a/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs +++ b/contrib/windows-agent/ExecutionPlanGenerator/Properties/AssemblyInfo.cs @@ -1,4 +1,19 @@ -using System.Reflection; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/contrib/windows-agent/ExecutionPlanGenerator/packages.config b/contrib/windows-agent/ExecutionPlanGenerator/packages.config index b82a8b0d..ee51c237 100644 --- a/contrib/windows-agent/ExecutionPlanGenerator/packages.config +++ b/contrib/windows-agent/ExecutionPlanGenerator/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/contrib/windows-agent/WindowsAgent/App.config b/contrib/windows-agent/WindowsAgent/App.config index 85d96b37..a2b7c155 100644 --- a/contrib/windows-agent/WindowsAgent/App.config +++ b/contrib/windows-agent/WindowsAgent/App.config @@ -1,10 +1,10 @@ - +
- + @@ -19,10 +19,12 @@ - + - + + + diff --git a/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs b/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs index 9761d36a..27ab4540 100644 --- a/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs +++ b/contrib/windows-agent/WindowsAgent/ExecutionPlan.cs @@ -1,12 +1,23 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Mirantis.Murano.WindowsAgent { - class ExecutionPlan + internal class ExecutionPlan { public class Command { @@ -17,5 +28,7 @@ namespace Mirantis.Murano.WindowsAgent public string[] Scripts { get; set; } public LinkedList Commands { get; set; } public int RebootOnCompletion { get; set; } + + public long Stamp { get; set; } } } diff --git a/contrib/windows-agent/WindowsAgent/Message.cs b/contrib/windows-agent/WindowsAgent/Message.cs new file mode 100644 index 00000000..e7e2d8e5 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/Message.cs @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; + +namespace Mirantis.Murano.WindowsAgent +{ + internal class Message : IDisposable + { + private readonly Action ackFunc; + + public Message(Action ackFunc) + { + this.ackFunc = ackFunc; + } + + public Message() + { + } + + public string Body { get; set; } + public string Id { get; set; } + + public void Dispose() + { + ackFunc(); + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/RabbitMqClient.cs b/contrib/windows-agent/WindowsAgent/MessageSource.cs similarity index 53% rename from contrib/windows-agent/WindowsAgent/RabbitMqClient.cs rename to contrib/windows-agent/WindowsAgent/MessageSource.cs index 6d42b51e..974122ae 100644 --- a/contrib/windows-agent/WindowsAgent/RabbitMqClient.cs +++ b/contrib/windows-agent/WindowsAgent/MessageSource.cs @@ -1,25 +1,38 @@ -using System; -using System.Collections.Generic; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; 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 + internal class MessageSource : IDisposable { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly Logger log = LogManager.GetCurrentClassLogger(); private static readonly ConnectionFactory connectionFactory; + private static readonly string queueName; private IConnection currentConnecton; + private readonly SignatureVerifier signatureVerifier; + - static RabbitMqClient() + static MessageSource() { var ssl = new SslOption { Enabled = bool.Parse(ConfigurationManager.AppSettings["rabbitmq.ssl"] ?? "false"), @@ -45,49 +58,65 @@ namespace Mirantis.Murano.WindowsAgent RequestedHeartbeat = 10, Ssl = ssl }; + queueName = ConfigurationManager.AppSettings["rabbitmq.inputQueue"]; } - public RabbitMqClient() + public MessageSource() { - + this.signatureVerifier = new SignatureVerifier(Encoding.ASCII.GetBytes(queueName)); } - public MqMessage GetMessage() + public Message GetMessage() { - var queueName = ConfigurationManager.AppSettings["rabbitmq.inputQueue"] ?? Dns.GetHostName().ToLower(); try { - IConnection connection = null; + IConnection connection; 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 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 - }; + while (true) + { + var e = consumer.Queue.Dequeue(); + Action ackFunc = delegate + { + session.BasicAck(e.DeliveryTag, false); + session.BasicCancel(consumeTag); + session.Close(); + }; + + byte[] signature = null; + if (e.BasicProperties.Headers.ContainsKey("signature")) + { + signature = (byte[]) e.BasicProperties.Headers["signature"]; + } + + if (this.signatureVerifier.Verify(e.Body, signature)) + { + return new Message(ackFunc) { + Body = Encoding.UTF8.GetString(e.Body), + Id = e.BasicProperties.MessageId, + }; + } + + log.Warn("Dropping message with invalid/missing signature"); + session.BasicReject(e.DeliveryTag, false); + } } - catch (Exception exception) + catch (Exception) { - - Dispose(); - throw; + if (this.currentConnecton == null) return null; + Dispose(); + throw; } } - public void SendResult(MqMessage message) + public void SendResult(Message message) { var exchangeName = ConfigurationManager.AppSettings["rabbitmq.resultExchange"] ?? ""; var resultRoutingKey = ConfigurationManager.AppSettings["rabbitmq.resultRoutingKey"] ?? "-execution-results"; @@ -95,22 +124,14 @@ namespace Mirantis.Murano.WindowsAgent try { - IConnection connection = null; + IConnection connection; 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.Persistent = durable; basicProperties.MessageId = message.Id; basicProperties.ContentType = "application/json"; session.BasicPublish(exchangeName, resultRoutingKey, basicProperties, Encoding.UTF8.GetBytes(message.Body)); @@ -129,18 +150,13 @@ namespace Mirantis.Murano.WindowsAgent { try { - if (this.currentConnecton != null) - { - this.currentConnecton.Close(); - } + var connection = this.currentConnecton; + this.currentConnecton = null; + connection.Close(); } catch { } - finally - { - this.currentConnecton = null; - } } } } diff --git a/contrib/windows-agent/WindowsAgent/MqMessage.cs b/contrib/windows-agent/WindowsAgent/MqMessage.cs deleted file mode 100644 index 76e2faab..00000000 --- a/contrib/windows-agent/WindowsAgent/MqMessage.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 index 0ce7a349..82ae9fea 100644 --- a/contrib/windows-agent/WindowsAgent/PlanExecutor.cs +++ b/contrib/windows-agent/WindowsAgent/PlanExecutor.cs @@ -1,4 +1,19 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections; using System.Collections.Generic; using System.IO; @@ -12,9 +27,10 @@ using Newtonsoft.Json; namespace Mirantis.Murano.WindowsAgent { - class PlanExecutor + internal class PlanExecutor { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly Logger log = LogManager.GetCurrentClassLogger(); + private long lastStamp = -1; class ExecutionResult { @@ -22,37 +38,45 @@ namespace Mirantis.Murano.WindowsAgent public object Result { get; set; } } - private readonly string path; + private readonly string baseDir; - public PlanExecutor(string path) + public PlanExecutor(string baseDir) { - this.path = path; + this.baseDir = baseDir; } public bool RebootNeeded { get; set; } - public void Execute() + public void Execute(string path) { RebootNeeded = false; - var resultPath = this.path + ".result"; + var resultPath = path + ".result"; + var tmpResultPath = resultPath + ".tmp"; Runspace runSpace = null; try { - var plan = JsonConvert.DeserializeObject(File.ReadAllText(this.path)); - List currentResults = null; + var plan = JsonConvert.DeserializeObject(File.ReadAllText(path)); + List currentResults; try { - currentResults = File.Exists(resultPath) ? - JsonConvert.DeserializeObject>(File.ReadAllText(resultPath)) : + currentResults = File.Exists(tmpResultPath) ? + JsonConvert.DeserializeObject>(File.ReadAllText(tmpResultPath)) : new List(); } catch(Exception exception) { - Log.WarnException("Cannot deserialize previous execution result", exception); + log.Warn(exception, "Cannot deserialize previous execution result"); currentResults = new List(); } - runSpace = RunspaceFactory.CreateRunspace(); + var lastStamp = GetLastStamp(); + if (plan.Stamp > 0 && plan.Stamp <= lastStamp) + { + log.Warn("Dropping old/duplicate plan"); + return; + } + + runSpace = RunspaceFactory.CreateRunspace(); runSpace.Open(); var runSpaceInvoker = new RunspaceInvoke(runSpace); @@ -63,36 +87,39 @@ namespace Mirantis.Murano.WindowsAgent foreach (var script in plan.Scripts) { runSpaceInvoker.Invoke(Encoding.UTF8.GetString(Convert.FromBase64String(script))); - Log.Debug("Loaded script #{0}", ++index); + 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); + 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); - } - } + if (command.Name != null) + { + 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())))); + log.Info("Executing {0} {1}", command.Name, string.Join(" ", + (command.Arguments ?? new Dictionary()).Select( + t => string.Format("{0}={1}", t.Key, t.Value?.ToString() ?? "null")))); - pipeline.Commands.Add(psCommand); + pipeline.Commands.Add(psCommand); + } - try + try { var result = pipeline.Invoke(); - Log.Debug("Command {0} executed", command.Name); + log.Debug("Command {0} executed", command.Name); if (result != null) { currentResults.Add(new ExecutionResult { @@ -104,21 +131,18 @@ namespace Mirantis.Murano.WindowsAgent 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; - } - } + 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); + log.Warn(exception, "Exception while executing command " + command.Name); currentResults.Add(new ExecutionResult { IsException = true, @@ -132,10 +156,14 @@ namespace Mirantis.Murano.WindowsAgent { plan.Commands.RemoveFirst(); File.WriteAllText(path, JsonConvert.SerializeObject(plan)); - File.WriteAllText(resultPath, JsonConvert.SerializeObject(currentResults)); + File.WriteAllText(tmpResultPath, JsonConvert.SerializeObject(currentResults)); } } runSpace.Close(); + if (plan.Stamp > 0) + { + SetLastStamp(plan.Stamp); + } var executionResult = JsonConvert.SerializeObject(new ExecutionResult { IsException = false, Result = currentResults @@ -152,11 +180,12 @@ namespace Mirantis.Murano.WindowsAgent RebootNeeded = true; } } - File.WriteAllText(resultPath, executionResult); + File.Delete(tmpResultPath); + File.WriteAllText(resultPath, executionResult); } catch (Exception exception) { - Log.WarnException("Exception while processing execution plan", exception); + log.Warn(exception, "Exception while processing execution plan"); File.WriteAllText(resultPath, JsonConvert.SerializeObject(new ExecutionResult { IsException = true, Result = exception.Message @@ -173,38 +202,33 @@ namespace Mirantis.Murano.WindowsAgent catch {} } - Log.Debug("Finished processing of execution plan"); + log.Debug("Finished processing of execution plan"); } } private static object ConvertArgument(object arg) { - if (arg is JArray) + switch (arg) { - var array = arg as JArray; - return array.Select(ConvertArgument).ToArray(); + case JArray array: + return array.Select(ConvertArgument).ToArray(); + case JValue value: + return value.Value; + case JObject dict: + var result = new Hashtable(); + foreach (var item in dict) + { + result.Add(item.Key, ConvertArgument(item.Value)); + } + return result; } - 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; + + return arg; } private static object SerializePsObject(PSObject obj) { - if (obj.BaseObject is PSCustomObject) + if (obj.BaseObject is PSCustomObject) { var result = new Dictionary(); foreach (var property in obj.Properties.Where(p => p.IsGettable)) @@ -219,15 +243,61 @@ namespace Mirantis.Murano.WindowsAgent } return result; } - else if (obj.BaseObject is IEnumerable) - { - return ((IEnumerable) obj.BaseObject).Select(SerializePsObject).ToArray(); - } - else - { - return obj.BaseObject; - } + + if (obj.BaseObject is IEnumerable objects) + { + return objects.Select(SerializePsObject).ToArray(); + } + + return obj.BaseObject; } + + private long GetLastStamp() + { + if (this.lastStamp >= 0) + { + return this.lastStamp; + } + + var path = Path.Combine(this.baseDir, "stamp.txt"); + if (File.Exists(path)) + { + try + { + var stampData = File.ReadAllText(path); + this.lastStamp = long.Parse(stampData); + } + catch (Exception e) + { + this.lastStamp = 0; + } + } + else + { + this.lastStamp = 0; + } + + return this.lastStamp; + } + + private void SetLastStamp(long value) + { + var path = Path.Combine(this.baseDir, "stamp.txt"); + try + { + File.WriteAllText(path, value.ToString()); + } + catch (Exception e) + { + log.Error(e, "Cannot persist last stamp"); + throw; + } + finally + { + this.lastStamp = value; + } + } } + } diff --git a/contrib/windows-agent/WindowsAgent/Program.cs b/contrib/windows-agent/WindowsAgent/Program.cs index 582ceafd..7396ddd6 100644 --- a/contrib/windows-agent/WindowsAgent/Program.cs +++ b/contrib/windows-agent/WindowsAgent/Program.cs @@ -1,55 +1,69 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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.Security.AccessControl; +using System.Security.Principal; using System.Threading; using NLog; namespace Mirantis.Murano.WindowsAgent { [DisplayName("Murano Agent")] - sealed public class Program : WindowsService + public sealed class Program : WindowsService { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly Logger log = LogManager.GetCurrentClassLogger(); private volatile bool stop; private Thread thread; - private RabbitMqClient rabbitMqClient; + private MessageSource messageSource; private int delayFactor = 1; private string plansDir; - static void Main(string[] args) + public static void Main(string[] args) { - Start(new Program(), args); + Start(new Program(), args); } protected override void OnStart(string[] args) { - base.OnStart(args); + base.OnStart(args); - Log.Info("Version 0.5.4"); + log.Info("Version 0.6"); - this.rabbitMqClient = new RabbitMqClient(); + this.messageSource = new MessageSource(); var basePath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); this.plansDir = Path.Combine(basePath, "plans"); - - if (!Directory.Exists(plansDir)) { - Directory.CreateDirectory(plansDir); + Directory.CreateDirectory(plansDir); } this.thread = new Thread(Loop); this.thread.Start(); } - void Loop() + private void Loop() { - const string unknownName = "unknown"; + const string unknownName = "unknown"; + var executor = new PlanExecutor(this.plansDir); while (!stop) { try @@ -57,40 +71,45 @@ namespace Mirantis.Murano.WindowsAgent 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; + var id = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(file)); 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 }); + log.Info("Sending results for {0}", id); + messageSource.SendResult(new Message { 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(); + using (var message = messageSource.GetMessage()) + { + if (message == null) + { + return; + } + 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); + } } else { var id = Path.GetFileNameWithoutExtension(path); - Log.Info("Executing exising plan {0}", id); + log.Info("Executing exising plan {0}", id); } - var executor = new PlanExecutor(path); - executor.Execute(); + + executor.Execute(path); File.Delete(path); delayFactor = 1; @@ -104,23 +123,13 @@ namespace Mirantis.Murano.WindowsAgent { WaitOnException(exception); } - } - } private void Reboot() { - Log.Info("Going for 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 @@ -130,24 +139,23 @@ namespace Mirantis.Murano.WindowsAgent catch (Exception exception) { - Log.FatalException("Reboot exception", exception); + log.Fatal(exception, "Reboot exception"); } finally { - Log.Info("Waiting for reboot"); + log.Info("Waiting for reboot"); for (var i = 0; i < 10 * 60 * 5 && !stop; i++) { Thread.Sleep(100); } - Log.Info("Done waiting for reboot"); + log.Info("Done waiting for reboot"); } - } private void WaitOnException(Exception exception) { if (stop) return; - Log.WarnException("Exception in main loop", exception); + log.Warn(exception, "Exception in main loop"); var i = 0; while (!stop && i < 10 * (delayFactor * delayFactor)) { @@ -160,9 +168,8 @@ namespace Mirantis.Murano.WindowsAgent protected override void OnStop() { stop = true; - this.rabbitMqClient.Dispose(); + this.messageSource.Dispose(); base.OnStop(); } - } } diff --git a/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs b/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs index f7d169bb..44779d64 100644 --- a/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs +++ b/contrib/windows-agent/WindowsAgent/Properties/AssemblyInfo.cs @@ -1,4 +1,19 @@ -using System.Reflection; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; diff --git a/contrib/windows-agent/WindowsAgent/ServiceManager.cs b/contrib/windows-agent/WindowsAgent/ServiceManager.cs index 6bf4d494..24afc66c 100644 --- a/contrib/windows-agent/WindowsAgent/ServiceManager.cs +++ b/contrib/windows-agent/WindowsAgent/ServiceManager.cs @@ -1,4 +1,19 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Configuration.Install; using System.Reflection; using System.ServiceProcess; @@ -15,7 +30,7 @@ namespace Mirantis.Murano.WindowsAgent this.serviceName = serviceName; } - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly Logger log = LogManager.GetCurrentClassLogger(); public bool Restart(string[] args, TimeSpan timeout) { @@ -26,7 +41,7 @@ namespace Mirantis.Murano.WindowsAgent service.Stop(); service.WaitForStatus(ServiceControllerStatus.Stopped, timeout); - Log.Info("Service is stopped"); + log.Info("Service is stopped"); // count the rest of the timeout var millisec2 = TimeSpan.FromMilliseconds(Environment.TickCount); @@ -34,12 +49,12 @@ namespace Mirantis.Murano.WindowsAgent service.Start(args); service.WaitForStatus(ServiceControllerStatus.Running, timeout); - Log.Info("Service has started"); + log.Info("Service has started"); return true; } catch (Exception ex) { - Log.ErrorException("Cannot restart service " + serviceName, ex); + log.Error(ex, "Cannot restart service " + serviceName); return false; } } @@ -55,7 +70,7 @@ namespace Mirantis.Murano.WindowsAgent } catch (Exception ex) { - Log.ErrorException("Cannot stop service " + serviceName, ex); + log.Error(ex, "Cannot stop service " + serviceName); return false; } } @@ -71,7 +86,7 @@ namespace Mirantis.Murano.WindowsAgent } catch (Exception ex) { - Log.ErrorException("Cannot start service " + serviceName, ex); + log.Error(ex, "Cannot start service " + serviceName); return false; } } @@ -81,11 +96,11 @@ namespace Mirantis.Murano.WindowsAgent try { ManagedInstallerClass.InstallHelper( - new string[] { Assembly.GetEntryAssembly().Location }); + new[] { Assembly.GetEntryAssembly().Location }); } catch(Exception ex) { - Log.ErrorException("Cannot install service " + serviceName, ex); + log.Error(ex, "Cannot install service " + serviceName); return false; } return true; @@ -96,11 +111,11 @@ namespace Mirantis.Murano.WindowsAgent try { ManagedInstallerClass.InstallHelper( - new string[] { "/u", Assembly.GetEntryAssembly().Location }); + new[] { "/u", Assembly.GetEntryAssembly().Location }); } catch (Exception ex) { - Log.ErrorException("Cannot uninstall service " + serviceName, ex); + log.Error(ex, "Cannot uninstall service " + serviceName); return false; } return true; diff --git a/contrib/windows-agent/WindowsAgent/SignatureVerifier.cs b/contrib/windows-agent/WindowsAgent/SignatureVerifier.cs new file mode 100644 index 00000000..ef781f55 --- /dev/null +++ b/contrib/windows-agent/WindowsAgent/SignatureVerifier.cs @@ -0,0 +1,60 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Configuration; +using System.IO; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; + +namespace Mirantis.Murano.WindowsAgent +{ + internal class SignatureVerifier + { + private readonly ISigner signer; + private readonly byte[] salt; + + public SignatureVerifier(byte[] salt) + { + var keyStr = ConfigurationManager.AppSettings["engine.key"]; + if (string.IsNullOrEmpty(keyStr)) return; + + var reader = new StringReader(keyStr); + var key = (RsaKeyParameters) new PemReader(reader).ReadObject(); + this.signer = SignerUtilities.GetSigner("SHA256withRSA"); + this.signer.Init(false, key); + this.salt = salt; + } + + public bool Verify(byte[] data, byte[] signature) + { + if (this.signer == null) + { + return true; + } + + if (signature == null) + { + return false; + } + + this.signer.Reset(); + this.signer.BlockUpdate(this.salt, 0, this.salt.Length); + this.signer.BlockUpdate(data, 0, data.Length); + return this.signer.VerifySignature(signature); + } + } +} diff --git a/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj b/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj index 34387e02..1aaae50d 100644 --- a/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj +++ b/contrib/windows-agent/WindowsAgent/WindowsAgent.csproj @@ -1,5 +1,5 @@  - + Debug @@ -9,9 +9,10 @@ Properties Mirantis.Murano.WindowsAgent WindowsAgent - v4.0 + v4.5 512 - Client + + AnyCPU @@ -34,15 +35,22 @@ 4 false + + + + - - ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + ..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll - - ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - ..\packages\RabbitMQ.Client.3.0.2\lib\net30\RabbitMQ.Client.dll + + ..\packages\NLog.4.4.12\lib\net45\NLog.dll + + + ..\packages\RabbitMQ.Client.3.6.9\lib\net45\RabbitMQ.Client.dll @@ -53,22 +61,18 @@ C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll - - - - - - + Component - + + Component @@ -77,7 +81,9 @@ - + + Designer + diff --git a/contrib/windows-agent/WindowsAgent/WindowsService.cs b/contrib/windows-agent/WindowsAgent/WindowsService.cs index 1748e047..61218fe1 100644 --- a/contrib/windows-agent/WindowsAgent/WindowsService.cs +++ b/contrib/windows-agent/WindowsAgent/WindowsService.cs @@ -1,4 +1,19 @@ -using System; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.ComponentModel; using System.IO; using System.Linq; @@ -10,8 +25,7 @@ namespace Mirantis.Murano.WindowsAgent { public abstract class WindowsService : ServiceBase { - private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public bool RunningAsService { get; private set; } + private static readonly Logger log = LogManager.GetCurrentClassLogger(); protected static void Start(WindowsService service, string[] arguments) { @@ -39,14 +53,12 @@ namespace Mirantis.Murano.WindowsAgent } 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(); @@ -81,14 +93,14 @@ namespace Mirantis.Murano.WindowsAgent protected override void OnStart(string[] args) { - Log.Info("Service {0} started", ServiceName); + log.Info("Service {0} started", ServiceName); base.OnStart(args); } protected override void OnStop() { - Log.Info("Service {0} exited", ServiceName); + log.Info("Service {0} exited", ServiceName); base.OnStop(); } } diff --git a/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs b/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs index c737b28c..d85aa9e3 100644 --- a/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs +++ b/contrib/windows-agent/WindowsAgent/WindowsServiceInstaller.cs @@ -1,4 +1,19 @@ -using System.ComponentModel; +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to you under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel; using System.Configuration.Install; using System.Linq; using System.Reflection; diff --git a/contrib/windows-agent/WindowsAgent/packages.config b/contrib/windows-agent/WindowsAgent/packages.config index 7aabef8e..ed746b6b 100644 --- a/contrib/windows-agent/WindowsAgent/packages.config +++ b/contrib/windows-agent/WindowsAgent/packages.config @@ -1,6 +1,7 @@  - - - + + + + \ No newline at end of file