From 9c5f624dae96d1880def779dc939ed140ad9e323 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 28 Nov 2020 20:06:43 +0200 Subject: [PATCH] Added shutdown functionalities --- Directory.Build.props | 2 +- .../Controllers/ResurrectorController.cs | 7 ++ NetworkResurrector.Api/appsettings.json | 3 + .../CommandHandlers/PingMachineHandler.cs | 6 +- .../CommandHandlers/ShutdownMachineHandler.cs | 45 +++++++ .../Commands/PingMachine.cs | 6 +- .../Commands/ShutdownMachine.cs | 15 +++ .../DependencyInjectionExtensions.cs | 2 + .../Events/MachineShutdown.cs | 18 +++ .../NetworkResurrector.Application.csproj | 1 + .../Services/IPingService.cs | 2 +- .../Services/IShutdownService.cs | 9 ++ .../Services/IValidationService.cs | 7 ++ .../Services/PingService.cs | 10 +- .../Services/ShutdownService.cs | 115 ++++++++++++++++++ .../Services/ValidationService.cs | 39 ++++++ 16 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 NetworkResurrector.Application/CommandHandlers/ShutdownMachineHandler.cs create mode 100644 NetworkResurrector.Application/Commands/ShutdownMachine.cs create mode 100644 NetworkResurrector.Application/Events/MachineShutdown.cs create mode 100644 NetworkResurrector.Application/Services/IShutdownService.cs create mode 100644 NetworkResurrector.Application/Services/IValidationService.cs create mode 100644 NetworkResurrector.Application/Services/ShutdownService.cs create mode 100644 NetworkResurrector.Application/Services/ValidationService.cs diff --git a/Directory.Build.props b/Directory.Build.props index 9f04ccb..55a8528 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.0.0.0 + 1.0.0.1 Tudor Stanciu STA NetworkResurrector diff --git a/NetworkResurrector.Api/Controllers/ResurrectorController.cs b/NetworkResurrector.Api/Controllers/ResurrectorController.cs index 85e5c93..2515887 100644 --- a/NetworkResurrector.Api/Controllers/ResurrectorController.cs +++ b/NetworkResurrector.Api/Controllers/ResurrectorController.cs @@ -46,5 +46,12 @@ namespace NetworkResurrector.Api.Controllers var result = await _mediator.Send(pingMachine); return Ok(result); } + + [HttpPost("shutdown")] + public async Task ShutdownMachine([FromBody] ShutdownMachine shutdownMachine) + { + var result = await _mediator.Send(shutdownMachine); + return Ok(result); + } } } diff --git a/NetworkResurrector.Api/appsettings.json b/NetworkResurrector.Api/appsettings.json index 663fdfd..6944a6e 100644 --- a/NetworkResurrector.Api/appsettings.json +++ b/NetworkResurrector.Api/appsettings.json @@ -23,5 +23,8 @@ "Use": "Inhouse", "Options": [ "Inhouse", "Nikeee" ] } + }, + "Shutdown": { + } } diff --git a/NetworkResurrector.Application/CommandHandlers/PingMachineHandler.cs b/NetworkResurrector.Application/CommandHandlers/PingMachineHandler.cs index e323b3b..c440001 100644 --- a/NetworkResurrector.Application/CommandHandlers/PingMachineHandler.cs +++ b/NetworkResurrector.Application/CommandHandlers/PingMachineHandler.cs @@ -24,9 +24,9 @@ namespace NetworkResurrector.Application.CommandHandlers { try { - _logger.LogDebug($"Start pinging '{command.IPAddress}'."); - var (success, status) = await _pingService.PingMachine(command.IPAddress); - _logger.LogDebug($"Pinging on '{command.IPAddress}' finished. Status: {status}"); + _logger.LogDebug($"Start pinging '{command.IPAddressOrMachineName}'."); + var (success, status) = await _pingService.PingMachine(command.IPAddressOrMachineName); + _logger.LogDebug($"Pinging on '{command.IPAddressOrMachineName}' finished. Status: {status}"); return new MachinePinged(success, status); } catch (Exception ex) diff --git a/NetworkResurrector.Application/CommandHandlers/ShutdownMachineHandler.cs b/NetworkResurrector.Application/CommandHandlers/ShutdownMachineHandler.cs new file mode 100644 index 0000000..9225784 --- /dev/null +++ b/NetworkResurrector.Application/CommandHandlers/ShutdownMachineHandler.cs @@ -0,0 +1,45 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using NetworkResurrector.Application.Commands; +using NetworkResurrector.Application.Events; +using NetworkResurrector.Application.Services; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace NetworkResurrector.Application.CommandHandlers +{ + public class ShutdownMachineHandler : IRequestHandler + { + private readonly ILogger _logger; + private readonly IShutdownService _shutdownService; + + public ShutdownMachineHandler(ILogger logger, IShutdownService shutdownService) + { + _logger = logger; + _shutdownService = shutdownService; + } + + public async Task Handle(ShutdownMachine command, CancellationToken cancellationToken) + { + return await Task.Run(() => Handle(command)); + } + + private MachineShutdown Handle(ShutdownMachine command) + { + try + { + _logger.LogDebug($"Start shutting down '{command.IPAddressOrMachineName}'."); + var status = _shutdownService.ShutdownMachine(command.IPAddressOrMachineName); + _logger.LogDebug($"Shutting down machine '{command.IPAddressOrMachineName}' finished. Status: {status}"); + return new MachineShutdown(true, status); + } + catch (Exception ex) + { + var correlationIdMsg = $"CorrelationId: {command.Metadata.CorrelationId}"; + _logger.LogError(ex, $"An unexpected error has occurred. {correlationIdMsg}"); + return new MachineShutdown(false, $"{ex.Message} {correlationIdMsg}"); + } + } + } +} diff --git a/NetworkResurrector.Application/Commands/PingMachine.cs b/NetworkResurrector.Application/Commands/PingMachine.cs index eb5ac76..bde8a81 100644 --- a/NetworkResurrector.Application/Commands/PingMachine.cs +++ b/NetworkResurrector.Application/Commands/PingMachine.cs @@ -5,11 +5,11 @@ namespace NetworkResurrector.Application.Commands { public class PingMachine : Command { - public string IPAddress { get; set; } + public string IPAddressOrMachineName { get; set; } - public PingMachine(string ipAddress) : base(new Metadata() { CorrelationId = Guid.NewGuid() }) + public PingMachine(string ipAddressOrMachineName) : base(new Metadata() { CorrelationId = Guid.NewGuid() }) { - IPAddress = ipAddress; + IPAddressOrMachineName = ipAddressOrMachineName; } } } diff --git a/NetworkResurrector.Application/Commands/ShutdownMachine.cs b/NetworkResurrector.Application/Commands/ShutdownMachine.cs new file mode 100644 index 0000000..cc3d32d --- /dev/null +++ b/NetworkResurrector.Application/Commands/ShutdownMachine.cs @@ -0,0 +1,15 @@ +using NetworkResurrector.Application.Events; +using System; + +namespace NetworkResurrector.Application.Commands +{ + public class ShutdownMachine : Command + { + public string IPAddressOrMachineName { get; set; } + + public ShutdownMachine(string ipAddressOrMachineName) : base(new Metadata() { CorrelationId = Guid.NewGuid() }) + { + IPAddressOrMachineName = ipAddressOrMachineName; + } + } +} diff --git a/NetworkResurrector.Application/DependencyInjectionExtensions.cs b/NetworkResurrector.Application/DependencyInjectionExtensions.cs index a5804aa..eb31137 100644 --- a/NetworkResurrector.Application/DependencyInjectionExtensions.cs +++ b/NetworkResurrector.Application/DependencyInjectionExtensions.cs @@ -12,7 +12,9 @@ namespace NetworkResurrector.Application services.AddSingleton(); services.AddScoped(); services.AddStores(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } private static void AddStores(this IServiceCollection services) diff --git a/NetworkResurrector.Application/Events/MachineShutdown.cs b/NetworkResurrector.Application/Events/MachineShutdown.cs new file mode 100644 index 0000000..89f81ab --- /dev/null +++ b/NetworkResurrector.Application/Events/MachineShutdown.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NetworkResurrector.Application.Events +{ + public class MachineShutdown + { + public bool Success { get; } + public string Status { get; } + + public MachineShutdown(bool success, string status) + { + Success = success; + Status = status; + } + } +} diff --git a/NetworkResurrector.Application/NetworkResurrector.Application.csproj b/NetworkResurrector.Application/NetworkResurrector.Application.csproj index 78e26c2..a7efcd2 100644 --- a/NetworkResurrector.Application/NetworkResurrector.Application.csproj +++ b/NetworkResurrector.Application/NetworkResurrector.Application.csproj @@ -11,6 +11,7 @@ + diff --git a/NetworkResurrector.Application/Services/IPingService.cs b/NetworkResurrector.Application/Services/IPingService.cs index 2fcef1a..35cfb89 100644 --- a/NetworkResurrector.Application/Services/IPingService.cs +++ b/NetworkResurrector.Application/Services/IPingService.cs @@ -4,6 +4,6 @@ namespace NetworkResurrector.Application.Services { public interface IPingService { - Task<(bool success, string status)> PingMachine(string ipAddress); + Task<(bool success, string status)> PingMachine(string ipAddressOrMachineName); } } \ No newline at end of file diff --git a/NetworkResurrector.Application/Services/IShutdownService.cs b/NetworkResurrector.Application/Services/IShutdownService.cs new file mode 100644 index 0000000..ac6737e --- /dev/null +++ b/NetworkResurrector.Application/Services/IShutdownService.cs @@ -0,0 +1,9 @@ +namespace NetworkResurrector.Application.Services +{ + public interface IShutdownService + { + string ShutdownMachine(string ipAddressOrMachineName); + string ShutdownMachineWithManagementScope(string ipAddressOrMachineName); + string ShutdownMachineWithManagementScope(string ipAddressOrMachineName, string user, string password, string domain); + } +} \ No newline at end of file diff --git a/NetworkResurrector.Application/Services/IValidationService.cs b/NetworkResurrector.Application/Services/IValidationService.cs new file mode 100644 index 0000000..531c8a2 --- /dev/null +++ b/NetworkResurrector.Application/Services/IValidationService.cs @@ -0,0 +1,7 @@ +namespace NetworkResurrector.Application.Services +{ + public interface IValidationService + { + bool IsValidIPAddress(string ipAddress); + } +} \ No newline at end of file diff --git a/NetworkResurrector.Application/Services/PingService.cs b/NetworkResurrector.Application/Services/PingService.cs index 0f52bcb..89868db 100644 --- a/NetworkResurrector.Application/Services/PingService.cs +++ b/NetworkResurrector.Application/Services/PingService.cs @@ -10,9 +10,9 @@ namespace NetworkResurrector.Application.Services /// Ping machine by IP /// https://docs.microsoft.com/en-us/dotnet/api/system.net.networkinformation.ping?redirectedfrom=MSDN&view=net-5.0 /// - /// + /// /// (bool success, string status) - public async Task<(bool success, string status)> PingMachine(string ipAddress) + public async Task<(bool success, string status)> PingMachine(string ipAddressOrMachineName) { var ping = new Ping(); @@ -28,12 +28,12 @@ namespace NetworkResurrector.Application.Services byte[] buffer = Encoding.ASCII.GetBytes(data); int timeout = 120; - PingReply reply = await ping.SendPingAsync(ipAddress, timeout, buffer, options); + PingReply reply = await ping.SendPingAsync(ipAddressOrMachineName, timeout, buffer, options); if (reply.Status == IPStatus.Success) { var builder = new StringBuilder(); - builder.AppendLine($"Machine '{ipAddress}' has responded to ping."); + builder.AppendLine($"Machine '{ipAddressOrMachineName}' has responded to ping."); builder.AppendLine($"Address: {reply.Address}"); builder.AppendLine($"RoundTrip time: {reply.RoundtripTime}"); @@ -44,7 +44,7 @@ namespace NetworkResurrector.Application.Services return (true, builder.ToString()); } else - return (false, $"Machine '{ipAddress}' does not respond to ping. Status: {reply.Status}"); + return (false, $"Machine '{ipAddressOrMachineName}' does not respond to ping. Status: {reply.Status}"); } } } diff --git a/NetworkResurrector.Application/Services/ShutdownService.cs b/NetworkResurrector.Application/Services/ShutdownService.cs new file mode 100644 index 0000000..9712d04 --- /dev/null +++ b/NetworkResurrector.Application/Services/ShutdownService.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Management; + +namespace NetworkResurrector.Application.Services +{ + public class ShutdownService : IShutdownService + { + /// + /// https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-xp/bb491003(v=technet.10)?redirectedfrom=MSDN + /// + /// + /// + public string ShutdownMachine(string ipAddressOrMachineName) + { + Process commandProcess = new Process(); + + commandProcess.StartInfo.FileName = "cmd.exe"; + commandProcess.StartInfo.UseShellExecute = false; + commandProcess.StartInfo.CreateNoWindow = true; + commandProcess.StartInfo.RedirectStandardError = true; + commandProcess.StartInfo.RedirectStandardInput = true; + commandProcess.StartInfo.RedirectStandardOutput = true; + commandProcess.Start(); + commandProcess.StandardInput.WriteLine($"shutdown /r /m {ipAddressOrMachineName} /t 200 /f"); + commandProcess.StandardInput.WriteLine("exit"); + + for (; !commandProcess.HasExited;) //wait executed + { + System.Threading.Thread.Sleep(1); + } + + string errorOutput = commandProcess.StandardError.ReadToEnd(); + string output = commandProcess.StandardOutput.ReadToEnd(); + + if (commandProcess != null) + commandProcess.Dispose(); + + return $"Output:{Environment.NewLine}{output}{Environment.NewLine}{Environment.NewLine}Errors:{Environment.NewLine}{errorOutput}"; + } + + public string ShutdownMachineWithManagementScope(string ipAddressOrMachineName) + => ShutdownMachineWithManagementScope(ipAddressOrMachineName, false, null, null, null); + + /// + /// https://msdn.microsoft.com/en-us/library/system.management.managementscope(v=vs.110).aspx + /// + /// + /// + /// + /// + public string ShutdownMachineWithManagementScope(string ipAddressOrMachineName, string user, string password, string domain) + => ShutdownMachineWithManagementScope(ipAddressOrMachineName, true, user, password, domain); + + private string ShutdownMachineWithManagementScope(string ipAddressOrMachineName, bool useSpecificCredentials, string user, string password, string domain) + { + Validate(ipAddressOrMachineName, useSpecificCredentials, user, password, domain); + /* Build an options object for the remote connection + * if you plan to connect to the remote computer with a different user name and password than the one you are currently using + + ConnectionOptions options = new ConnectionOptions(); + + * and then set the options.Username and options.Password properties to the correct values + * and also set options.Authority = "ntlmdomain:DOMAIN"; + * and replace DOMAIN with the remote computer's domain. You can also use Kerberos instead of ntlmdomain. + */ + + ConnectionOptions options = new ConnectionOptions + { + Username = user, + Password = password, + Authority = $"ntlmdomain:{domain}" + }; + + // Make a connection to a remote computer. + ManagementScope scope = new ManagementScope($"\\\\{ipAddressOrMachineName}\\root\\cimv2", options); + scope.Connect(); + + //Query system for Operating System information + ObjectQuery query = new ObjectQuery("SELECT * FROM Win32_OperatingSystem"); + + ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query); + ManagementObjectCollection queryCollection = searcher.Get(); + + foreach (ManagementObject obj in queryCollection) + { + // Display the remote computer information + Console.WriteLine("Computer Name : {0}", obj["csname"]); + Console.WriteLine("Windows Directory : {0}", obj["WindowsDirectory"]); + Console.WriteLine("Operating System: {0}", obj["Caption"]); + Console.WriteLine("Version: {0}", obj["Version"]); + Console.WriteLine("Manufacturer : {0}", obj["Manufacturer"]); + + obj.InvokeMethod("ShutDown", null); //shutdown + } + + return "x"; + } + + private void Validate(string ipAddressOrMachineName, bool useSpecificCredentials, string user, string password, string domain) + { + if (string.IsNullOrEmpty(ipAddressOrMachineName)) + throw new ArgumentException("The provided ipAddressOrMachineName input is null or empty.", nameof(ipAddressOrMachineName)); + + if (useSpecificCredentials && OneOfStringsIsNullOrEmpty(user, password, domain)) + throw new ArgumentException($"One of user, password or domain inputs is null or empty."); + } + + private bool OneOfStringsIsNullOrEmpty(params string[] inputs) + { + return inputs.Any(z => string.IsNullOrEmpty(z)); + } + } +} diff --git a/NetworkResurrector.Application/Services/ValidationService.cs b/NetworkResurrector.Application/Services/ValidationService.cs new file mode 100644 index 0000000..567ac7b --- /dev/null +++ b/NetworkResurrector.Application/Services/ValidationService.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; + +namespace NetworkResurrector.Application.Services +{ + public class ValidationService : IValidationService + { + private readonly ILogger _logger; + + public ValidationService(ILogger logger) + { + _logger = logger; + } + + public bool IsValidIPAddress(string ipAddress) + { + if (IPAddress.TryParse(ipAddress, out IPAddress address)) + { + switch (address.AddressFamily) + { + case System.Net.Sockets.AddressFamily.InterNetwork: + _logger.LogDebug($"IP address '{ipAddress}' is v4."); + break; + case System.Net.Sockets.AddressFamily.InterNetworkV6: + _logger.LogDebug($"IP address '{ipAddress}' is v6."); + break; + default: + _logger.LogWarning($"IP address '{ipAddress}' is valid but is not v4 or v6! Family: [{address.AddressFamily}]"); + break; + } + + return true; + } + + return false; + } + } +}