Skip to content

Commit 09537f9

Browse files
committed
Support allow-list for SOP classes. (#91)
* gh-33 Support allow-list for SOP classes. * gh-33 Update DB migration Signed-off-by: Victor Chang <[email protected]>
1 parent f424204 commit 09537f9

33 files changed

+290
-53
lines changed

docs/api/rest/config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ curl --location --request GET 'http://localhost:5000/config/ae'
4141
"workflows": ["brain-tumor", "b75cd27a-068a-4f9c-b3da-e5d4ea08c55a"],
4242
"grouping": "0020,000D",
4343
"timeout": 5,
44-
"ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"]
44+
"ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"],
45+
"allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"]
4546
},
4647
{
4748
"name": "liver-seg",

src/Api/MonaiApplicationEntity.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace Monai.Deploy.InformaticsGateway.Api
2626
/// "workflows": [ "EXAM", "Delta", "b75cd27a-068a-4f9c-b3da-e5d4ea08c55a"],
2727
/// "grouping": [ "0010,0020"],
2828
/// "ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"],
29+
/// "allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"],
2930
/// "timeout": 300
3031
/// }
3132
/// </code>
@@ -58,9 +59,16 @@ public class MonaiApplicationEntity
5859

5960
/// <summary>
6061
/// Optional field to specify SOP Class UIDs to ignore.
62+
/// <see cref="IgnoredSopClasses"/> and <see cref="AllowedSopClasses"/> are mutually exclusive.
6163
/// </summary>
6264
public List<string> IgnoredSopClasses { get; set; }
6365

66+
/// <summary>
67+
/// Optional field to specify accepted SOP Class UIDs.
68+
/// <see cref="IgnoredSopClasses"/> and <see cref="AllowedSopClasses"/> are mutually exclusive.
69+
/// </summary>
70+
public List<string> AllowedSopClasses { get; set; }
71+
6472
/// <summary>
6573
/// Timeout, in seconds, to wait for instances before notifying other subsystems of data arrival
6674
/// for the specified data group.
@@ -89,6 +97,11 @@ public void SetDefaultValues()
8997
{
9098
IgnoredSopClasses = new List<string>();
9199
}
100+
101+
if (AllowedSopClasses is null)
102+
{
103+
AllowedSopClasses = new List<string>();
104+
}
92105
}
93106
}
94107
}

src/CLI/Commands/AetCommand.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.CommandLine;
7-
using System.CommandLine.Invocation;
7+
using System.CommandLine.NamingConventionBinder;
88
using System.CommandLine.Rendering;
99
using System.CommandLine.Rendering.Views;
1010
using System.Linq;
@@ -65,20 +65,24 @@ private void SetupAddAetCommand()
6565
addCommand.AddOption(groupingOption);
6666
var timeoutOption = new Option<uint>(new string[] { "-t", "--timeout" }, getDefaultValue: () => 5, "Timeout, in seconds, to wait for instances") { IsRequired = false };
6767
addCommand.AddOption(timeoutOption);
68-
var workflowsOption = new Option<string[]>(new string[] { "-w", "--workflows" }, () => Array.Empty<string>(), "A space separated list of workflow names or IDs to be associated with the SCP AE Title")
68+
var workflowsOption = new Option<List<string>>(new string[] { "-w", "--workflows" }, description: "A space separated list of workflow names or IDs to be associated with the SCP AE Title")
6969
{
7070
AllowMultipleArgumentsPerToken = true,
7171
IsRequired = false,
72-
Name = "--workflows"
7372
};
7473
addCommand.AddOption(workflowsOption);
75-
var ignoredSopsOption = new Option<string[]>(new string[] { "-i", "--ignored-sops" }, () => Array.Empty<string>(), "A space separated list of SOP Class UIDs to be ignoredS")
74+
var ignoredSopsOption = new Option<List<string>>(new string[] { "-i", "--ignored-sop-classes" }, description: "A space separated list of SOP Class UIDs to be ignored")
7675
{
7776
AllowMultipleArgumentsPerToken = true,
7877
IsRequired = false,
79-
Name = "--ignored-sops"
8078
};
8179
addCommand.AddOption(ignoredSopsOption);
80+
var allowedSopsOption = new Option<List<string>>(new string[] { "-s", "--allowed-sop-classes" }, description: "A space separated list of SOP Class UIDs to be accepted")
81+
{
82+
AllowMultipleArgumentsPerToken = true,
83+
IsRequired = false,
84+
};
85+
addCommand.AddOption(allowedSopsOption);
8286

8387
addCommand.Handler = CommandHandler.Create<MonaiApplicationEntity, IHost, bool, CancellationToken>(AddAeTitlehandlerAsync);
8488
}
@@ -139,8 +143,14 @@ private async Task<int> ListAeTitlehandlerAsync(IHost host, bool verbose, Cancel
139143
};
140144
table.AddColumn(p => p.Name, new ContentView("Name".Underline()));
141145
table.AddColumn(p => p.AeTitle, new ContentView("AE Title".Underline()));
146+
table.AddColumn(p => p.Timeout, new ContentView("Timeout".Underline()));
147+
table.AddColumn(p => p.Grouping, new ContentView("Grouping".Underline()));
142148
table.AddColumn(p => p.Workflows.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.Workflows), new ContentView("Workflows".Underline()));
149+
table.AddColumn(p => p.AllowedSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.AllowedSopClasses), new ContentView("Accepted SOP Classes".Underline()));
150+
table.AddColumn(p => p.IgnoredSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.IgnoredSopClasses), new ContentView("Ignored SOP Classes".Underline()));
143151
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());
152+
153+
logger.ListedNItems(items.Count);
144154
}
145155
return ExitCodes.Success;
146156
}
@@ -213,7 +223,12 @@ private async Task<int> AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH
213223
if (result.IgnoredSopClasses.Any())
214224
{
215225
logger.MonaiAeIgnoredSops(string.Join(',', result.IgnoredSopClasses));
216-
logger.IgnoreSopClassesWarning();
226+
logger.IgnoredSopClassesWarning();
227+
}
228+
if (result.AllowedSopClasses.Any())
229+
{
230+
logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses));
231+
logger.AcceptedSopClassesWarning();
217232
}
218233
}
219234
catch (ConfigurationException ex)

src/CLI/Commands/ConfigCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using System;
55
using System.CommandLine;
6-
using System.CommandLine.Invocation;
6+
using System.CommandLine.NamingConventionBinder;
77
using System.Threading;
88
using System.Threading.Tasks;
99
using Ardalis.GuardClauses;

src/CLI/Commands/DestinationCommand.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.CommandLine;
77
using System.CommandLine.Invocation;
8+
using System.CommandLine.NamingConventionBinder;
89
using System.CommandLine.Rendering;
910
using System.CommandLine.Rendering.Views;
1011
using System.Linq;
@@ -130,6 +131,8 @@ private async Task<int> ListDestinationHandlerAsync(DestinationApplicationEntity
130131
table.AddColumn(p => p.HostIp, new ContentView("Host/IP Address".Underline()));
131132
table.AddColumn(p => p.Port, new ContentView("Port".Underline()));
132133
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());
134+
135+
logger.ListedNItems(items.Count);
133136
}
134137
return ExitCodes.Success;
135138
}

src/CLI/Commands/RestartCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache License 2.0
33

44
using System;
5-
using System.CommandLine.Invocation;
5+
using System.CommandLine.NamingConventionBinder;
66
using System.Threading;
77
using System.Threading.Tasks;
88
using Ardalis.GuardClauses;

src/CLI/Commands/SourceCommand.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.CommandLine;
77
using System.CommandLine.Invocation;
8+
using System.CommandLine.NamingConventionBinder;
89
using System.CommandLine.Rendering;
910
using System.CommandLine.Rendering.Views;
1011
using System.Linq;
@@ -126,6 +127,8 @@ private async Task<int> ListSourceHandlerAsync(SourceApplicationEntity entity, I
126127
table.AddColumn(p => p.AeTitle, new ContentView("AE Title".Underline()));
127128
table.AddColumn(p => p.HostIp, new ContentView("Host/IP Address".Underline()));
128129
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());
130+
131+
logger.ListedNItems(items.Count);
129132
}
130133
return ExitCodes.Success;
131134
}

src/CLI/Commands/StartCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.CommandLine.Invocation;
6+
using System.CommandLine.NamingConventionBinder;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Ardalis.GuardClauses;

src/CLI/Commands/StatusCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.CommandLine.Invocation;
6+
using System.CommandLine.NamingConventionBinder;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Ardalis.GuardClauses;

src/CLI/Commands/StopCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.CommandLine.Invocation;
6+
using System.CommandLine.NamingConventionBinder;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Ardalis.GuardClauses;

src/CLI/Logging/Log.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static partial class Log
3434
public static partial void MonaiAeIgnoredSops(this ILogger logger, string ignoredSopClasses);
3535

3636
[LoggerMessage(EventId = 30014, Level = LogLevel.Warning, Message = "Instances with matching SOP class UIDs are accepted but dropped.")]
37-
public static partial void IgnoreSopClassesWarning(this ILogger logger);
37+
public static partial void IgnoredSopClassesWarning(this ILogger logger);
3838

3939
[LoggerMessage(EventId = 30015, Level = LogLevel.Critical, Message = "Error creating MONAI SCP AE Title {aeTitle}: {message}.")]
4040
public static partial void MonaiAeCreateCritical(this ILogger logger, string aeTitle, string message);
@@ -111,7 +111,7 @@ public static partial class Log
111111
[LoggerMessage(EventId = 30040, Level = LogLevel.Information, Message = "\t\t{name}: {status}")]
112112
public static partial void ServiceStatusItem(this ILogger logger, string name, ServiceStatus status);
113113

114-
[LoggerMessage(EventId = 30041, Level = LogLevel.Warning, Message = "Action cancelled.")]
114+
[LoggerMessage(EventId = 30041, Level = LogLevel.Warning, Message = "Action canceled.")]
115115
public static partial void ActionCancelled(this ILogger logger);
116116

117117
[LoggerMessage(EventId = 30042, Level = LogLevel.Critical, Message = "Error restarting {applicationName}: {message}.")]
@@ -138,6 +138,18 @@ public static partial class Log
138138
[LoggerMessage(EventId = 30049, Level = LogLevel.Information, Message = "Configuration updated successfully.")]
139139
public static partial void ConfigurationUpdated(this ILogger logger);
140140

141+
[LoggerMessage(EventId = 30050, Level = LogLevel.Information, Message = "\tAccepted SOP Classes: {alowedSopClasses}")]
142+
public static partial void MonaiAeAllowedSops(this ILogger logger, string alowedSopClasses);
143+
144+
[LoggerMessage(EventId = 30051, Level = LogLevel.Warning, Message = "Instances without matching SOP class UIDs are accepted but dropped.")]
145+
public static partial void AllowedSopClassesWarning(this ILogger logger);
146+
147+
[LoggerMessage(EventId = 30052, Level = LogLevel.Warning, Message = "Only instances with matching SOP class UIDs are accepted and stored.")]
148+
public static partial void AcceptedSopClassesWarning(this ILogger logger);
149+
150+
[LoggerMessage(EventId = 30053, Level = LogLevel.Information, Message = "\n\nFound {count} items.")]
151+
public static partial void ListedNItems(this ILogger logger, int count);
152+
141153
// Docker Runner
142154
[LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")]
143155
public static partial void CheckingExistingAppContainer(this ILogger logger, string applicationName, string version);

src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SPDX-License-Identifier: Apache License 2.0
1212
<SelfContained>true</SelfContained>
1313
<PublishTrimmed>false</PublishTrimmed>
1414
<PublishReadyToRun>true</PublishReadyToRun>
15-
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
15+
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
1616
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
1717
<AssemblyName>mig-cli</AssemblyName>
1818
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
@@ -48,9 +48,9 @@ SPDX-License-Identifier: Apache License 2.0
4848
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
4949
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
5050
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
51-
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21308.1" />
52-
<PackageReference Include="System.CommandLine.Hosting" Version="0.3.0-alpha.21216.1" />
53-
<PackageReference Include="System.CommandLine.Rendering" Version="0.3.0-alpha.21216.1" />
51+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
52+
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22272.1" />
53+
<PackageReference Include="System.CommandLine.Rendering" Version="0.4.0-alpha.22272.1" />
5454
<PackageReference Include="System.IO.Abstractions" Version="17.0.18" />
5555
</ItemGroup>
5656
</Project>

src/CLI/Program.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ private static async Task<int> Main(string[] args)
2929
internal static Parser BuildParser()
3030
{
3131
var verboseOption = new Option<bool>(new[] { "--verbose", "-v" }, () => false, "Show verbose output");
32-
return new CommandLineBuilder(new RootCommand($"{Strings.ApplicationName} CLI"))
32+
var commandLineBuilder = new CommandLineBuilder(new RootCommand($"{Strings.ApplicationName} CLI"))
3333
.UseHost(
3434
_ => Host.CreateDefaultBuilder(),
3535
host =>
3636
{
3737
_ = host.ConfigureLogging((context, logging) =>
3838
{
3939
var invocationContext = context.GetInvocationContext();
40-
var verboseEnabled = invocationContext.ParseResult.ValueForOption(verboseOption);
40+
var verboseEnabled = invocationContext.ParseResult.GetValueForOption(verboseOption);
4141
logging.ClearProviders();
4242

4343
_ = logging.AddInformaticsGatewayConsole(options => options.MinimumLogLevel = verboseEnabled ? LogLevel.Trace : LogLevel.Information)
@@ -60,22 +60,22 @@ internal static Parser BuildParser()
6060
services.AddTransient<IDockerClient>(p => new DockerClientConfiguration().CreateClient());
6161
});
6262
})
63-
.AddGlobalOption(verboseOption)
64-
.AddCommand(new ConfigCommand())
65-
.AddCommand(new StartCommand())
66-
.AddCommand(new StopCommand())
67-
.AddCommand(new RestartCommand())
68-
.AddCommand(new AetCommand())
69-
.AddCommand(new SourceCommand())
70-
.AddCommand(new DestinationCommand())
71-
.AddCommand(new StatusCommand())
7263
.UseAnsiTerminalWhenAvailable()
7364
.UseExceptionHandler((exception, context) =>
7465
{
7566
Console.Out.WriteLineAsync(Crayon.Output.Bright.Red($"Exception: {exception.Message}"));
7667
})
77-
.UseDefaults()
78-
.Build();
68+
.UseDefaults();
69+
commandLineBuilder.Command.AddGlobalOption(verboseOption);
70+
commandLineBuilder.Command.AddCommand(new ConfigCommand());
71+
commandLineBuilder.Command.AddCommand(new StartCommand());
72+
commandLineBuilder.Command.AddCommand(new StopCommand());
73+
commandLineBuilder.Command.AddCommand(new RestartCommand());
74+
commandLineBuilder.Command.AddCommand(new AetCommand());
75+
commandLineBuilder.Command.AddCommand(new SourceCommand());
76+
commandLineBuilder.Command.AddCommand(new DestinationCommand());
77+
commandLineBuilder.Command.AddCommand(new StatusCommand());
78+
return commandLineBuilder.Build();
7979
}
8080
}
8181
}

src/CLI/Test/AetCommandTest.cs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public AetCommandTest()
5353
services.AddSingleton<IInformaticsGatewayClient>(p => _informaticsGatewayClient.Object);
5454
services.AddSingleton<IConfigurationService>(p => _configurationService.Object);
5555
});
56-
})
57-
.AddCommand(new AetCommand());
56+
});
57+
_commandLineBuilder.Command.AddCommand(new AetCommand());
5858
_paser = _commandLineBuilder.Build();
5959

6060
_loggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
@@ -110,6 +110,54 @@ public async Task AetAdd_Command()
110110
It.IsAny<CancellationToken>()), Times.Once());
111111
}
112112

113+
[Fact(DisplayName = "aet add comand with allowed & ignored SOP classes")]
114+
public async Task AetAdd_Command_AllowedIgnoredSopClasses()
115+
{
116+
var command = "aet add -n MyName -a MyAET --workflows App MyCoolApp TheApp -i A B C -s D E F";
117+
var result = _paser.Parse(command);
118+
Assert.Equal(ExitCodes.Success, result.Errors.Count);
119+
120+
var entity = new MonaiApplicationEntity()
121+
{
122+
Name = result.CommandResult.Children[0].Tokens[0].Value,
123+
AeTitle = result.CommandResult.Children[1].Tokens[0].Value,
124+
Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(),
125+
IgnoredSopClasses = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(),
126+
AllowedSopClasses = result.CommandResult.Children[4].Tokens.Select(p => p.Value).ToList(),
127+
};
128+
Assert.Equal("MyName", entity.Name);
129+
Assert.Equal("MyAET", entity.AeTitle);
130+
Assert.Collection(entity.Workflows,
131+
item => item.Equals("App"),
132+
item => item.Equals("MyCoolApp"),
133+
item => item.Equals("TheApp"));
134+
Assert.Collection(entity.AllowedSopClasses,
135+
item => item.Equals("D"),
136+
item => item.Equals("E"),
137+
item => item.Equals("F"));
138+
Assert.Collection(entity.IgnoredSopClasses,
139+
item => item.Equals("A"),
140+
item => item.Equals("B"),
141+
item => item.Equals("C"));
142+
143+
_informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Create(It.IsAny<MonaiApplicationEntity>(), It.IsAny<CancellationToken>()))
144+
.ReturnsAsync(entity);
145+
146+
int exitCode = await _paser.InvokeAsync(command);
147+
148+
Assert.Equal(ExitCodes.Success, exitCode);
149+
150+
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny<Uri>()), Times.Once());
151+
_informaticsGatewayClient.Verify(
152+
p => p.MonaiScpAeTitle.Create(
153+
It.Is<MonaiApplicationEntity>(o => o.AeTitle == entity.AeTitle &&
154+
o.Name == entity.Name &&
155+
Enumerable.SequenceEqual(o.Workflows, entity.Workflows) &&
156+
Enumerable.SequenceEqual(o.AllowedSopClasses, entity.AllowedSopClasses) &&
157+
Enumerable.SequenceEqual(o.IgnoredSopClasses, entity.IgnoredSopClasses)),
158+
It.IsAny<CancellationToken>()), Times.Once());
159+
}
160+
113161
[Fact(DisplayName = "aet add comand exception")]
114162
public async Task AetAdd_Command_Exception()
115163
{

0 commit comments

Comments
 (0)