Skip to content

Commit cb74f3a

Browse files
committed
gh-33 Support allow-list for SOP classes.
Signed-off-by: Victor Chang <[email protected]>
1 parent 67476d2 commit cb74f3a

28 files changed

+252
-46
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: 16 additions & 5 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
}
@@ -140,6 +144,8 @@ private async Task<int> ListAeTitlehandlerAsync(IHost host, bool verbose, Cancel
140144
table.AddColumn(p => p.Name, new ContentView("Name".Underline()));
141145
table.AddColumn(p => p.AeTitle, new ContentView("AE Title".Underline()));
142146
table.AddColumn(p => p.Workflows.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.Workflows), new ContentView("Workflows".Underline()));
147+
table.AddColumn(p => p.AllowedSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.AllowedSopClasses), new ContentView("Accepted SOP Classes".Underline()));
148+
table.AddColumn(p => p.IgnoredSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.IgnoredSopClasses), new ContentView("Ignored SOP Classes".Underline()));
143149
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());
144150
}
145151
return ExitCodes.Success;
@@ -215,6 +221,11 @@ private async Task<int> AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH
215221
logger.MonaiAeIgnoredSops(string.Join(',', result.IgnoredSopClasses));
216222
logger.IgnoreSopClassesWarning();
217223
}
224+
if (result.AllowedSopClasses.Any())
225+
{
226+
logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses));
227+
logger.IgnoreSopClassesWarning();
228+
}
218229
}
219230
catch (ConfigurationException ex)
220231
{

src/CLI/Commands/ConfigCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.CommandLine;
66
using System.CommandLine.Invocation;
7+
using System.CommandLine.NamingConventionBinder;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Ardalis.GuardClauses;

src/CLI/Commands/DestinationCommand.cs

Lines changed: 1 addition & 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;

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: 1 addition & 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;

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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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,12 @@ 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+
141147
// Docker Runner
142148
[LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")]
143149
public static partial void CheckingExistingAppContainer(this ILogger logger, string applicationName, string version);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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="16.1.25" />
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
{

src/CLI/Test/CommandBaseTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using System.CommandLine;
55
using System.CommandLine.Builder;
66
using System.CommandLine.Hosting;
7-
using System.CommandLine.Invocation;
7+
using System.CommandLine.NamingConventionBinder;
88
using System.CommandLine.Parsing;
99
using System.Threading.Tasks;
1010
using Microsoft.Extensions.DependencyInjection;
@@ -36,9 +36,9 @@ public CommandBaseTest()
3636
{
3737
services.AddSingleton<ILoggerFactory>(p => _loggerFactory.Object);
3838
});
39-
})
40-
.AddGlobalOption(new Option<bool>(new[] { "--verbose", "-v" }, () => false, "Show verbose output"))
41-
.AddCommand(new TestCommand());
39+
});
40+
_commandLineBuilder.Command.AddGlobalOption(new Option<bool>(new[] { "--verbose", "-v" }, () => false, "Show verbose output"));
41+
_commandLineBuilder.Command.AddCommand(new TestCommand());
4242
_paser = _commandLineBuilder.Build();
4343
_loggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
4444
_logger.Setup(p => p.IsEnabled(It.IsAny<LogLevel>())).Returns(true);

src/CLI/Test/ConfigCommandTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public ConfigCommandTest()
4545
services.AddSingleton<ILoggerFactory>(p => _loggerFactory.Object);
4646
services.AddSingleton<IConfigurationService>(p => _configurationService.Object);
4747
});
48-
})
49-
.AddCommand(new ConfigCommand());
48+
});
49+
_commandLineBuilder.Command.AddCommand(new ConfigCommand());
5050
_paser = _commandLineBuilder.Build();
5151
_loggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
5252
_logger.Setup(p => p.IsEnabled(It.IsAny<LogLevel>())).Returns(true);

src/CLI/Test/DestinationCommandTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public DestinationCommandTest()
5454
services.AddSingleton<IInformaticsGatewayClient>(p => _informaticsGatewayClient.Object);
5555
services.AddSingleton<IConfigurationService>(p => _configurationService.Object);
5656
});
57-
})
58-
.AddCommand(new DestinationCommand());
57+
});
58+
_commandLineBuilder.Command.AddCommand(new DestinationCommand());
5959
_paser = _commandLineBuilder.Build();
6060

6161
_loggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
</PackageReference>
2323
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
2424
<PackageReference Include="Moq" Version="4.17.2" />
25-
<PackageReference Include="System.CommandLine.Hosting" Version="0.3.0-alpha.21216.1" />
25+
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22272.1" />
2626
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="16.1.25" />
2727
<PackageReference Include="xRetry" Version="1.8.0" />
2828
<PackageReference Include="xunit" Version="2.4.1" />

0 commit comments

Comments
 (0)