refactor: make exporting consistent

This commit is contained in:
Gary Sharp
2025-02-06 19:14:36 +11:00
parent f946f3250c
commit 67f1c2a5d1
69 changed files with 908 additions and 921 deletions
@@ -1,32 +1,89 @@
using Disco.Data.Repository;
using Disco.Models.Exporting;
using Disco.Models.Repository;
using Disco.Models.Services.Devices.Exporting;
using Disco.Models.Services.Devices;
using Disco.Models.Services.Exporting;
using Disco.Services.Exporting;
using Disco.Services.Plugins.Features.DetailsProvider;
using Disco.Services.Tasks;
using Disco.Services.Users;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Text.Json.Serialization;
namespace Disco.Services.Devices.Exporting
namespace Disco.Services.Devices
{
using Metadata = ExportFieldMetadata<DeviceExportRecord>;
public static class DeviceExport
public class DeviceExportContext : IExportContext<DeviceExportOptions, DeviceExportRecord>
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool TimestampSuffix { get; set; }
public DeviceExportOptions Options { get; set; }
public static ExportResult GenerateExport(DiscoDataContext Database, Func<IQueryable<Device>, IQueryable<Device>> Filter, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus)
public string SuggestedFilenamePrefix { get; } = "DeviceExport";
public string ExcelWorksheetName { get; } = "DeviceExport";
public string ExcelTableName { get; } = "Devices";
[JsonConstructor]
private DeviceExportContext()
{
var deviceQuery = Database.Devices
}
public DeviceExportContext(string name, string description, bool timestampSuffix, DeviceExportOptions options)
{
Id = Guid.NewGuid();
Name = name;
Description = description;
TimestampSuffix = timestampSuffix;
Options = options;
}
public DeviceExportContext(DeviceExportOptions options)
: this("Device Export", null, true, options)
{
}
public ExportResult Export(DiscoDataContext database, IScheduledTaskStatus taskStatus)
=> Exporter.Export(database, this, taskStatus);
private IQueryable<Device> BuildFilteredRecords(DiscoDataContext database)
{
var query = database.Devices
.Include(d => d.AssignedUser.UserDetails)
.Include(d => d.DeviceDetails);
if (Filter != null)
deviceQuery = Filter(deviceQuery);
switch (Options.ExportType)
{
case DeviceExportTypes.All:
break;
case DeviceExportTypes.Batch:
if (Options.ExportTypeTargetId.HasValue && Options.ExportTypeTargetId.Value > 0)
query = query.Where(d => d.DeviceBatchId != Options.ExportTypeTargetId);
else
query = query.Where(d => d.DeviceBatchId != null);
break;
case DeviceExportTypes.Model:
query = query.Where(d => d.DeviceModelId == Options.ExportTypeTargetId);
break;
case DeviceExportTypes.Profile:
query = query.Where(d => d.DeviceProfileId == Options.ExportTypeTargetId);
break;
default:
throw new ArgumentException($"Unknown Device Export Type '{Options.ExportType}'", nameof(Options.ExportType));
}
return query;
}
public List<DeviceExportRecord> BuildRecords(DiscoDataContext database, IScheduledTaskStatus taskStatus)
{
var query = BuildFilteredRecords(database);
// Update Users
if (Options.AssignedUserDisplayName ||
@@ -35,13 +92,13 @@ namespace Disco.Services.Devices.Exporting
Options.AssignedUserPhoneNumber ||
Options.AssignedUserEmailAddress)
{
TaskStatus.UpdateStatus(5, "Refreshing user details from Active Directory");
var userIds = deviceQuery.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList();
taskStatus.UpdateStatus(5, "Refreshing user details from Active Directory");
var userIds = query.Where(d => d.AssignedUserId != null).Select(d => d.AssignedUserId).Distinct().ToList();
foreach (var userId in userIds)
{
try
{
UserService.GetUser(userId, Database);
UserService.GetUser(userId, database);
}
catch (Exception) { } // Ignore Errors
}
@@ -50,91 +107,18 @@ namespace Disco.Services.Devices.Exporting
// Update Last Network Logon Date
if (Options.DeviceLastNetworkLogon)
{
TaskStatus.UpdateStatus(15, "Refreshing device last network logon dates from Active Directory");
taskStatus.UpdateStatus(15, "Refreshing device last network logon dates from Active Directory");
try
{
Interop.ActiveDirectory.ADNetworkLogonDatesUpdateTask.UpdateLastNetworkLogonDates(Database, ScheduledTaskMockStatus.Create("UpdateLastNetworkLogonDates"));
Database.SaveChanges();
Interop.ActiveDirectory.ADNetworkLogonDatesUpdateTask.UpdateLastNetworkLogonDates(database, ScheduledTaskMockStatus.Create("UpdateLastNetworkLogonDates"));
database.SaveChanges();
}
catch (Exception) { } // Ignore Errors
}
TaskStatus.UpdateStatus(25, "Extracting records from the database");
taskStatus.UpdateStatus(25, "Gathering database records");
var records = BuildRecords(deviceQuery).ToList();
// materialize device details
records.ForEach(r =>
{
if (Options.DetailBios)
r.DeviceDetailBios = r.DeviceDetails.Bios();
if (Options.DetailBaseBoard)
r.DeviceDetailBaseBoard = r.DeviceDetails.BaseBoard();
if (Options.DetailComputerSystem)
r.DeviceDetailComputerSystem = r.DeviceDetails.ComputerSystem();
if (Options.DetailProcessors)
r.DeviceDetailProcessors = r.DeviceDetails.Processors();
if (Options.DetailMemory)
r.DeviceDetailPhysicalMemory = r.DeviceDetails.PhysicalMemory();
if (Options.DetailDiskDrives)
r.DeviceDetailDiskDrives = r.DeviceDetails.DiskDrives();
if (Options.DetailLanAdapters || Options.DetailWLanAdapters)
{
r.DeviceDetailNetworkAdapters = r.DeviceDetails.NetworkAdapters();
if (r.DeviceDetailNetworkAdapters == null)
{
r.DeviceDetailLanMacAddresses = r.DeviceDetails.LanMacAddress()?.Split(';').Select(a => a.Trim()).ToList();
r.DeviceDetailWlanMacAddresses = r.DeviceDetails.WLanMacAddress()?.Split(';').Select(a => a.Trim()).ToList();
}
}
if (Options.DetailBatteries)
r.DeviceDetailBatteries = r.DeviceDetails.Batteries();
if (Options.AssignedUserDetailCustom && r.AssignedUser != null)
{
var detailsService = new DetailsProviderService(Database);
r.AssignedUserCustomDetails = detailsService.GetDetails(r.AssignedUser);
}
});
TaskStatus.UpdateStatus(70, "Building metadata");
var metadata = Options.BuildMetadata(records);
if (metadata.Count == 0)
throw new ArgumentException("At least one export field must be specified", "Options");
TaskStatus.UpdateStatus(80, $"Formatting {records.Count} records for export");
return ExportHelpers.WriteExport(Options, TaskStatus, metadata, records);
}
public static ExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options, IScheduledTaskStatus TaskStatus)
{
switch (Options.ExportType)
{
case DeviceExportTypes.All:
return GenerateExport(Database, null, Options, TaskStatus);
case DeviceExportTypes.Batch:
if (Options.ExportTypeTargetId.HasValue && Options.ExportTypeTargetId.Value > 0)
return GenerateExport(Database, devices => devices.Where(d => d.DeviceBatchId == Options.ExportTypeTargetId), Options, TaskStatus);
else
return GenerateExport(Database, devices => devices.Where(d => d.DeviceBatchId == null), Options, TaskStatus);
case DeviceExportTypes.Model:
return GenerateExport(Database, devices => devices.Where(d => d.DeviceModelId == Options.ExportTypeTargetId), Options, TaskStatus);
case DeviceExportTypes.Profile:
return GenerateExport(Database, devices => devices.Where(d => d.DeviceProfileId == Options.ExportTypeTargetId), Options, TaskStatus);
default:
throw new ArgumentException(string.Format("Unknown Device Export Type", Options.ExportType.ToString()), "Options");
}
}
public static ExportResult GenerateExport(DiscoDataContext Database, DeviceExportOptions Options)
{
return GenerateExport(Database, Options, ScheduledTaskMockStatus.Create("Device Export"));
}
private static IEnumerable<DeviceExportRecord> BuildRecords(IQueryable<Device> Devices)
{
return Devices.Select(d => new DeviceExportRecord()
var records = query.Select(d => new DeviceExportRecord()
{
Device = d,
@@ -170,10 +154,46 @@ namespace Disco.Services.Devices.Exporting
AttachmentsCount = d.DeviceAttachments.Count(),
DeviceCertificates = d.DeviceCertificates.Where(dc => dc.Enabled).OrderByDescending(dc => dc.AllocatedDate)
}).ToList();
// materialize device details
records.ForEach(r =>
{
if (Options.DetailBios)
r.DeviceDetailBios = r.DeviceDetails.Bios();
if (Options.DetailBaseBoard)
r.DeviceDetailBaseBoard = r.DeviceDetails.BaseBoard();
if (Options.DetailComputerSystem)
r.DeviceDetailComputerSystem = r.DeviceDetails.ComputerSystem();
if (Options.DetailProcessors)
r.DeviceDetailProcessors = r.DeviceDetails.Processors();
if (Options.DetailMemory)
r.DeviceDetailPhysicalMemory = r.DeviceDetails.PhysicalMemory();
if (Options.DetailDiskDrives)
r.DeviceDetailDiskDrives = r.DeviceDetails.DiskDrives();
if (Options.DetailLanAdapters || Options.DetailWLanAdapters)
{
r.DeviceDetailNetworkAdapters = r.DeviceDetails.NetworkAdapters();
if (r.DeviceDetailNetworkAdapters == null)
{
r.DeviceDetailLanMacAddresses = r.DeviceDetails.LanMacAddress()?.Split(';').Select(a => a.Trim()).ToList();
r.DeviceDetailWlanMacAddresses = r.DeviceDetails.WLanMacAddress()?.Split(';').Select(a => a.Trim()).ToList();
}
}
if (Options.DetailBatteries)
r.DeviceDetailBatteries = r.DeviceDetails.Batteries();
if (Options.AssignedUserDetailCustom && r.AssignedUser != null)
{
var detailsService = new DetailsProviderService(database);
r.AssignedUserCustomDetails = detailsService.GetDetails(r.AssignedUser);
}
});
return records;
}
private static List<Metadata> BuildMetadata(this DeviceExportOptions options, List<DeviceExportRecord> records)
public List<Metadata> BuildMetadata(DiscoDataContext database, List<DeviceExportRecord> records, IScheduledTaskStatus taskStatus)
{
var processorMaxCount = Math.Max(1, records.Max(r => r.DeviceDetailProcessors?.Count ?? 0));
var memoryMaxCount = Math.Max(1, records.Max(r => r.DeviceDetailPhysicalMemory?.Count ?? 0));
@@ -184,7 +204,7 @@ namespace Disco.Services.Devices.Exporting
var batteriesMaxCount = Math.Max(1, records.Max(r => r.DeviceDetailBatteries?.Count ?? 0));
IEnumerable<string> assignedUserDetailCustomKeys = null;
if (options.AssignedUserDetailCustom)
if (Options.AssignedUserDetailCustom)
assignedUserDetailCustomKeys = records.Where(r => r.AssignedUserCustomDetails != null).SelectMany(r => r.AssignedUserCustomDetails.Keys).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var allAccessors = BuildRecordAccessors(processorMaxCount, memoryMaxCount, diskDriveMaxCount, lanAdapterMaxCount, wlanAdapterMaxCount, certificateMaxCount, batteriesMaxCount, assignedUserDetailCustomKeys);
@@ -196,7 +216,7 @@ namespace Disco.Services.Devices.Exporting
property = p,
details = (DisplayAttribute)p.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault()
})
.Where(p => p.details != null && (bool)p.property.GetValue(options))
.Where(p => p.details != null && (bool)p.property.GetValue(Options))
.SelectMany(p =>
{
var fieldMetadata = allAccessors[p.property.Name];
@@ -460,6 +480,5 @@ namespace Disco.Services.Devices.Exporting
return metadata;
}
}
}
@@ -2,9 +2,11 @@
using Disco.Models.Exporting;
using Disco.Models.Services.Devices.DeviceFlag;
using Disco.Models.Services.Exporting;
using Disco.Services.Exporting;
using Disco.Services.Plugins.Features.DetailsProvider;
using Disco.Services.Tasks;
using Disco.Services.Users;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -15,57 +17,67 @@ namespace Disco.Services.Devices.DeviceFlags
{
using Metadata = ExportFieldMetadata<DeviceFlagExportRecord>;
public class DeviceFlagExport
public class DeviceFlagExportContext : IExportContext<DeviceFlagExportOptions, DeviceFlagExportRecord>
{
private readonly DiscoDataContext database;
private readonly DeviceFlagExportOptions options;
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool TimestampSuffix { get; set; }
public DeviceFlagExportOptions Options { get; set; }
public DeviceFlagExport(DiscoDataContext database, DeviceFlagExportOptions options)
public string SuggestedFilenamePrefix { get; } = "DeviceFlagExport";
public string ExcelWorksheetName { get; } = "DeviceFlagExport";
public string ExcelTableName { get; } = "DeviceFlags";
[JsonConstructor]
private DeviceFlagExportContext()
{
this.database = database;
this.options = options;
}
public ExportResult Generate(IScheduledTaskStatus status)
public DeviceFlagExportContext(string name, string description, bool timestampSuffix, DeviceFlagExportOptions options)
{
var records = BuildRecords(status);
var metadata = BuildMetadata(records, status);
if (metadata.Count == 0)
throw new ArgumentException("At least one export field must be specified", nameof(options));
status.UpdateStatus(90, $"Formatting {records.Count} records for export");
return ExportHelpers.WriteExport(options, status, metadata, records);
Id = Guid.NewGuid();
Name = name;
Description = description;
TimestampSuffix = timestampSuffix;
Options = options;
}
private List<DeviceFlagExportRecord> BuildRecords(IScheduledTaskStatus status)
public DeviceFlagExportContext(DeviceFlagExportOptions options)
: this("Device Flag Export", null, true, options)
{
}
public ExportResult Export(DiscoDataContext database, IScheduledTaskStatus status)
=> Exporter.Export(database, this, status);
public List<DeviceFlagExportRecord> BuildRecords(DiscoDataContext database, IScheduledTaskStatus status)
{
var query = database.DeviceFlagAssignments
.Include(a => a.DeviceFlag);
if (options.HasDeviceOptions())
if (Options.HasDeviceOptions())
query = query.Include(a => a.Device);
if (options.HasDeviceModelOptions())
if (Options.HasDeviceModelOptions())
query = query.Include(a => a.Device.DeviceModel);
if (options.HasDeviceBatchOptions())
if (Options.HasDeviceBatchOptions())
query = query.Include(a => a.Device.DeviceBatch);
if (options.HasDeviceProfileOptions())
if (Options.HasDeviceProfileOptions())
query = query.Include(a => a.Device.DeviceProfile);
if (options.HasAssignedUserOptions())
if (Options.HasAssignedUserOptions())
query = query.Include(a => a.Device.AssignedUser);
if (options.AssignedUserDetailCustom)
if (Options.AssignedUserDetailCustom)
query = query.Include(a => a.Device.AssignedUser.UserDetails);
query = query.Where(a => options.DeviceFlagIds.Contains(a.DeviceFlagId));
query = query.Where(a => Options.DeviceFlagIds.Contains(a.DeviceFlagId));
if (options.CurrentOnly)
if (Options.CurrentOnly)
{
query = query.Where(a => !a.RemovedDate.HasValue);
}
// Update Users
if (options.HasAssignedUserOptions())
if (Options.HasAssignedUserOptions())
{
status.UpdateStatus(5, "Refreshing user details from Active Directory");
var userIds = query.Where(d => d.Device.AssignedUserId != null).Select(d => d.Device.AssignedUserId).Distinct().ToList();
@@ -86,7 +98,7 @@ namespace Disco.Services.Devices.DeviceFlags
Assignment = a
}).ToList();
if (options.AssignedUserDetailCustom)
if (Options.AssignedUserDetailCustom)
{
status.UpdateStatus(50, "Extracting custom user detail records");
@@ -108,12 +120,10 @@ namespace Disco.Services.Devices.DeviceFlags
return records;
}
private List<Metadata> BuildMetadata(List<DeviceFlagExportRecord> records, IScheduledTaskStatus status)
public List<Metadata> BuildMetadata(DiscoDataContext database, List<DeviceFlagExportRecord> records, IScheduledTaskStatus status)
{
status.UpdateStatus(80, "Building metadata");
IEnumerable<string> userDetailCustomKeys = null;
if (options.AssignedUserDetailCustom)
if (Options.AssignedUserDetailCustom)
userDetailCustomKeys = records.Where(r => r.AssignedUserCustomDetails != null).SelectMany(r => r.AssignedUserCustomDetails.Keys).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var accessors = BuildAccessors(userDetailCustomKeys);
@@ -125,7 +135,7 @@ namespace Disco.Services.Devices.DeviceFlags
property = p,
details = (DisplayAttribute)p.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault()
})
.Where(p => p.details != null && p.property.Name != nameof(options.CurrentOnly) && (bool)p.property.GetValue(options))
.Where(p => p.details != null && p.property.Name != nameof(Options.CurrentOnly) && (bool)p.property.GetValue(Options))
.SelectMany(p =>
{
var fieldMetadata = accessors[p.property.Name];
@@ -224,6 +234,5 @@ namespace Disco.Services.Devices.DeviceFlags
return metadata;
}
}
}
@@ -1,46 +0,0 @@
using Disco.Data.Repository;
using Disco.Models.Services.Devices.DeviceFlag;
using Disco.Services.Exporting;
using Disco.Services.Tasks;
using Quartz;
namespace Disco.Services.Devices.DeviceFlags
{
public class DeviceFlagExportTask : ScheduledTask
{
private const string JobDataMapContext = "Context";
public override string TaskName { get; } = "Export Device Flags";
public override bool SingleInstanceTask { get { return false; } }
public override bool CancelInitiallySupported { get { return false; } }
public static ExportTaskContext<DeviceFlagExportOptions> ScheduleNow(DeviceFlagExportOptions options)
{
// Build Context
var context = new ExportTaskContext<DeviceFlagExportOptions>(options);
// Build Data Map
var task = new DeviceFlagExportTask();
JobDataMap taskData = new JobDataMap() { { JobDataMapContext, context } };
// Schedule Task
context.TaskStatus = task.ScheduleTask(taskData);
return context;
}
protected override void ExecuteTask()
{
var context = (ExportTaskContext<DeviceFlagExportOptions>)ExecutionContext.JobDetail.JobDataMap[JobDataMapContext];
Status.UpdateStatus(10, "Exporting Device Flag Records", "Starting...");
using (DiscoDataContext Database = new DiscoDataContext())
{
var export = new DeviceFlagExport(Database, context.Options);
context.Result = export.Generate(Status);
}
}
}
}
@@ -1,44 +0,0 @@
using Disco.Models.Services.Devices.Exporting;
using Disco.Services.Tasks;
using Quartz;
using Disco.Data.Repository;
using Disco.Services.Exporting;
namespace Disco.Services.Devices.Exporting
{
public class DeviceExportTask : ScheduledTask
{
private const string JobDataMapContext = "Context";
public override string TaskName { get { return "Export Devices"; } }
public override bool SingleInstanceTask { get { return false; } }
public override bool CancelInitiallySupported { get { return false; } }
public static ExportTaskContext<DeviceExportOptions> ScheduleNow(DeviceExportOptions Options)
{
// Build Context
var context = new ExportTaskContext<DeviceExportOptions>(Options);
// Build Data Map
var task = new DeviceExportTask();
JobDataMap taskData = new JobDataMap() { { JobDataMapContext, context} };
// Schedule Task
context.TaskStatus = task.ScheduleTask(taskData);
return context;
}
protected override void ExecuteTask()
{
var context = (ExportTaskContext<DeviceExportOptions>)ExecutionContext.JobDetail.JobDataMap[JobDataMapContext];
Status.UpdateStatus(10, "Exporting Device Records", "Starting...");
using (DiscoDataContext Database = new DiscoDataContext())
{
context.Result = DeviceExport.GenerateExport(Database, context.Options, Status);
}
}
}
}