Newer
Older
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Xml.Linq;
using System.Xml.Serialization;
namespace ATxCommon.Serializables
/// AutoTx service configuration class.
/// </summary>
[Serializable]
public class ServiceConfig
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
#region required configuration parameters
/// <summary>
/// A human friendly name for the host, to be used in emails etc.
/// </summary>
public string HostAlias { get; set; }
/// <summary>
/// The base drive for the spooling directories (incoming and managed).
/// </summary>
public string SourceDrive { get; set; }
/// <summary>
/// The name of a directory on SourceDrive that is monitored for new files.
/// </summary>
public string IncomingDirectory { get; set; }
/// <summary>
/// A directory on SourceDrive to hold the three subdirectories "DONE",
/// "PROCESSING" and "UNMATCHED" used during and after transfers.
/// </summary>
public string ManagedDirectory { get; set; }
/// <summary>
/// A human friendly name for the target, to be used in emails etc.
/// </summary>
public string DestinationAlias { get; set; }
/// <summary>
/// Target path to transfer files to. Usually a UNC location.
/// </summary>
public string DestinationDirectory { get; set; }
/// <summary>
/// The name of a subdirectory in the DestinationDirectory to be used
/// to keep the temporary data of running transfers.
/// </summary>
public string TmpTransferDir { get; set; }
/// <summary>
/// Maximum allowed CPU usage across all cores in percent. Running transfers will be paused
/// if this limit is exceeded.
/// </summary>
public int MaxCpuUsage { get; set; }
/// <summary>
/// Maximum length of the disk queue multiplied by 1000 (so a value of "25" here means the
/// queue length is required to be "0.025" or less). Running transfers will be paused if
/// this limit is exceeded.
/// </summary>
public int MaxDiskQueue { get; set; }
/// <summary>
/// Minimum amount of free RAM (in MB) required for the service to operate.
/// </summary>
public int MinAvailableMemory { get; set; }
#endregion
#region optional configuration parameters
/// NLog log level, one of "Warn", "Info", "Debug", "Trace". Default: "Info"
public string LogLevel { get; set; } = "Info";
/// <summary>
/// Log level to use for performance monitoring messages. Default: "Trace"
/// </summary>
public string LogLevelMonitoring { get; set; } = "Trace";
/// <summary>
/// Enable debug messages from the RoboSharp library. Default: false.
/// </summary>
public bool DebugRoboSharp { get; set; } = false;
/// <summary>
/// The full path of a file to be used for RoboCopy log messages. Default: "" (off).
/// </summary>
public string RoboCopyLog { get; set; } = "";
/// <summary>
/// The interval (in ms) for checking for new files and system parameters. Default: 1000.
/// </summary>
public int ServiceTimer { get; set; } = 1000;
/// The name of a marker file to be placed in all **sub**directories
/// inside the IncomingDirectory.
public string MarkerFile { get; set; }
/// <summary>
/// Number of days after data in the "DONE" location expires. Default: 30.
/// </summary>
public int GracePeriod { get; set; } = 30;
/// <summary>
/// Whether to enforce ACL inheritance when moving files and directories, see
/// https://support.microsoft.com/en-us/help/320246 for more details. Default: false.
/// </summary>
// ReSharper disable once InconsistentNaming
public bool EnforceInheritedACLs { get; set; } = false;
/// <summary>
/// Limit RoboCopy transfer bandwidth (mostly for testing purposes). Default: 0.
/// </summary>
/// See the RoboCopy documentation for more details.
public int InterPacketGap { get; set; } = 0;
/// <summary>
/// Setting for the RoboCopy /COPY parameter, valid flags are D=Data, A=Attributes,
/// T=Timestamps, S=Security(ACLs), O=Owner info, U=aUditing info. Default: "DT".
/// </summary>
public string CopyFlags { get; set; } = "DT";
/// <summary>
/// Setting for the RoboCopy /DCOPY parameter, valid flags depend on the version of
/// RoboCopy used and should only be changed with greatest care! Default: "T".
/// </summary>
public string DirectoryCopyFlags { get; set; } = "T";
/// <summary>
/// A list of process names causing transfers to be suspended if running.
/// </summary>
[XmlArray]
[XmlArrayItem(ElementName = "ProcessName")]
public List<string> BlacklistedProcesses { get; set; }
/// <summary>
/// A list of drives and thresholds to monitor free space.
/// </summary>
[XmlArray]
[XmlArrayItem(ElementName = "DriveToCheck")]
public List<DriveToCheck> SpaceMonitoring { get; set; }
#endregion
#region optional configuration parameters - notification settings
/// SMTP server to send mails and Fatal/Error log messages. No mails if omitted.
public string SmtpHost { get; set; }
/// SMTP port for sending emails. Default: 25.
public int SmtpPort { get; set; } = 25;
/// <summary>
/// SMTP username to authenticate when sending emails (if required).
/// </summary>
public string SmtpUserCredential { get; set; }
/// <summary>
/// SMTP password to authenticate when sending emails (if required).
/// </summary>
public string SmtpPasswortCredential { get; set; }
/// <summary>
/// The email address to be used as "From:" when sending mail notifications.
public string EmailFrom { get; set; }
/// A prefix to be added to any email subject. Default: "[AutoTx Service] ".
public string EmailPrefix { get; set; } = "[AutoTx Service] ";
/// <summary>
/// The mail recipient address for admin notifications (including "Fatal" log messages).
/// </summary>
public string AdminEmailAddress { get; set; }
/// <summary>
/// The mail recipient address for debug notifications (including "Error" log messages).
/// </summary>
public string AdminDebugEmailAddress { get; set; }
/// <summary>
/// Send an email to the user upon completed transfers. Default: true.
/// </summary>
public bool SendTransferNotification { get; set; } = true;
/// <summary>
/// Send email notifications to the admin on selected events. Default: true.
/// </summary>
public bool SendAdminNotification { get; set; } = true;
/// <summary>
/// Minimum time in minutes between two notifications to the admin. Default: 60.
/// </summary>
public int AdminNotificationDelta { get; set; } = 60;
/// Minimum time in minutes between two mails about expired folders. Default: 720 (12h).
public int GraceNotificationDelta { get; set; } = 720;
/// Minimum time in minutes between two low-space notifications. Default: 720 (12h).
public int StorageNotificationDelta { get; set; } = 720;
/// <summary>
/// Minimum time in minutes between two startup system health notifications.
/// Default: 2880 (2d).
/// </summary>
public int StartupNotificationDelta { get; set; } = 2880;
Niko Ehrenfeuchter
committed
#region wrappers for derived parameters
/// <summary>
/// The full path to the incoming directory.
/// </summary>
[XmlIgnore]
public string IncomingPath => Path.Combine(SourceDrive, IncomingDirectory);
Niko Ehrenfeuchter
committed
/// <summary>
/// The full path to the managed directory.
/// </summary>
[XmlIgnore]
public string ManagedPath => Path.Combine(SourceDrive, ManagedDirectory);
Niko Ehrenfeuchter
committed
/// <summary>
/// The full path to the processing directory.
/// </summary>
[XmlIgnore]
public string ProcessingPath => Path.Combine(ManagedPath, "PROCESSING");
Niko Ehrenfeuchter
committed
/// <summary>
/// The full path to the done directory / grace location.
/// </summary>
[XmlIgnore]
public string DonePath => Path.Combine(ManagedPath, "DONE");
Niko Ehrenfeuchter
committed
/// <summary>
/// The full path to the directory for unmatched user directories.
/// </summary>
[XmlIgnore]
public string UnmatchedPath => Path.Combine(ManagedPath, "UNMATCHED");
Niko Ehrenfeuchter
committed
/// <summary>
/// The full path to the directory for directories that were moved out of the way due to
/// any kind of error in processing them.
/// </summary>
[XmlIgnore]
public string ErrorPath => Path.Combine(ManagedPath, "ERROR");
/// <summary>
/// The LogLevel to be used for performance monitoring messages.
/// </summary>
[XmlIgnore]
public LogLevel MonitoringLogLevel {
get {
switch (LogLevelMonitoring) {
case "Warn":
return global::NLog.LogLevel.Warn;
case "Info":
return global::NLog.LogLevel.Info;
case "Debug":
return global::NLog.LogLevel.Debug;
case "Trace":
return global::NLog.LogLevel.Trace;
default:
return global::NLog.LogLevel.Info;
}
}
}
[XmlIgnore]
public static string ValidatorWarnings { get; set; }
Niko Ehrenfeuchter
committed
/// <summary>
/// Convenience property converting the grace period to a human-friendly format.
/// </summary>
[XmlIgnore]
public string HumanGracePeriod => TimeUtils.DaysToHuman(GracePeriod, false);
Niko Ehrenfeuchter
committed
#endregion
/// <summary>
/// ServiceConfig constructor, currently empty.
/// </summary>
private ServiceConfig() {
Log.Trace("ServiceConfig() constructor.");
}
/// <summary>
/// Dummy method raising an exception (this class must not be serialized).
/// </summary>
public static void Serialize(string file, ServiceConfig c) {
// the config is never meant to be written by us, therefore:
throw new SettingsPropertyIsReadOnlyException("The config file must not be written by the service!");
}
/// <summary>
/// Load the host specific and the common XML configuration files, combine them and
/// deserialize them into a ServiceConfig object. The host specific configuration file's
/// name is defined as the hostname with an ".xml" suffix.
/// </summary>
/// <param name="path">The path to the configuration files.</param>
/// <returns>A ServiceConfig object with validated settings.</returns>
public static ServiceConfig Deserialize(string path) {
Log.Trace("ServiceConfig.Deserialize({0})", path);
ServiceConfig config;
var commonFile = Path.Combine(path, "config.common.xml");
var specificFile = Path.Combine(path, Environment.MachineName + ".xml");
// for parsing the configuration from two separate files we are using the default
// behaviour of the .NET XmlSerializer on duplicates: only the first occurrence is
// used, all other ones are silentley being discarded - this way we simply append the
// contents of the common config file to the host-specific and deserialize then:
Log.Debug("Loading host specific configuration XML file: [{0}]", specificFile);
var combined = XElement.Load(specificFile);
// the common configuration file is optional, so check if it exists at all:
if (File.Exists(commonFile)) {
Log.Debug("Loading common configuration XML file: [{0}]", commonFile);
var common = XElement.Load(commonFile);
combined.Add(common.Nodes());
Log.Trace("Combined XML structure:\n\n{0}\n\n", combined);
}
using (var reader = XmlReader.Create(new StringReader(combined.ToString()))) {
Log.Debug("Trying to parse combined XML...");
var serializer = new XmlSerializer(typeof(ServiceConfig));
config = (ServiceConfig) serializer.Deserialize(reader);
}
ValidateConfiguration(config);
Log.Debug("Successfully parsed a valid configuration from [{0}].", path);
return config;
}
/// <summary>
/// Validate the configuration, throwing exceptions on invalid parameters.
/// </summary>
private static void ValidateConfiguration(ServiceConfig c) {
Log.Debug("Validating configuration...");
var errmsg = "";
string CheckEmpty(string value, string name) {
// if the string is null terminate the validation immediately since this means the
// file doesn't contain a required parameter at all:
if (value == null) {
var msg = $"mandatory parameter missing: <{name}>";
Log.Error(msg);
throw new ConfigurationErrorsException(msg);
}
if (string.IsNullOrWhiteSpace(value))
return $"mandatory parameter unset: <{name}>\n";
return string.Empty;
}
string CheckMinValue(int value, string name, int min) {
if (value == 0)
return $"<{name}> is unset (or set to 0), minimal accepted value is {min}\n";
if (value < min)
return $"<{name}> must not be smaller than {min}\n";
return string.Empty;
}
string CheckLocalDrive(string value, string name) {
var driveType = new DriveInfo(value).DriveType;
if (driveType != DriveType.Fixed)
return $"<{name}> ({value}) must be a local fixed drive, not '{driveType}'!\n";
return string.Empty;
}
void WarnOnHighValue(int value, string name, int thresh) {
if (value > thresh)
SubOptimal(value.ToString(), name, "value is set very high, please check!");
}
void SubOptimal(string value, string name, string message) {
var msg = $">>> Sub-optimal setting detected: <{name}> [{value}] {message}";
ValidatorWarnings += msg + "\n";
Log.Warn(msg);
}
void LogAndThrow(string msg) {
msg = $"Configuration issues detected:\n{msg}";
Log.Error(msg);
throw new ConfigurationErrorsException(msg);
}
// check if all required parameters are there and non-empty / non-zero:
errmsg += CheckEmpty(c.HostAlias, nameof(c.HostAlias));
errmsg += CheckEmpty(c.SourceDrive, nameof(c.SourceDrive));
errmsg += CheckEmpty(c.IncomingDirectory, nameof(c.IncomingDirectory));
errmsg += CheckEmpty(c.ManagedDirectory, nameof(c.ManagedDirectory));
errmsg += CheckEmpty(c.DestinationAlias, nameof(c.DestinationAlias));
errmsg += CheckEmpty(c.DestinationDirectory, nameof(c.DestinationDirectory));
errmsg += CheckEmpty(c.TmpTransferDir, nameof(c.TmpTransferDir));
errmsg += CheckMinValue(c.ServiceTimer, nameof(c.ServiceTimer), 1000);
errmsg += CheckMinValue(c.MaxCpuUsage, nameof(c.MaxCpuUsage), 5);
errmsg += CheckMinValue(c.MaxDiskQueue, nameof(c.MaxDiskQueue), 1);
errmsg += CheckMinValue(c.MinAvailableMemory, nameof(c.MinAvailableMemory), 256);
// if any of the required parameter checks failed we terminate now as many of the
// string checks below would fail on empty strings:
if (!string.IsNullOrWhiteSpace(errmsg))
LogAndThrow(errmsg);
////////// REQUIRED PARAMETERS SETTINGS VALIDATION //////////
// SourceDrive
if (c.SourceDrive.Substring(1) != @":\")
errmsg += "<SourceDrive> must be of form [X:\\]\n!";
errmsg += CheckLocalDrive(c.SourceDrive, nameof(c.SourceDrive));
// spooling directories: IncomingDirectory + ManagedDirectory
if (c.IncomingDirectory.StartsWith(@"\"))
errmsg += "<IncomingDirectory> must not start with a backslash!\n";
if (c.ManagedDirectory.StartsWith(@"\"))
errmsg += "<ManagedDirectory> must not start with a backslash!\n";
// DestinationDirectory
if (!Directory.Exists(c.DestinationDirectory))
errmsg += $"can't find (or reach) destination: {c.DestinationDirectory}\n";
// TmpTransferDir
var tmpTransferPath = Path.Combine(c.DestinationDirectory, c.TmpTransferDir);
if (!Directory.Exists(tmpTransferPath))
errmsg += $"can't find (or reach) temporary transfer dir: {tmpTransferPath}\n";
////////// OPTIONAL PARAMETERS SETTINGS VALIDATION //////////
// EmailFrom
if (!string.IsNullOrWhiteSpace(c.SmtpHost) &&
string.IsNullOrWhiteSpace(c.EmailFrom))
errmsg += "<EmailFrom> must not be empty if <SmtpHost> is configured!\n";
// DriveName
foreach (var driveToCheck in c.SpaceMonitoring) {
errmsg += CheckLocalDrive(driveToCheck.DriveName, nameof(driveToCheck.DriveName));
}
////////// WEAK CHECKS ON PARAMETERS SETTINGS //////////
// those checks are non-critical and are simply reported to the logs
WarnOnHighValue(c.ServiceTimer, nameof(c.ServiceTimer), 10000);
WarnOnHighValue(c.MaxCpuUsage, nameof(c.MaxCpuUsage), 75);
WarnOnHighValue(c.MaxDiskQueue, nameof(c.MaxDiskQueue), 2000);
WarnOnHighValue(c.MinAvailableMemory, nameof(c.MinAvailableMemory), 8192);
WarnOnHighValue(c.AdminNotificationDelta, nameof(c.AdminNotificationDelta), 1440);
WarnOnHighValue(c.GraceNotificationDelta, nameof(c.GraceNotificationDelta), 10080);
WarnOnHighValue(c.StorageNotificationDelta, nameof(c.StorageNotificationDelta), 10080);
WarnOnHighValue(c.StartupNotificationDelta, nameof(c.StartupNotificationDelta), 40320);
WarnOnHighValue(c.GracePeriod, nameof(c.GracePeriod), 100);
if (!c.DestinationDirectory.StartsWith(@"\\"))
SubOptimal(c.DestinationDirectory, "DestinationDirectory", "is not a UNC path!");
// LogLevel
var validLogLevels = new List<string> {"Warn", "Info", "Debug", "Trace"};
if (!validLogLevels.Contains(c.LogLevel)) {
SubOptimal(c.LogLevel, "LogLevel", "is invalid, using 'Debug'. Valid options: " +
string.Join(", ", validLogLevels));
c.LogLevel = "Debug";
}
if (string.IsNullOrWhiteSpace(errmsg))
return;
LogAndThrow(errmsg);
/// <summary>
/// Generate a human-readable sumary of the current configuration.
/// </summary>
/// <returns>A string with details on the configuration.</returns>
public string Summary() {
var msg =
"############### REQUIRED PARAMETERS ###############\n" +
$"HostAlias: {HostAlias}\n" +
$"SourceDrive: {SourceDrive}\n" +
$"IncomingDirectory: {IncomingDirectory}\n" +
$"ManagedDirectory: {ManagedDirectory}\n" +
$"DestinationAlias: {DestinationAlias}\n" +
$"DestinationDirectory: {DestinationDirectory}\n" +
$"TmpTransferDir: {TmpTransferDir}\n" +
$"MaxCpuUsage: {MaxCpuUsage}%\n" +
$"MaxDiskQueue: {MaxDiskQueue} / 1000 (effectively {(float)MaxDiskQueue/1000:0.000})\n" +
$"MinAvailableMemory: {MinAvailableMemory} MB\n" +
"\n" +
"############### OPTIONAL PARAMETERS ###############\n" +
$"LogLevel: {LogLevel}\n" +
$"LogLevelMonitoring: {LogLevelMonitoring}\n" +
$"ServiceTimer: {ServiceTimer} ms\n" +
$"MarkerFile: {MarkerFile}\n" +
$"GracePeriod: {GracePeriod} days (" +
TimeUtils.DaysToHuman(GracePeriod, false) + ")\n" +
$"EnforceInheritedACLs: {EnforceInheritedACLs}\n" +
$"InterPacketGap: {InterPacketGap}\n" +
"";
var blacklist = "";
foreach (var processName in BlacklistedProcesses) {
blacklist += $" ProcessName: {processName}\n";
if (!string.IsNullOrWhiteSpace(blacklist))
msg += $"BlacklistedProcesses:\n{blacklist}";
var space = "";
foreach (var drive in SpaceMonitoring) {
space += $" DriveName: {drive.DriveName} " +
$"(threshold: {Conv.GigabytesToString(drive.SpaceThreshold)})\n";
if (!string.IsNullOrWhiteSpace(space))
msg += $"SpaceMonitoring:\n{space}";
if (string.IsNullOrWhiteSpace(SmtpHost)) {
msg += "SmtpHost: ====== Not configured, disabling email! ======" + "\n";
} else {
msg +=
$"SmtpHost: {SmtpHost}\n" +
$"SmtpPort: {SmtpPort}\n" +
$"SmtpUserCredential: {SmtpUserCredential}\n" +
$"SmtpPasswortCredential: --- not showing ---\n" +
$"EmailFrom: {EmailFrom}\n" +
$"EmailPrefix: {EmailPrefix}\n" +
$"AdminEmailAddress: {AdminEmailAddress}\n" +
$"AdminDebugEmailAddress: {AdminDebugEmailAddress}\n" +
$"SendTransferNotification: {SendTransferNotification}\n" +
$"SendAdminNotification: {SendAdminNotification}\n" +
$"AdminNotificationDelta: {AdminNotificationDelta} min (" +
TimeUtils.MinutesToHuman(AdminNotificationDelta, false) + ")\n" +
$"GraceNotificationDelta: {GraceNotificationDelta} min (" +
TimeUtils.MinutesToHuman(GraceNotificationDelta, false) + ")\n" +
$"StorageNotificationDelta: {StorageNotificationDelta} min (" +
TimeUtils.MinutesToHuman(StorageNotificationDelta, false) + ")\n" +
$"StartupNotificationDelta: {StartupNotificationDelta} min (" +
TimeUtils.MinutesToHuman(StartupNotificationDelta, false) + ")\n" +
"";
}
return msg;
}
}
}