diff --git a/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj b/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj index bb7a3b9e..1ec5510c 100644 --- a/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj +++ b/DevPack.Tests/Skyline.DataMiner.Dev.Utils.Solutions.MediaOps.Plan.Tests.csproj @@ -15,7 +15,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs b/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs new file mode 100644 index 00000000..de62bddb --- /dev/null +++ b/DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs @@ -0,0 +1,215 @@ +namespace RT_MediaOps.Plan.Workflow.Jobs +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Skyline.DataMiner.Net.Messages; + using Skyline.DataMiner.Net.ResourceManager.Helpers; + using Skyline.DataMiner.Net.ResponseErrorData; + using Skyline.DataMiner.Net.SRM.Capabilities; + using Skyline.DataMiner.Net.SRM.Capacities; + using Skyline.DataMiner.Net.SRM.Quarantine; + using Skyline.DataMiner.Solutions.MediaOps.Plan.API; + using Skyline.DataMiner.Solutions.MediaOps.Plan.Exceptions; + using Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Simulation; + + using ResourcePool = Skyline.DataMiner.Solutions.MediaOps.Plan.API.ResourcePool; + using PlanResource = Skyline.DataMiner.Solutions.MediaOps.Plan.API.Resource; + + /// + /// Deterministic, simulation-backed tests for translating core ResourceManager errors into DevPack job resource errors. + /// A real Resource Studio resource is created so the handler can resolve the core resource id to its DOM counterpart. + /// + [TestClass] + public sealed class ResourceManagerTraceDataHandlerTests + { + private static (IMediaOpsPlanApi Api, PlanResource Resource) CreateContextWithResource() + { + var dms = MediaOpsPlanSimulation.Create(); + var connection = dms.CreateConnection(); + var api = connection.GetMediaOpsPlanApi(); + + var prefix = Guid.NewGuid(); + + var pool = api.ResourcePools.Create(new ResourcePool { Name = $"{prefix}_Pool" }); + pool = api.ResourcePools.Complete(pool); + + var resource = new UnmanagedResource { Name = $"{prefix}_Resource" }.AssignToPool(pool); + resource = api.Resources.Create(resource); + resource = api.Resources.Complete(resource); + + return (api, resource); + } + + [TestMethod] + public void Translate_QuarantineError_EmitsResourceNotAvailableWithDomResourceId() + { + var (api, resource) = CreateContextWithResource(); + var reservationId = Guid.NewGuid(); + + var error = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine, + reservationId, + (Guid?)null, + new List()) + { + MustBeMovedToQuarantine = new List + { + new QuarantinedUsagesOnSingleReservation + { + QuarantinedUsages = new List + { + new QuarantinedResourceUsageDefinition + { + QuarantinedResourceUsage = new ResourceUsageDefinition(resource.CoreResourceId), + }, + }, + }, + }, + }; + + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var result = handler.Translate(new[] { error }); + + Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id."); + var notAvailable = result[reservationId].ErrorData.OfType().Single(); + Assert.AreEqual(resource.Id, notAvailable.ResourceId, "Expected the DOM resource id to be reported."); + } + + [TestMethod] + public void Translate_ResourceCapacityInvalid_EmitsInvalidCapacityWithIds() + { + var (api, resource) = CreateContextWithResource(); + var reservationId = Guid.NewGuid(); + var capacityId = Guid.NewGuid(); + + var error = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.ResourceCapacityInvalid, + reservationId, + resource.CoreResourceId, + new MultiResourceCapacityUsage { CapacityProfileID = capacityId }); + + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var result = handler.Translate(new[] { error }); + + Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id."); + var capacityError = result[reservationId].ErrorData.OfType().Single(); + Assert.AreEqual(resource.Id, capacityError.ResourceId, "Expected the DOM resource id to be reported."); + Assert.AreEqual(capacityId, capacityError.CapacityId, "Expected the capacity profile id to be reported."); + } + + [TestMethod] + public void Translate_ResourceCapabilityInvalid_EmitsInvalidCapabilityWithIds() + { + var (api, resource) = CreateContextWithResource(); + var reservationId = Guid.NewGuid(); + var capabilityId = Guid.NewGuid(); + + var error = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.ResourceCapabilityInvalid, + reservationId, + resource.CoreResourceId, + new ResourceCapabilityUsage { CapabilityProfileID = capabilityId }, + "Capability not available."); + + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var result = handler.Translate(new[] { error }); + + Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id."); + var capabilityError = result[reservationId].ErrorData.OfType().Single(); + Assert.AreEqual(resource.Id, capabilityError.ResourceId, "Expected the DOM resource id to be reported."); + Assert.AreEqual(capabilityId, capabilityError.CapabilityId, "Expected the capability profile id to be reported."); + } + + [TestMethod] + public void Translate_UncategorizedError_FallsBackToRawMessage() + { + var (api, _) = CreateContextWithResource(); + var reservationId = Guid.NewGuid(); + + var error = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.UnknownError, + reservationId, + (Guid?)null, + new List()); + + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var result = handler.Translate(new[] { error }); + + Assert.IsTrue(result.ContainsKey(reservationId), "Expected the raw fallback to be keyed by the reservation id."); + var errorData = result[reservationId].ErrorData.ToList(); + Assert.IsFalse( + errorData.OfType().Any(), + "Expected no typed job resource error for an uncategorized reason."); + Assert.IsTrue( + errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)), + "Expected a raw MediaOpsErrorData fallback for an uncategorized reason."); + } + + [TestMethod] + public void Translate_KnownAndUnknownErrors_MapsKnownAndAddsRawForUnknown() + { + var (api, resource) = CreateContextWithResource(); + var reservationId = Guid.NewGuid(); + var capacityId = Guid.NewGuid(); + + var capacityError = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.ResourceCapacityInvalid, + reservationId, + resource.CoreResourceId, + new MultiResourceCapacityUsage { CapacityProfileID = capacityId }); + + var unknownError = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.UnknownError, + reservationId, + (Guid?)null, + new List()); + + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var result = handler.Translate(new[] { capacityError, unknownError }); + + Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id."); + var errorData = result[reservationId].ErrorData.ToList(); + Assert.AreEqual( + capacityId, + errorData.OfType().Single().CapacityId, + "Expected the known capacity error to be translated."); + Assert.IsTrue( + errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)), + "Expected the unknown error to be added as a raw default alongside the translated one."); + } + [TestMethod] + public void Translate_WhenCalledMultipleTimes_DoesNotReturnPreviousResults() + { + var (api, resource) = CreateContextWithResource(); + var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api); + + var reservationId1 = Guid.NewGuid(); + var reservationId2 = Guid.NewGuid(); + + var first = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.ResourceCapacityInvalid, + reservationId1, + resource.CoreResourceId, + new MultiResourceCapacityUsage { CapacityProfileID = Guid.NewGuid() }); + + var second = new ResourceManagerErrorData( + ResourceManagerErrorData.Reason.UnknownError, + reservationId2, + (Guid?)null, + new List()); + + Assert.IsTrue(handler.Translate(new[] { first }).ContainsKey(reservationId1)); + + var secondResult = handler.Translate(new[] { second }); + Assert.IsFalse(secondResult.ContainsKey(reservationId1), "Expected previous translation output not to leak into subsequent calls."); + Assert.IsTrue(secondResult.ContainsKey(reservationId2)); + } + } +} diff --git a/DevPack.UnitTesting/Simulation/SimulatedDms.cs b/DevPack.UnitTesting/Simulation/SimulatedDms.cs index 9c6f3486..34206964 100644 --- a/DevPack.UnitTesting/Simulation/SimulatedDms.cs +++ b/DevPack.UnitTesting/Simulation/SimulatedDms.cs @@ -264,6 +264,14 @@ internal bool TryHandleMessage(DMSMessage message, out IEnumerable r throw new ArgumentNullException(nameof(message)); } + // A DOM select read only asks for specific fields. The DOM message handler does not support it, + // so it is translated into a regular read and then reduced to the requested fields. + if (DomInstanceSelectStore.TryHandleMessage(message, _domSlNetMessageHandler, out var selectResponse)) + { + responses = new[] { selectResponse }; + return true; + } + if (_domSlNetMessageHandler.TryHandleMessage(message, out var domResponse)) { responses = new[] { domResponse }; diff --git a/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs b/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs new file mode 100644 index 00000000..8b79a4c1 --- /dev/null +++ b/DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs @@ -0,0 +1,101 @@ +namespace Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Stores +{ + using System; + using System.Collections.Generic; + + using Skyline.DataMiner.Net.Apps.DataMinerObjectModel; + using Skyline.DataMiner.Net.Apps.DataMinerObjectModel.Select; + using Skyline.DataMiner.Net.Apps.ManagerStore.Select; + using Skyline.DataMiner.Net.Messages; + using Skyline.DataMiner.Utils.DOM.UnitTesting; + + /// + /// Handles DOM instance select (partial object) read requests, mirroring how a real DataMiner Agent + /// answers a . + /// + /// + /// A select read returns only the requested fields instead of full objects. The agent replies with a + /// whose custom response data holds a . + /// does not support these requests and returns , + /// so the request is translated into a regular read whose results are reduced to the requested fields. + /// + internal static class DomInstanceSelectStore + { + public static bool TryHandleMessage(DMSMessage message, DomSLNetMessageHandler domHandler, out DMSMessage response) + { + if (domHandler is null) + { + throw new ArgumentNullException(nameof(domHandler)); + } + + response = null; + + if (!(message is ManagerStoreSelectReadRequest request)) + { + return false; + } + + var readRequest = new ManagerStoreReadRequest(request.Query) + { + ModuleId = request.ModuleId, + }; + + if (!domHandler.TryHandleMessage(readRequest, out var readResponse) + || !(readResponse is ManagerStoreCrudResponse crudResponse)) + { + return false; + } + + response = CreateResponse(request, crudResponse.Objects ?? new List()); + return true; + } + + private static DMSMessage CreateResponse(ManagerStoreSelectReadRequest request, IEnumerable instances) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + var selectedFields = request.SelectedFields ?? new List(); + var objects = new List(); + + foreach (var instance in instances) + { + if (instance?.ID is null) + { + continue; + } + + var factory = new PartialDomInstanceFactory(instance.ID.ModuleId); + var values = new List(selectedFields.Count); + + foreach (var selectedField in selectedFields) + { + values.Add(new PartialObjectValue + { + FieldReferenceId = selectedField.Id, + Value = Execute(selectedField, instance), + }); + } + + objects.Add(factory.GetPartialObjectData(instance.ID, values)); + } + + return new ManagerStoreCrudResponse((object)new SelectResult { Objects = objects }); + } + + private static object Execute(SelectedFieldReference selectedField, DomInstance instance) + { + try + { + return selectedField.SerializableExposer?.Exposer?.execute(instance); + } + catch + { + // Mirror the agent's fail-safe behavior: a field that cannot be read is returned as no value. + return null; + } + } + } +} diff --git a/DevPack/API/Handlers/Workflow/Jobs/CoreJobHandler.cs b/DevPack/API/Handlers/Workflow/Jobs/CoreJobHandler.cs index 5f5370dd..684f8494 100644 --- a/DevPack/API/Handlers/Workflow/Jobs/CoreJobHandler.cs +++ b/DevPack/API/Handlers/Workflow/Jobs/CoreJobHandler.cs @@ -10,7 +10,6 @@ using Skyline.DataMiner.Net.Jobs; using Skyline.DataMiner.Net.Messages.SLDataGateway; using Skyline.DataMiner.Net.ResourceManager.Objects; - using Skyline.DataMiner.Net.ResponseErrorData; using Skyline.DataMiner.Net.SRM.Capabilities; using Skyline.DataMiner.Net.SRM.Capacities; using Skyline.DataMiner.Solutions.MediaOps.Plan.Exceptions; @@ -1076,71 +1075,5 @@ private static CoreReservation BuildCoreReservation() }; } } - - private sealed class ResourceManagerTraceDataHandler : ITraceDataHandler - { - private readonly MediaOpsPlanApi planApi; - - private readonly Dictionary traceDataPerReservationId = new Dictionary(); - - public ResourceManagerTraceDataHandler(MediaOpsPlanApi planApi) - { - this.planApi = planApi ?? throw new ArgumentNullException(nameof(planApi)); - } - - public IReadOnlyDictionary Translate(ICollection resourceManagerErrors) - { - if (resourceManagerErrors == null) - { - throw new ArgumentNullException(nameof(resourceManagerErrors)); - } - - if (resourceManagerErrors.Count == 0) - { - return new Dictionary(); - } - - var reservationUpdateCausedReservationsToGoToQuarantineErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine).ToList(); - var resourceCapacityInvalidErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ResourceCapacityInvalid).ToList(); - var resourceCapabilityInvalidErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ResourceCapabilityInvalid).ToList(); - if ((reservationUpdateCausedReservationsToGoToQuarantineErrors.Count + resourceCapacityInvalidErrors.Count + resourceCapabilityInvalidErrors.Count) != resourceManagerErrors.Count) - { - return ReturnDefaultTraceData(resourceManagerErrors); - } - - throw new NotImplementedException(); - } - - private MediaOpsTraceData GetOrCreateTraceData(Guid id) - { - if (!traceDataPerReservationId.TryGetValue(id, out var traceData)) - { - traceData = new MediaOpsTraceData(); - traceDataPerReservationId[id] = traceData; - } - - return traceData; - } - - private Dictionary ReturnDefaultTraceData(ICollection resourceManagerErrors) - { - foreach (var error in resourceManagerErrors) - { - if (error.SubjectId == Guid.Empty) - { - planApi.Logger.Error(this, $"Error with reason {error.ErrorReason} has empty SubjectId. This should not happen. Error message: {error.Message}"); - continue; - } - - var traceData = GetOrCreateTraceData(error.SubjectId.Value); - traceData.Add(new MediaOpsErrorData - { - ErrorMessage = error.ToString(), - }); - } - - return traceDataPerReservationId; - } - } } } diff --git a/DevPack/API/Handlers/Workflow/Jobs/ResourceManagerTraceDataHandler.cs b/DevPack/API/Handlers/Workflow/Jobs/ResourceManagerTraceDataHandler.cs new file mode 100644 index 00000000..cf6f06c1 --- /dev/null +++ b/DevPack/API/Handlers/Workflow/Jobs/ResourceManagerTraceDataHandler.cs @@ -0,0 +1,279 @@ +namespace Skyline.DataMiner.Solutions.MediaOps.Plan.API +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Skyline.DataMiner.Net.Apps.DataMinerObjectModel; + using Skyline.DataMiner.Net.Apps.ManagerStore.Select; + using Skyline.DataMiner.Net.Messages.SLDataGateway; + using Skyline.DataMiner.Net.ResponseErrorData; + using Skyline.DataMiner.Solutions.MediaOps.Plan.Exceptions; + using Skyline.DataMiner.Solutions.MediaOps.Plan.Storage.DOM.SlcResource_Studio; + + // Translates core ResourceManager errors into DevPack job resource errors keyed per reservation (SubjectId). Only the + // resource, capacity and capability ids are surfaced; the consumer is responsible for resolving them to names. + internal sealed class ResourceManagerTraceDataHandler : ITraceDataHandler + { + private readonly MediaOpsPlanApi planApi; + + private readonly Dictionary traceDataPerReservationId = new Dictionary(); + + public ResourceManagerTraceDataHandler(MediaOpsPlanApi planApi) + { + this.planApi = planApi ?? throw new ArgumentNullException(nameof(planApi)); + } + + public IReadOnlyDictionary Translate(ICollection resourceManagerErrors) + { + if (resourceManagerErrors == null) + { + throw new ArgumentNullException(nameof(resourceManagerErrors)); + } + + traceDataPerReservationId.Clear(); + + if (resourceManagerErrors.Count == 0) + { + return traceDataPerReservationId; + } + + var reservationUpdateCausedReservationsToGoToQuarantineErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine).ToList(); + var resourceCapacityInvalidErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ResourceCapacityInvalid).ToList(); + var resourceCapabilityInvalidErrors = resourceManagerErrors.Where(x => x.ErrorReason == ResourceManagerErrorData.Reason.ResourceCapabilityInvalid).ToList(); + + // The DevPack surfaces resources by their Resource Studio (DOM) id, so the core resource ids in the errors are + // resolved to their DOM counterparts in a batched query (one or more backend calls, depending on filter size). + var domResourceIdByCoreId = BuildDomResourceIdByCoreId( + reservationUpdateCausedReservationsToGoToQuarantineErrors, + resourceCapacityInvalidErrors, + resourceCapabilityInvalidErrors); + + HandleReservationUpdateCausedReservationsToGoToQuarantine(reservationUpdateCausedReservationsToGoToQuarantineErrors, domResourceIdByCoreId); + HandleResourceCapacityInvalid(resourceCapacityInvalidErrors, domResourceIdByCoreId); + HandleResourceCapabilityInvalid(resourceCapabilityInvalidErrors, domResourceIdByCoreId); + + // Errors that are not one of the known types are added to their reservation's trace data as raw defaults. + AddDefaultTraceData(GetUnknownErrors(resourceManagerErrors)); + + return traceDataPerReservationId; + } + + private static List GetUnknownErrors(ICollection resourceManagerErrors) + { + return resourceManagerErrors + .Where(x => x.ErrorReason != ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine + && x.ErrorReason != ResourceManagerErrorData.Reason.ResourceCapacityInvalid + && x.ErrorReason != ResourceManagerErrorData.Reason.ResourceCapabilityInvalid) + .ToList(); + } + + private Dictionary BuildDomResourceIdByCoreId( + IEnumerable quarantineErrors, + IEnumerable capacityErrors, + IEnumerable capabilityErrors) + { + var coreResourceIds = new HashSet(); + + foreach (var error in quarantineErrors) + { + foreach (var coreResourceId in GetQuarantinedCoreResourceIds(error)) + { + coreResourceIds.Add(coreResourceId); + } + } + + foreach (var error in capacityErrors.Concat(capabilityErrors)) + { + if (error.ResourceId.HasValue) + { + coreResourceIds.Add(error.ResourceId.Value); + } + } + + if (coreResourceIds.Count == 0) + { + return new Dictionary(); + } + + var selectedFields = new SelectedFields() + .Add(SlcResource_StudioIds.Sections.ResourceInternalProperties.Resource_Id); + + FilterElement Filter(Guid coreResourceId) => + DomInstanceExposers.DomDefinitionId.Equal(SlcResource_StudioIds.Definitions.Resource.Id) + .AND(DomInstanceExposers.FieldValues.DomInstanceField(SlcResource_StudioIds.Sections.ResourceInternalProperties.Resource_Id).Equal(coreResourceId)); + + var domResourceIdByCoreId = new Dictionary(); + foreach (var partialObject in planApi.DomHelpers.SlcResourceStudioHelper.GetResourceStudioFields(coreResourceIds, Filter, selectedFields)) + { + if (!partialObject.TryGetValue(SlcResource_StudioIds.Sections.ResourceInternalProperties.Resource_Id, out var coreId) + || coreId == Guid.Empty) + { + continue; + } + + domResourceIdByCoreId[coreId] = partialObject.Id.Id; + } + + return domResourceIdByCoreId; + } + + private void HandleReservationUpdateCausedReservationsToGoToQuarantine( + IReadOnlyCollection quarantineErrors, + IReadOnlyDictionary domResourceIdByCoreId) + { + foreach (var error in quarantineErrors) + { + if (!TryGetReservationId(error, out var reservationId)) + { + continue; + } + + var traceData = GetOrCreateTraceData(reservationId); + + var emittedAny = false; + foreach (var coreResourceId in GetQuarantinedCoreResourceIds(error).Distinct()) + { + if (!domResourceIdByCoreId.TryGetValue(coreResourceId, out var domResourceId)) + { + planApi.Logger.Error(this, $"Could not resolve a Resource Studio resource for core resource {coreResourceId}."); + continue; + } + + traceData.Add(new JobResourceNotAvailableError + { + ErrorMessage = error.Message, + ResourceId = domResourceId, + }); + emittedAny = true; + } + + if (!emittedAny) + { + AddRawFallback(traceData, error); + } + } + } + + private void HandleResourceCapacityInvalid( + IReadOnlyCollection capacityErrors, + IReadOnlyDictionary domResourceIdByCoreId) + { + foreach (var error in capacityErrors) + { + if (!TryGetReservationId(error, out var reservationId)) + { + continue; + } + + var traceData = GetOrCreateTraceData(reservationId); + + if (!TryGetDomResourceId(error, domResourceIdByCoreId, out var domResourceId)) + { + AddRawFallback(traceData, error); + continue; + } + + traceData.Add(new JobResourceInvalidCapacityError + { + ErrorMessage = error.Message, + ResourceId = domResourceId, + CapacityId = error.ResourceCapacityUsage.CapacityProfileID, + }); + } + } + + private void HandleResourceCapabilityInvalid( + IReadOnlyCollection capabilityErrors, + IReadOnlyDictionary domResourceIdByCoreId) + { + foreach (var error in capabilityErrors) + { + if (!TryGetReservationId(error, out var reservationId)) + { + continue; + } + + var traceData = GetOrCreateTraceData(reservationId); + + if (!TryGetDomResourceId(error, domResourceIdByCoreId, out var domResourceId)) + { + AddRawFallback(traceData, error); + continue; + } + + traceData.Add(new JobResourceInvalidCapabilityError + { + ErrorMessage = error.Message, + ResourceId = domResourceId, + CapabilityId = error.ResourceCapabilityUsage.CapabilityProfileID, + }); + } + } + + private static IEnumerable GetQuarantinedCoreResourceIds(ResourceManagerErrorData error) + { + return error.MustBeMovedToQuarantine + .SelectMany(x => x.QuarantinedUsages) + .Select(x => x.QuarantinedResourceUsage.GUID); + } + + private bool TryGetDomResourceId(ResourceManagerErrorData error, IReadOnlyDictionary domResourceIdByCoreId, out Guid domResourceId) + { + domResourceId = Guid.Empty; + + if (!error.ResourceId.HasValue || !domResourceIdByCoreId.TryGetValue(error.ResourceId.Value, out domResourceId)) + { + planApi.Logger.Error(this, $"Could not resolve a Resource Studio resource for core resource {error.ResourceId}."); + return false; + } + + return true; + } + + private bool TryGetReservationId(ResourceManagerErrorData error, out Guid reservationId) + { + reservationId = error.SubjectId.GetValueOrDefault(); + if (reservationId == Guid.Empty) + { + planApi.Logger.Error(this, $"Error with reason {error.ErrorReason} has empty SubjectId. This should not happen. Error message: {error.Message}"); + return false; + } + + return true; + } + + private void AddRawFallback(MediaOpsTraceData traceData, ResourceManagerErrorData error) + { + planApi.Logger.Error(this, $"Falling back to the raw error data (ToString) for an error with reason {error.ErrorReason}."); + traceData.Add(new MediaOpsErrorData { ErrorMessage = error.ToString() }); + } + + private MediaOpsTraceData GetOrCreateTraceData(Guid id) + { + if (!traceDataPerReservationId.TryGetValue(id, out var traceData)) + { + traceData = new MediaOpsTraceData(); + traceDataPerReservationId[id] = traceData; + } + + return traceData; + } + + private void AddDefaultTraceData(ICollection resourceManagerErrors) + { + foreach (var error in resourceManagerErrors) + { + if (!TryGetReservationId(error, out var reservationId)) + { + continue; + } + + GetOrCreateTraceData(reservationId).Add(new MediaOpsErrorData + { + ErrorMessage = error.ToString(), + }); + } + } + } +} diff --git a/DevPack/Storage/DOM/Helpers/SlcResourceStudioHelper.cs b/DevPack/Storage/DOM/Helpers/SlcResourceStudioHelper.cs index 2bbae41c..c709ec65 100644 --- a/DevPack/Storage/DOM/Helpers/SlcResourceStudioHelper.cs +++ b/DevPack/Storage/DOM/Helpers/SlcResourceStudioHelper.cs @@ -6,10 +6,12 @@ using Skyline.DataMiner.Net; using Skyline.DataMiner.Net.Apps.DataMinerObjectModel; + using Skyline.DataMiner.Net.Apps.ManagerStore.Select; using Skyline.DataMiner.Net.Messages.SLDataGateway; using Skyline.DataMiner.Solutions.MediaOps.Plan.Storage.DOM.SlcResource_Studio; using Skyline.DataMiner.Utils.DOM.Extensions; + using SLDataGateway.API.Querying; using SLDataGateway.API.Types.Querying; internal class SlcResourceStudioHelper : DomModuleHelperBase @@ -230,6 +232,29 @@ public IEnumerable GetResourceStudioInstances(IEnumerable ids x => DomHelper.DomInstances.Read(x)); } + public IEnumerable> GetResourceStudioFields(IEnumerable values, Func> filter, SelectedFields selectedFields) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (filter == null) + { + throw new ArgumentNullException(nameof(filter)); + } + + if (selectedFields == null) + { + throw new ArgumentNullException(nameof(selectedFields)); + } + + return FilterQueryExecutor.RetrieveFilteredItems( + values.Distinct(), + x => filter(x), + x => DomHelper.DomInstances.Read(x.ToQuery(), selectedFields)); + } + public DomInstance TransitionResourceToComplete(Guid resourceId) { var transitionId = SlcResource_StudioIds.Behaviors.Resource_Behavior.Transitions.Draft_To_Complete; diff --git a/Directory.Packages.props b/Directory.Packages.props index 19a60bee..14617079 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,13 +13,12 @@ - - - + + + -