Merged PR 69: Network resurrector notification sistem

- Added messaging
- Removed Netmash.Application.DataContracts dependency
- removed local CorrelationManager
- release notes
- agent GetSystemVersion query update
- Server SystemController
- Network resurrector notification sistem
- NotificationService update
- NotificationContext: rename MachineStatus to ActionStatus
- added readme file
- config update
- configs
- release notes update
master
Tudor Stanciu 2023-01-28 22:31:54 +00:00
parent c4b129a565
commit f732578fb5
56 changed files with 631 additions and 178 deletions

View File

@ -55,6 +55,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution items", "solution
dependencies.props = dependencies.props dependencies.props = dependencies.props
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
NuGet.config = NuGet.config NuGet.config = NuGet.config
README.md = README.md
ReleaseNotes.xml = ReleaseNotes.xml ReleaseNotes.xml = ReleaseNotes.xml
EndProjectSection EndProjectSection
EndProject EndProject

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# Network resurrector
Everything must be able to be managed remotely. Even the powered off servers. That's how Network resurrector appeared, the tool I wrote specifically to be able to wake up my machines that I don't need to be powered on all the time.
Network Resurrector is a system that comprises of five essential services which allow for the execution of its core functionality. To enable various additional features, such as the notification mechanism, supplementary components may be added to the system as an option.
## Main components
### Frontend
* The frontend component is a web application written in React JS that has the role of providing the user with a friendly visual interface through which to interact with the system.
### API
* The API component is a .NET 6 REST API that has the role of mediating the exchange of data and commands between the frontend component and the database or server component.
### Server
* The server component is a .NET 6 service specialized in executing 'WakeOnLAN', 'Ping' and 'Shutdown' actions for the machines in its network.
### Agent
* The agent is a .NET 6 service specialized in executing 'Shutdown', 'Restart', 'Sleep', 'Logout' and 'Lock' actions on the host machine on which it is installed. Each action can be executed at the time of launch or with a certain delay. If an action is requested with a delay and later the user changes his mind, he can cancel the action by executing the separate 'Cancel' type action.
* The need for the agent appeared after I realized that the server cannot perform any action on a machine, being outside of it.
* The agent solves this problem because it is installed directly on the targeted machine. Later, the API component can delegate the resolution of an action directly to the agent.
* Most of the time, the execution flow of an action is realized in the following way:
* The user initiates an action, such as starting or stopping a machine, by pressing the corresponding button in the user interface.
* The frontend component sends the command to the API.
* The API checks who is configured as performer for the respective action for the machine and sends the command to it.
* The performer of the action (Agent or Server) executes the command and responds on the flow with status.
* In most cases, the Server component handles the machine startup action while the Agent component manages the machine shutdown action.
* As is probably already obvious, the agent can be installed on as many machines as desired.
### Gundo
* Gundo is my personal identity server. It manages user authentication within the application and authorizes requests made by it. Further information about Gundo can be found on its dedicated page.
All communication between the five main components is done exclusively through HTTP.
## Secondary components
* NATS Streaming - Used to publish notifications about executed actions.
* Correo - Used to send by email the notifications generated by the system.
* Seq - Used to collect and view system logs.
## Notification system
The notification system is focused on system actions (Wake, Shutdown, etc), and the configuration of notifications is done in structures of the following form:
```
{
"To": [ "user@homelab.com" ],
"Subject": "Network resurrector: Machine {MACHINE_NAME} has been waked. Status: {ACTION_STATUS}",
"Body": "Hello,{NEWLINE:2}Network resurrector processed a command to start machine {MACHINE_FULLNAME} with IP {MACHINE_IP} at {SYSTEM_DATETIME}.{NEWLINE}The performer who was delegated for this action was {ACTION_PERFORMER}.{NEWLINE}Action status: {ACTION_STATUS}{NEWLINE:2}Have a nice day,{NEWLINE}Network resurrector notification system."
}
```
The texts can be written at the user's free choice, and the following placeholders can be used in their composition: ```{MACHINE_NAME}```, ```{MACHINE_FULLNAME}```, ```{MACHINE_IP}```, ```{ACTION_STATUS}```, ```{ACTION_PERFORMER}```, ```{SYSTEM_DATETIME}```, ```{ERROR_MESSGE}```, ```{NEWLINE}```, ```{NEWLINE:2}```
```{NEWLINE:x}``` is a dynamic placeholder. Any number can be written in place of the 'x' character and the notification system will add that many new lines.
## Database
Currently, the database server supported by the system is only Microsoft SQL Server. In the following versions, the system will also be compatible with PostgreSQL and SQLite.
## Logging
The logging functionality is managed with Serilog, and its configuration is done in the ```appsettings.json``` file. In addition to its standard configuration, Network resurrector also has a preconfigured area where two destinations for logs are available: SqlServer database and Seq. Each of the destinations can be activated or not. If logging in the console is sufficient, all additional logging destinations can be disabled.
This configuration area is:
```
"Logs": {
"SqlServer": {
"Enabled": false,
"Connection": "Server=<server>;Database=<database>;User Id=<user>;Password=<password>;"
},
"Seq": {
"Enabled": false,
"Url": "",
"ApiKey": ""
}
}
```
## Hosting
All the components of the system are written in cross-platform technologies, so its host can be any environment.

View File

@ -41,7 +41,13 @@
<Note> <Note>
<Version>1.1.0</Version> <Version>1.1.0</Version>
<Content> <Content>
.NET 6 upgrade Massive improvements
</Content> • .NET 6 upgrade
• Nuget packages upgrade
• Added Seq logging
• Added messaging and published notifications from command handlers
• Refactoring and code cleanup
• Added README.md file
</Content>
</Note> </Note>
</ReleaseNotes> </ReleaseNotes>

View File

@ -4,15 +4,16 @@
<MicrosoftExtensionsHostingPackageVersion>6.0.1</MicrosoftExtensionsHostingPackageVersion> <MicrosoftExtensionsHostingPackageVersion>6.0.1</MicrosoftExtensionsHostingPackageVersion>
<SerilogPackageVersion>4.1.0</SerilogPackageVersion> <SerilogPackageVersion>4.1.0</SerilogPackageVersion>
<SerilogExtensionsPackageVersion>3.1.0</SerilogExtensionsPackageVersion> <SerilogExtensionsPackageVersion>3.1.0</SerilogExtensionsPackageVersion>
<SerilogSinksConsolePackageVersion>4.0.1</SerilogSinksConsolePackageVersion> <SerilogSinksSeqPackageVersion>5.2.2</SerilogSinksSeqPackageVersion>
<SerilogSinksMSSqlServerPackageVersion>5.6.1</SerilogSinksMSSqlServerPackageVersion> <SerilogSinksMSSqlServerPackageVersion>5.6.1</SerilogSinksMSSqlServerPackageVersion>
<AutoMapperPackageVersion>10.1.1</AutoMapperPackageVersion> <AutoMapperPackageVersion>10.1.1</AutoMapperPackageVersion>
<AutoMapperExtensionsPackageVersion>8.1.1</AutoMapperExtensionsPackageVersion> <AutoMapperExtensionsPackageVersion>8.1.1</AutoMapperExtensionsPackageVersion>
<MediatRPackageVersion>9.0.0</MediatRPackageVersion> <MediatRPackageVersion>9.0.0</MediatRPackageVersion>
<NBBPackageVersion>6.0.30</NBBPackageVersion>
<EntityFrameworkCorePackageVersion>6.0.1</EntityFrameworkCorePackageVersion> <EntityFrameworkCorePackageVersion>6.0.1</EntityFrameworkCorePackageVersion>
<NetmashExtensionsSwaggerPackageVersion>1.0.6</NetmashExtensionsSwaggerPackageVersion> <NetmashExtensionsSwaggerPackageVersion>1.0.6</NetmashExtensionsSwaggerPackageVersion>
<NetmashApplicationPackageVersion>1.0.1</NetmashApplicationPackageVersion>
<NetmashSecurityAuthenticationPackageVersion>1.0.8</NetmashSecurityAuthenticationPackageVersion> <NetmashSecurityAuthenticationPackageVersion>1.0.8</NetmashSecurityAuthenticationPackageVersion>
<NetmashDatabaseMigrationPackageVersion>1.2.0</NetmashDatabaseMigrationPackageVersion> <NetmashDatabaseMigrationPackageVersion>1.2.0</NetmashDatabaseMigrationPackageVersion>
<CorreoPublishedLanguage>1.0.1</CorreoPublishedLanguage>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -10,7 +10,6 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Netmash.Application.DataContracts" Version="$(NetmashApplicationPackageVersion)" />
<PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" /> <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
using MediatR; using MediatR;
using Netmash.Application.DataContracts;
using System; using System;
using System.IO;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,31 +9,29 @@ namespace NetworkResurrector.Agent.Application.Queries
{ {
public class GetSystemVersion public class GetSystemVersion
{ {
public class Query : Query<Model> public class Query : IRequest<Model> { }
{
public Query() { }
}
public class Model public record Model(string Version, DateTime LastUpdateDate);
{
public string Version { get; set; }
public DateTime LastUpdateDate { get; set; }
}
public class QueryHandler : IRequestHandler<Query, Model> public class QueryHandler : IRequestHandler<Query, Model>
{ {
public QueryHandler() public QueryHandler() { }
{
}
public async Task<Model> Handle(Query request, CancellationToken cancellationToken) public async Task<Model> Handle(Query request, CancellationToken cancellationToken)
{ {
var result = new Model() var version = Environment.GetEnvironmentVariable("APP_VERSION");
{ var appDate = Environment.GetEnvironmentVariable("APP_DATE");
Version = "1.0.0",
LastUpdateDate = DateTime.Now
};
if (string.IsNullOrEmpty(version))
version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
if (!DateTime.TryParse(appDate, out var lastUpdateDate))
{
var location = Assembly.GetExecutingAssembly().Location;
lastUpdateDate = File.GetLastWriteTime(location);
}
var result = new Model(version, lastUpdateDate);
return await Task.FromResult(result); return await Task.FromResult(result);
} }
} }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Cancel : Command<CancelResult> public class Cancel : IRequest<CancelResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
} }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Lock : Command<LockResult> public class Lock : IRequest<LockResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
} }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Logout : Command<LogoutResult> public class Logout : IRequest<LogoutResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
} }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Restart : Command<RestartResult> public class Restart : IRequest<RestartResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
public ActionOptions Options { get; set; } public ActionOptions Options { get; set; }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Shutdown : Command<ShutdownResult> public class Shutdown : IRequest<ShutdownResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
public ActionOptions Options { get; set; } public ActionOptions Options { get; set; }

View File

@ -1,10 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.PublishedLanguage.Events; using NetworkResurrector.Agent.PublishedLanguage.Events;
namespace NetworkResurrector.Agent.PublishedLanguage.Commands namespace NetworkResurrector.Agent.PublishedLanguage.Commands
{ {
public class Sleep : Command<SleepResult> public class Sleep : IRequest<SleepResult>
{ {
public ActionOwner Owner { get; set; } public ActionOwner Owner { get; set; }
public ActionOptions Options { get; set; } public ActionOptions Options { get; set; }

View File

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Netmash.Application.DataContracts" Version="$(NetmashApplicationPackageVersion)" /> <PackageReference Include="MediatR" Version="$(MediatRPackageVersion)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -14,7 +14,6 @@
<PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" /> <PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" />
<PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" /> <PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" />
<PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" /> <PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" />
<PackageReference Include="Serilog.Sinks.Console" Version="$(SerilogSinksConsolePackageVersion)" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" /> <PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" />

View File

@ -3,6 +3,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NetworkResurrector.Agent.PublishedLanguage.Dto; using NetworkResurrector.Agent.PublishedLanguage.Dto;
using NetworkResurrector.Agent.Wrapper.Services; using NetworkResurrector.Agent.Wrapper.Services;
using NetworkResurrector.Api.Application.Extensions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Constants; using NetworkResurrector.Api.Domain.Constants;
using NetworkResurrector.Api.Domain.Repositories; using NetworkResurrector.Api.Domain.Repositories;
using NetworkResurrector.Api.PublishedLanguage.Commands; using NetworkResurrector.Api.PublishedLanguage.Commands;
@ -21,14 +23,16 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
private readonly INetworkRepository _repository; private readonly INetworkRepository _repository;
private readonly IResurrectorAgentService _resurrectorAgentService; private readonly IResurrectorAgentService _resurrectorAgentService;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly INotificationService _notificationService;
public ShutdownMachineHandler(ILogger<ShutdownMachineHandler> logger, IResurrectorService resurrectorService, INetworkRepository repository, IResurrectorAgentService resurrectorAgentService, IConfiguration configuration) public ShutdownMachineHandler(ILogger<ShutdownMachineHandler> logger, IResurrectorService resurrectorService, INetworkRepository repository, IResurrectorAgentService resurrectorAgentService, IConfiguration configuration, INotificationService notificationService)
{ {
_logger=logger; _logger=logger;
_resurrectorService=resurrectorService; _resurrectorService=resurrectorService;
_repository=repository; _repository=repository;
_resurrectorAgentService=resurrectorAgentService; _resurrectorAgentService=resurrectorAgentService;
_configuration=configuration; _configuration=configuration;
_notificationService=notificationService;
} }
public async Task<MachineShutdown> Handle(ShutdownMachine command, CancellationToken cancellationToken) public async Task<MachineShutdown> Handle(ShutdownMachine command, CancellationToken cancellationToken)
@ -42,7 +46,8 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
var ipAddressOrMachineName = machine.IPv4Address ?? machine.MachineName; var ipAddressOrMachineName = machine.IPv4Address ?? machine.MachineName;
MachineShutdown result; MachineShutdown result;
switch (powerConfiguration.Performer.PerformerCode) var performer = powerConfiguration.Performer.PerformerCode;
switch (performer)
{ {
case PowerActionPerformers.NETWORK_RESURRECTOR_SERVER: case PowerActionPerformers.NETWORK_RESURRECTOR_SERVER:
if (command.Delay.HasValue || command.Force) if (command.Delay.HasValue || command.Force)
@ -64,10 +69,12 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
break; break;
default: default:
throw new Exception($"Power action performer {powerConfiguration.Performer.PerformerCode} is not implemented."); throw new Exception($"Power action performer {performer} is not implemented.");
} }
_logger.LogDebug($"Machine {command.MachineId} shutdown finished. Success: {result.Success}; Status: {result.Status}"); _logger.LogDebug($"Machine {command.MachineId} shutdown finished. Success: {result.Success}; Status: {result.Status}");
var notificationContext = machine.ToNotificationContext(result.Status, performer);
await _notificationService.Notify(NotificationType.Shutdown, notificationContext, cancellationToken);
return result; return result;
} }

View File

@ -1,5 +1,7 @@
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NetworkResurrector.Api.Application.Extensions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Constants; using NetworkResurrector.Api.Domain.Constants;
using NetworkResurrector.Api.Domain.Repositories; using NetworkResurrector.Api.Domain.Repositories;
using NetworkResurrector.Api.PublishedLanguage.Commands; using NetworkResurrector.Api.PublishedLanguage.Commands;
@ -16,12 +18,14 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
private readonly ILogger<WakeMachineHandler> _logger; private readonly ILogger<WakeMachineHandler> _logger;
private readonly IResurrectorService _resurrectorService; private readonly IResurrectorService _resurrectorService;
private readonly INetworkRepository _repository; private readonly INetworkRepository _repository;
private readonly INotificationService _notificationService;
public WakeMachineHandler(ILogger<WakeMachineHandler> logger, IResurrectorService resurrectorService, INetworkRepository repository) public WakeMachineHandler(ILogger<WakeMachineHandler> logger, IResurrectorService resurrectorService, INetworkRepository repository, INotificationService notificationService)
{ {
_logger=logger; _logger=logger;
_resurrectorService=resurrectorService; _resurrectorService=resurrectorService;
_repository=repository; _repository=repository;
_notificationService=notificationService;
} }
public async Task<MachineWaked> Handle(WakeMachine command, CancellationToken cancellationToken) public async Task<MachineWaked> Handle(WakeMachine command, CancellationToken cancellationToken)
@ -33,7 +37,8 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
//log activity //log activity
MachineWaked result; MachineWaked result;
switch (powerConfiguration.Performer.PerformerCode) var performer = powerConfiguration.Performer.PerformerCode;
switch (performer)
{ {
case PowerActionPerformers.NETWORK_RESURRECTOR_SERVER: case PowerActionPerformers.NETWORK_RESURRECTOR_SERVER:
var wakeResult = await _resurrectorService.Wake(machine.MACAddress); var wakeResult = await _resurrectorService.Wake(machine.MACAddress);
@ -45,6 +50,8 @@ namespace NetworkResurrector.Api.Application.CommandHandlers
throw new Exception($"Power action performer {powerConfiguration.Performer.PerformerCode} is not implemented."); throw new Exception($"Power action performer {powerConfiguration.Performer.PerformerCode} is not implemented.");
} }
var notificationContext = machine.ToNotificationContext(result.Status, performer);
await _notificationService.Notify(NotificationType.Wake, notificationContext, cancellationToken);
return result; return result;
} }
} }

View File

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NetworkResurrector.Api.Application.Services; using NetworkResurrector.Api.Application.Services;
using NetworkResurrector.Api.Application.Services.Abstractions;
namespace NetworkResurrector.Api.Application namespace NetworkResurrector.Api.Application
{ {
@ -7,7 +8,7 @@ namespace NetworkResurrector.Api.Application
{ {
public static void AddApplicationServices(this IServiceCollection services) public static void AddApplicationServices(this IServiceCollection services)
{ {
services.AddScoped<CorrelationManager>(); services.AddSingleton<INotificationService, NotificationService>();
} }
} }
} }

View File

@ -0,0 +1,20 @@
using NetworkResurrector.Api.Domain.Entities;
using NetworkResurrector.Api.Domain.Models.Notifications;
namespace NetworkResurrector.Api.Application.Extensions
{
internal static class EntityExtensions
{
public static NotificationContext ToNotificationContext(this Machine machine, string actionStatus = null, string actionPerformer = null)
{
return new NotificationContext()
{
MachineName = machine.MachineName,
MachineFullName = machine.FullMachineName,
MachineIP = machine.IPv4Address,
ActionStatus = actionStatus,
ActionPerformer = actionPerformer
};
}
}
}

View File

@ -6,11 +6,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="$(AutoMapperPackageVersion)" /> <PackageReference Include="AutoMapper" Version="$(AutoMapperPackageVersion)" />
<PackageReference Include="Correo.PublishedLanguage" Version="$(CorreoPublishedLanguage)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Netmash.Application.DataContracts" Version="$(NetmashApplicationPackageVersion)" /> <PackageReference Include="NBB.Messaging.Abstractions" Version="$(NBBPackageVersion)" />
<PackageReference Include="NetworkResurrector.Agent.Wrapper" Version="1.0.3.1" /> <PackageReference Include="NetworkResurrector.Agent.Wrapper" Version="1.0.3.1" />
<PackageReference Include="NetworkResurrector.Server.Wrapper" Version="1.0.3.3" /> <PackageReference Include="NetworkResurrector.Server.Wrapper" Version="1.0.3.3" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,5 @@
using AutoMapper; using AutoMapper;
using MediatR; using MediatR;
using Netmash.Application.DataContracts;
using NetworkResurrector.Api.Domain.Repositories; using NetworkResurrector.Api.Domain.Repositories;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,10 +8,7 @@ namespace NetworkResurrector.Api.Application.Queries
{ {
public class GetMachines public class GetMachines
{ {
public class Query : Query<Model[]> public class Query : IRequest<Model[]> { }
{
public Query() { }
}
public class Model public class Model
{ {

View File

@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Netmash.Application.DataContracts;
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
@ -10,12 +9,7 @@ namespace NetworkResurrector.Api.Application.Queries
{ {
public class GetSystemVersion public class GetSystemVersion
{ {
public class Query : Query<Model> public class Query : IRequest<Model> { }
{
public Query()
{
}
}
public record Model(string Version, DateTime LastUpdateDate); public record Model(string Version, DateTime LastUpdateDate);

View File

@ -0,0 +1,14 @@
using NetworkResurrector.Api.Domain.Constants;
using NetworkResurrector.Api.Domain.Models.Notifications;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services.Abstractions
{
public interface INotificationService
{
Task Notify(NotificationType type, NotificationContext context, CancellationToken cancellationToken = default);
Task Notify(NotificationType type, string machineName, string machineFullName, string machineIP, string actionStatus, string actionPerformer, string errorMessage, CancellationToken cancellationToken = default);
Task NotifyError(string errorMessage, CancellationToken cancellationToken = default);
}
}

View File

@ -1,14 +0,0 @@
using System;
namespace NetworkResurrector.Api.Application.Services
{
public class CorrelationManager
{
public Guid CorrelationId { get; }
public CorrelationManager()
{
CorrelationId=Guid.NewGuid();
}
}
}

View File

@ -0,0 +1,138 @@
using Correo.PublishedLanguage.Commands;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NBB.Messaging.Abstractions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Constants;
using NetworkResurrector.Api.Domain.Models.Notifications;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services
{
internal class NotificationService : INotificationService
{
private readonly IMessageBusPublisher _messageBusPublisher;
private readonly IConfiguration _configuration;
private readonly ILogger<NotificationService> _logger;
public NotificationService(IMessageBusPublisher messageBusPublisher, IConfiguration configuration, ILogger<NotificationService> logger)
{
_messageBusPublisher=messageBusPublisher;
_configuration=configuration;
_logger=logger;
}
private Notification GetNotification(NotificationType type, NotificationContext context)
{
var template = _configuration.GetSection($"Notifications:{type}").Get<NotificationTemplate>();
var notification = new Notification()
{
To = template.To,
Subject = ReplacePlaceholders(template.Subject, context),
Body = ReplacePlaceholders(template.Body, context)
};
return notification;
}
private string ReplacePlaceholders(string text, NotificationContext context)
{
var regex = new Regex(@"\{[A-Z_:0-9]+\}");
MatchCollection matches = regex.Matches(text);
var distinctMatches = matches.OfType<Match>().Select(m => m.Value).Distinct();
foreach (var placeholder in distinctMatches)
{
var placeholderValue = GetPlaceholderValue(placeholder, context);
if (string.IsNullOrEmpty(placeholderValue))
_logger.LogWarning($"The placeholder '{placeholder}' was not recognized and was ignored.");
else
text = text.Replace(placeholder, placeholderValue);
}
return text;
}
private string GetPlaceholderValue(string placeholder, NotificationContext context)
{
return placeholder switch
{
NotificationPlaceholder.MACHINE_NAME => context.MachineName,
NotificationPlaceholder.MACHINE_FULLNAME => context.MachineFullName,
NotificationPlaceholder.MACHINE_IP => context.MachineIP,
NotificationPlaceholder.ACTION_STATUS => context.ActionStatus,
NotificationPlaceholder.ACTION_PERFORMER => context.ActionPerformer,
NotificationPlaceholder.SYSTEM_DATETIME => DateTime.Now.ToString(),
NotificationPlaceholder.ERROR_MESSGE => context.ErrorMessage,
NotificationPlaceholder.NEWLINE => Environment.NewLine,
_ => GetRegexPlaceholderValue(placeholder),
};
}
private string GetRegexPlaceholderValue(string placeholder)
{
var placeholderValue = GetNewLinesPlaceholderValue(placeholder);
// check here for other regex placeholders
return placeholderValue;
}
private string GetNewLinesPlaceholderValue(string placeholder)
{
var regex = new Regex(@"\{NEWLINE(?::\d+)?\}");
var match = regex.Match(placeholder);
if (!match.Success)
return null;
var linesCountRegex = new Regex(@"(?<=:)[0-9]+");
var linesCountMatch = linesCountRegex.Match(placeholder);
if (!linesCountMatch.Success)
return null;
var linesCount = Convert.ToInt32(linesCountMatch.Value);
var placeHolderValue = string.Concat(Enumerable.Repeat(Environment.NewLine, linesCount));
return placeHolderValue;
}
public async Task Notify(NotificationType type, NotificationContext context, CancellationToken cancellationToken = default)
{
var notification = GetNotification(type, context);
var cmd = new SendEmail()
{
Subject = notification.Subject,
Body = notification.Body,
IsBodyHtml = false,
To = notification.To.Select(z => new SendEmail.MailAddress(z))
};
await _messageBusPublisher.PublishAsync(cmd, cancellationToken);
}
public async Task NotifyError(string errorMessage, CancellationToken cancellationToken = default)
{
var context = new NotificationContext()
{
ErrorMessage = errorMessage
};
await Notify(NotificationType.Error, context, cancellationToken);
}
public async Task Notify(NotificationType type, string machineName, string machineFullName, string machineIP, string actionStatus, string actionPerformer, string errorMessage, CancellationToken cancellationToken = default)
{
var context = new NotificationContext()
{
MachineName = machineName,
MachineFullName = machineFullName,
MachineIP = machineIP,
ActionStatus = actionStatus,
ActionPerformer = actionPerformer,
ErrorMessage= errorMessage,
};
await Notify(type, context, cancellationToken);
}
}
}

View File

@ -0,0 +1,22 @@
namespace NetworkResurrector.Api.Domain.Constants
{
public enum NotificationType
{
Wake,
Shutdown,
Error
}
public struct NotificationPlaceholder
{
public const string
MACHINE_NAME = "{MACHINE_NAME}",
MACHINE_FULLNAME = "{MACHINE_FULLNAME}",
MACHINE_IP = "{MACHINE_IP}",
ACTION_STATUS = "{ACTION_STATUS}",
ACTION_PERFORMER = "{ACTION_PERFORMER}",
SYSTEM_DATETIME = "{SYSTEM_DATETIME}",
ERROR_MESSGE = "{ERROR_MESSGE}",
NEWLINE = "{NEWLINE}";
}
}

View File

@ -0,0 +1,9 @@
namespace NetworkResurrector.Api.Domain.Models.Notifications
{
public record Notification
{
public string[] To { get; init; }
public string Subject { get; init; }
public string Body { get; init; }
}
}

View File

@ -0,0 +1,12 @@
namespace NetworkResurrector.Api.Domain.Models.Notifications
{
public record NotificationContext
{
public string MachineName { get; set; }
public string MachineFullName { get; set; }
public string MachineIP { get; set; }
public string ActionStatus { get; set; }
public string ActionPerformer { get; set; }
public string ErrorMessage { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace NetworkResurrector.Api.Domain.Models.Notifications
{
public record NotificationTemplate
{
public string[] To { get; init; }
public string Subject { get; init; }
public string Body { get; init; }
}
}

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class CancelAction : Command<ActionCanceled> public class CancelAction : IRequest<ActionCanceled>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
public int ActionId { get; set; } public int ActionId { get; set; }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class LockMachine : Command<MachineLocked> public class LockMachine : IRequest<MachineLocked>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
} }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class LogoutUser : Command<UserLoggedOut> public class LogoutUser : IRequest<UserLoggedOut>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
} }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class PingMachine : Command<MachinePinged> public class PingMachine : IRequest<MachinePinged>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
} }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class RestartMachine : Command<MachineRestarted> public class RestartMachine : IRequest<MachineRestarted>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
public int? Delay { get; set; } public int? Delay { get; set; }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class ShutdownMachine : Command<MachineShutdown> public class ShutdownMachine : IRequest<MachineShutdown>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
public int? Delay { get; set; } public int? Delay { get; set; }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class SleepMachine : Command<MachineSlept> public class SleepMachine : IRequest<MachineSlept>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
public int? Delay { get; set; } public int? Delay { get; set; }

View File

@ -1,9 +1,9 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Api.PublishedLanguage.Events; using NetworkResurrector.Api.PublishedLanguage.Events;
namespace NetworkResurrector.Api.PublishedLanguage.Commands namespace NetworkResurrector.Api.PublishedLanguage.Commands
{ {
public class WakeMachine : Command<MachineWaked> public class WakeMachine : IRequest<MachineWaked>
{ {
public int MachineId { get; set; } public int MachineId { get; set; }
} }

View File

@ -4,8 +4,8 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Netmash.Application.DataContracts" Version="$(NetmashApplicationPackageVersion)" /> <PackageReference Include="MediatR" Version="$(MediatRPackageVersion)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NetworkResurrector.Api.Application.Services;
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@ -12,27 +11,23 @@ namespace NetworkResurrector.Api.Controllers
[ApiExplorerSettings(IgnoreApi = true)] [ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase public class ErrorsController : ControllerBase
{ {
private readonly CorrelationManager _correlationManager; public ErrorsController()
public ErrorsController(CorrelationManager correlationManager)
{ {
_correlationManager=correlationManager;
} }
internal record Error(int Status, string Title, Guid CorrelationId, string Message = null); internal record Error(int Status, string Title, string Message = null);
[Route("error")] [Route("error")]
public IActionResult HandleErrors() public IActionResult HandleErrors()
{ {
var correlationId = _correlationManager.CorrelationId;
var context = HttpContext.Features.Get<IExceptionHandlerFeature>(); var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error; var exception = context.Error;
return exception switch return exception switch
{ {
ValidationException => StatusCode(StatusCodes.Status404NotFound, new Error(StatusCodes.Status404NotFound, "Internal server error", correlationId, exception.Message)), ValidationException => StatusCode(StatusCodes.Status404NotFound, new Error(StatusCodes.Status404NotFound, "Internal server error", exception.Message)),
UnauthorizedAccessException => StatusCode(StatusCodes.Status401Unauthorized, new Error(StatusCodes.Status401Unauthorized, "Internal server error", correlationId)), UnauthorizedAccessException => StatusCode(StatusCodes.Status401Unauthorized, new Error(StatusCodes.Status401Unauthorized, "Internal server error")),
_ => StatusCode(StatusCodes.Status500InternalServerError, new Error(StatusCodes.Status500InternalServerError, "Internal server error", correlationId)), _ => StatusCode(StatusCodes.Status500InternalServerError, new Error(StatusCodes.Status500InternalServerError, "Internal server error")),
}; };
} }
} }

View File

@ -0,0 +1,8 @@
namespace NetworkResurrector.Api.Extensions
{
internal static class DataTypeExtensions
{
public static string Nullify(this string value)
=> string.IsNullOrWhiteSpace(value) ? null : value;
}
}

View File

@ -0,0 +1,41 @@
using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Configuration;
using Serilog.Sinks.MSSqlServer;
using System;
namespace NetworkResurrector.Api.Extensions
{
internal static class LoggingExtensions
{
internal static LoggerSinkConfiguration ConfiguredDestinations(this LoggerSinkConfiguration writeTo, IConfiguration configuration)
{
var sqlServerEnabled = configuration.GetValue<bool>("Logs:SqlServer:Enabled");
var sqlServerConnection = configuration.GetValue<string>("Logs:SqlServer:Connection");
var seqEnabled = configuration.GetValue<bool>("Logs:Seq:Enabled");
var seqUrl = configuration.GetValue<string>("Logs:Seq:Url");
var seqApiKey = configuration.GetValue<string>("Logs:Seq:ApiKey");
if (sqlServerEnabled && string.IsNullOrWhiteSpace(sqlServerConnection))
throw new Exception("If SqlServer logging is enabled, the SqlServer connection must be configured.");
if (seqEnabled && string.IsNullOrWhiteSpace(seqUrl))
throw new Exception("If Seq logging is enabled, the Seq URL must be configured.");
if (sqlServerEnabled)
{
var columnOptions = new ColumnOptions();
columnOptions.Store.Remove(StandardColumn.Properties);
columnOptions.Store.Remove(StandardColumn.MessageTemplate);
columnOptions.Store.Add(StandardColumn.LogEvent);
var mssqlSinkOptions = new MSSqlServerSinkOptions() { AutoCreateSqlTable = true, TableName = "__Logs" };
writeTo.MSSqlServer(sqlServerConnection, mssqlSinkOptions, columnOptions: columnOptions);
}
if (seqEnabled)
writeTo.Seq(seqUrl, apiKey: seqApiKey.Nullify());
return writeTo;
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NBB.Messaging.Abstractions;
using NetworkResurrector.Api.Services;
namespace NetworkResurrector.Api.Extensions
{
public static class MessagingExtensions
{
public static void AddMessageBus(this IServiceCollection services, IConfiguration configuration)
{
var enabled = configuration.GetValue<bool>("Messaging:Enabled", false);
if (enabled)
services.AddMessageBus().AddNatsTransport(configuration);
else
services.AddSingleton<IMessageBusPublisher, EmptyMessageBusPublisher>();
}
}
}

View File

@ -15,7 +15,6 @@ using NetworkResurrector.Api.Domain.Data;
using NetworkResurrector.Api.Services; using NetworkResurrector.Api.Services;
using NetworkResurrector.Server.Wrapper; using NetworkResurrector.Server.Wrapper;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Reflection;
namespace NetworkResurrector.Api.Extensions namespace NetworkResurrector.Api.Extensions
{ {
@ -33,7 +32,7 @@ namespace NetworkResurrector.Api.Extensions
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
// MediatR // MediatR
services.AddMediatR(GetMediatRAssemblies()); services.AddMediatR(typeof(Application.Queries.GetMachines).Assembly);
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));
@ -53,14 +52,11 @@ namespace NetworkResurrector.Api.Extensions
services.AddMigration(DatabaseType.SQLServer, MetadataLocation.Database); services.AddMigration(DatabaseType.SQLServer, MetadataLocation.Database);
services.AddDataAccess(); services.AddDataAccess();
// Application // Application services
services.AddApplicationServices(); services.AddApplicationServices();
}
private static Assembly[] GetMediatRAssemblies() // Messaging
{ services.AddMessageBus(configuration);
var assembly = typeof(Application.Queries.GetMachines).Assembly;
return new Assembly[] { assembly };
} }
public static void Configure(this IApplicationBuilder app) public static void Configure(this IApplicationBuilder app)

View File

@ -11,13 +11,14 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="NBB.Messaging.Nats" Version="$(NBBPackageVersion)" />
<PackageReference Include="Netmash.Extensions.Swagger" Version="$(NetmashExtensionsSwaggerPackageVersion)" /> <PackageReference Include="Netmash.Extensions.Swagger" Version="$(NetmashExtensionsSwaggerPackageVersion)" />
<PackageReference Include="Netmash.Infrastructure.DatabaseMigration" Version="$(NetmashDatabaseMigrationPackageVersion)" /> <PackageReference Include="Netmash.Infrastructure.DatabaseMigration" Version="$(NetmashDatabaseMigrationPackageVersion)" />
<PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" /> <PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" />
<PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" /> <PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" />
<PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" /> <PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" />
<PackageReference Include="Serilog.Sinks.Console" Version="$(SerilogSinksConsolePackageVersion)" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" /> <PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" />
<PackageReference Include="Serilog.Sinks.Seq" Version="$(SerilogSinksSeqPackageVersion)" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(MicrosoftExtensionsHostingPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="$(MicrosoftExtensionsHostingPackageVersion)" />

View File

@ -4,9 +4,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using NetworkResurrector.Api.Extensions; using NetworkResurrector.Api.Extensions;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
using System; using System;
namespace NetworkResurrector.Api namespace NetworkResurrector.Api
@ -19,27 +16,11 @@ namespace NetworkResurrector.Api
builder.Host.UseSerilog((_, lc) => builder.Host.UseSerilog((_, lc) =>
{ {
var connectionString = builder.Configuration.GetConnectionString("DatabaseConnection");
var loggingLevelParam = builder.Configuration.GetValue<string>("Logging:LogLevel:Default");
var loggingLevelOk = Enum.TryParse(loggingLevelParam, out LogEventLevel loggingLevel);
if (!loggingLevelOk)
throw new Exception($"Logging level '{loggingLevelParam}' is not valid.");
var loggingLevelSwitch = new LoggingLevelSwitch(loggingLevel);
var columnOptions = new ColumnOptions();
columnOptions.Store.Remove(StandardColumn.Properties);
columnOptions.Store.Remove(StandardColumn.MessageTemplate);
columnOptions.Store.Add(StandardColumn.LogEvent);
var mssqlSinkOptions = new MSSqlServerSinkOptions() { AutoCreateSqlTable = true, TableName = "__Logs" };
lc lc
.MinimumLevel.ControlledBy(loggingLevelSwitch) .ReadFrom.Configuration(builder.Configuration)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.Console() .WriteTo.Console()
.WriteTo.MSSqlServer(connectionString, mssqlSinkOptions, columnOptions: columnOptions); .WriteTo.ConfiguredDestinations(builder.Configuration);
}); });
builder.Services.ConfigureServices(builder.Configuration); builder.Services.ConfigureServices(builder.Configuration);

View File

@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;
using NBB.Messaging.Abstractions;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Services
{
internal class EmptyMessageBusPublisher : IMessageBusPublisher
{
private readonly ILogger<EmptyMessageBusPublisher> _logger;
public EmptyMessageBusPublisher(ILogger<EmptyMessageBusPublisher> logger)
{
_logger=logger;
}
public Task PublishAsync<T>(T message, MessagingPublisherOptions publisherOptions = null, CancellationToken cancellationToken = default)
{
_logger.LogDebug($"EmptyMessageBusPublisher: Message {message.GetType().FullName} was published.");
return Task.CompletedTask;
}
}
}

View File

@ -3,11 +3,23 @@
"ConnectionStrings": { "ConnectionStrings": {
"DatabaseConnection": "Server=#########;Database=#########;User Id=#########;Password=#########;MultipleActiveResultSets=true" "DatabaseConnection": "Server=#########;Database=#########;User Id=#########;Password=#########;MultipleActiveResultSets=true"
}, },
"Logging": { "Serilog": {
"LogLevel": { "MinimumLevel": {
"Default": "Debug", "Default": "Information",
"Microsoft": "Warning", "Override": {
"Microsoft.Hosting.Lifetime": "Information" "Microsoft": "Information"
}
}
},
"Logs": {
"SqlServer": {
"Enabled": false,
"Connection": "Server=#########;Database=#########;User Id=#########;Password=#########;MultipleActiveResultSets=true"
},
"Seq": {
"Enabled": false,
"Url": "",
"ApiKey": ""
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
@ -15,10 +27,38 @@
"Code": "NETWORK_RESURRECTOR_API" "Code": "NETWORK_RESURRECTOR_API"
}, },
"IdentityServer": { "IdentityServer": {
//"BaseAddress": "http://localhost:5063/" "BaseAddress": "http://<host>:<port>/api/"
"BaseAddress": "https://lab.code-rove.com/identity-server-api/"
}, },
"NetworkResurrectorServer": { "NetworkResurrectorServer": {
"BaseAddress": "http://localhost:5062" "BaseAddress": "http://<host>:<port>"
},
"Messaging": {
"Enabled": false,
"TopicPrefix": "HomeLab.",
"Source": "NetworkResurrector.Api",
"Nats": {
"NatsUrl": "nats://<host>:4222",
"Cluster": "<cluster>",
"ClientId": "NetworkResurrector_Api",
"QGroup": "NetworkResurrector.Api",
"DurableName": "durable"
}
},
"Notifications": {
"Wake": {
"To": [ "user@homelab.com" ],
"Subject": "Network resurrector: Machine {MACHINE_NAME} has been waked. Status: {ACTION_STATUS}",
"Body": "Hello,{NEWLINE:2}Network resurrector processed a command to start machine {MACHINE_FULLNAME} with IP {MACHINE_IP} at {SYSTEM_DATETIME}.{NEWLINE}The performer who was delegated for this action was {ACTION_PERFORMER}.{NEWLINE}Action status: {ACTION_STATUS}{NEWLINE:2}Have a nice day,{NEWLINE}Network resurrector notification system."
},
"Shutdown": {
"To": [ "user@homelab.com" ],
"Subject": "Network resurrector: Machine {MACHINE_NAME} has been shut down. Status: {ACTION_STATUS}",
"Body": "Hello,{NEWLINE:2}Network resurrector processed a command to shut down machine {MACHINE_FULLNAME} with IP {MACHINE_IP} at {SYSTEM_DATETIME}.{NEWLINE}The performer who was delegated for this action was {ACTION_PERFORMER}.{NEWLINE}Action status: {ACTION_STATUS}{NEWLINE:2}Have a nice day,{NEWLINE}Network resurrector notification system."
},
"Error": {
"To": [ "user@homelab.com" ],
"Subject": "Network resurrector: An unexpected error has occurred",
"Body": "Hello,{NEWLINE:2}Network resurrector encountered an unexpected error: {ERROR_MESSGE}.{NEWLINE}For more details about the error, check the logging system.{NEWLINE:2}Have a nice day,{NEWLINE}Network resurrector notification system."
}
} }
} }

View File

@ -31,9 +31,8 @@ namespace NetworkResurrector.Server.Application.CommandHandlers
} }
catch (Exception ex) catch (Exception ex)
{ {
var correlationIdMsg = $"CorrelationId: {command.Metadata.CorrelationId}"; _logger.LogError(ex, "An unexpected error has occurred.");
_logger.LogError(ex, $"An unexpected error has occurred. {correlationIdMsg}"); return new MachinePinged(false, ex.Message);
return new MachinePinged(false, $"{ex.Message} {correlationIdMsg}");
} }
} }
} }

View File

@ -36,9 +36,8 @@ namespace NetworkResurrector.Server.Application.CommandHandlers
} }
catch (Exception ex) catch (Exception ex)
{ {
var correlationIdMsg = $"CorrelationId: {command.Metadata.CorrelationId}"; _logger.LogError(ex, "An unexpected error has occurred.");
_logger.LogError(ex, $"An unexpected error has occurred. {correlationIdMsg}"); return new MachineShutdown(false, ex.Message);
return new MachineShutdown(false, $"{ex.Message} {correlationIdMsg}");
} }
} }
} }

View File

@ -29,9 +29,8 @@ namespace NetworkResurrector.Server.Application.CommandHandlers
} }
catch (Exception ex) catch (Exception ex)
{ {
var correlationIdMsg = $"CorrelationId: {command.Metadata.CorrelationId}"; _logger.LogError(ex, "An unexpected error has occurred.");
_logger.LogError(ex, $"An unexpected error has occurred. {correlationIdMsg}"); return new MachineWaked(false, ex.Message);
return new MachineWaked(false, $"{ex.Message} {correlationIdMsg}");
} }
} }
} }

View File

@ -1,7 +1,41 @@
namespace NetworkResurrector.Server.Application.Queries using MediatR;
using System;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkResurrector.Server.Application.Queries
{ {
class GetServiceVersion public class GetSystemVersion
{ {
//TO DO public class Query : IRequest<Model> { }
public record Model(string Version, DateTime LastUpdateDate);
public class QueryHandler : IRequestHandler<Query, Model>
{
public QueryHandler()
{
}
public async Task<Model> Handle(Query request, CancellationToken cancellationToken)
{
var version = Environment.GetEnvironmentVariable("APP_VERSION");
var appDate = Environment.GetEnvironmentVariable("APP_DATE");
if (string.IsNullOrEmpty(version))
version = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
if (!DateTime.TryParse(appDate, out var lastUpdateDate))
{
var location = Assembly.GetExecutingAssembly().Location;
lastUpdateDate = File.GetLastWriteTime(location);
}
var result = new Model(version, lastUpdateDate);
return await Task.FromResult(result);
}
}
} }
} }

View File

@ -1,16 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Server.PublishedLanguage.Events; using NetworkResurrector.Server.PublishedLanguage.Events;
using System;
namespace NetworkResurrector.Server.PublishedLanguage.Commands namespace NetworkResurrector.Server.PublishedLanguage.Commands
{ {
public class PingMachine : Command<MachinePinged> public class PingMachine : IRequest<MachinePinged>
{ {
public string IpAddressOrMachineName { get; set; } public string IpAddressOrMachineName { get; set; }
public PingMachine(string ipAddressOrMachineName) : base(new Metadata() { CorrelationId = Guid.NewGuid() })
{
IpAddressOrMachineName = ipAddressOrMachineName;
}
} }
} }

View File

@ -1,16 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Server.PublishedLanguage.Events; using NetworkResurrector.Server.PublishedLanguage.Events;
using System;
namespace NetworkResurrector.Server.PublishedLanguage.Commands namespace NetworkResurrector.Server.PublishedLanguage.Commands
{ {
public class ShutdownMachine : Command<MachineShutdown> public class ShutdownMachine : IRequest<MachineShutdown>
{ {
public string IPAddressOrMachineName { get; set; } public string IPAddressOrMachineName { get; set; }
public ShutdownMachine(string ipAddressOrMachineName) : base(new Metadata() { CorrelationId = Guid.NewGuid() })
{
IPAddressOrMachineName = ipAddressOrMachineName;
}
} }
} }

View File

@ -1,16 +1,10 @@
using Netmash.Application.DataContracts; using MediatR;
using NetworkResurrector.Server.PublishedLanguage.Events; using NetworkResurrector.Server.PublishedLanguage.Events;
using System;
namespace NetworkResurrector.Server.PublishedLanguage.Commands namespace NetworkResurrector.Server.PublishedLanguage.Commands
{ {
public class WakeMachine : Command<MachineWaked> public class WakeMachine : IRequest<MachineWaked>
{ {
public string MacAddress { get; set; } public string MacAddress { get; set; }
public WakeMachine(string macAddress) : base(new Metadata() { CorrelationId = Guid.NewGuid() })
{
MacAddress = macAddress;
}
} }
} }

View File

@ -9,7 +9,7 @@
<Version>1.0.3</Version> <Version>1.0.3</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Netmash.Application.DataContracts" Version="$(NetmashApplicationPackageVersion)" /> <PackageReference Include="MediatR" Version="$(MediatRPackageVersion)" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,36 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetworkResurrector.Server.Application.Queries;
using System;
using System.Threading.Tasks;
namespace NetworkResurrector.Server.Controllers
{
[Authorize]
[ApiController]
[Route("system")]
public class SystemController : ControllerBase
{
private readonly IMediator _mediator;
public SystemController(IMediator mediator)
{
_mediator = mediator;
}
[AllowAnonymous]
[HttpGet("ping")]
public IActionResult Ping()
{
return Ok($"Ping success. System datetime: {DateTime.Now}");
}
[HttpGet("version")]
public async Task<IActionResult> GetSystemVersion([FromRoute] GetSystemVersion.Query query)
{
var result = await _mediator.Send(query);
return Ok(result);
}
}
}

View File

@ -14,7 +14,6 @@
<PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" /> <PackageReference Include="Netmash.Security.Authentication.Identity" Version="$(NetmashSecurityAuthenticationPackageVersion)" />
<PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" /> <PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" />
<PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" /> <PackageReference Include="Serilog.Extensions.Logging" Version="$(SerilogExtensionsPackageVersion)" />
<PackageReference Include="Serilog.Sinks.Console" Version="$(SerilogSinksConsolePackageVersion)" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" /> <PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="$(AutoMapperExtensionsPackageVersion)" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="$(MediatRPackageVersion)" />