From 7a88124604595839cdfebcc123f9b64bff9269aa Mon Sep 17 00:00:00 2001 From: Eugene Bekker Date: Sun, 28 Jan 2018 23:05:00 -0500 Subject: [PATCH] First major commit in support of #87 Migrated first 3 major projects over to .NET Core/Standard 2.0 - adjusted configuration handling (still some work left here) - adjusted logging setup (still some work left here) - added support for running as a Windows Service - cleaned up namespace naming conventions NOTE, unit tests have not yet been moved over. --- .vscode/launch.json | 13 +- .vscode/settings.json | 9 + TugDSC.common.props | 56 ++- TugDSC.sharedasm.props | 54 +++ TugDSC.sln | 69 ++++ appveyor.yml | 52 +++ .../Configuration/ReadOnlyConfiguration.cs | 85 ++++ .../ReadOnlyConfigurationSection.cs | 106 +++++ src/TugDSC.Abstractions/Ext/IProvider.cs | 21 + .../Ext/IProviderManager.cs | 19 + .../Ext/IProviderProduct.cs | 19 + src/TugDSC.Abstractions/Ext/ProviderInfo.cs | 27 ++ .../Ext/ProviderParameterInfo.cs | 34 ++ .../Ext/Util/MefExtensions.cs | 103 +++++ .../Ext/Util/ProviderExtensions.cs | 222 +++++++++++ .../Ext/Util/ProviderManagerBase.cs | 298 ++++++++++++++ ...ServiceProviderExportDescriptorProvider.cs | 61 +++ .../Messages/DscRequest.cs | 110 ++++++ .../Messages/DscResponse.cs | 20 + .../Messages/GetConfiguration.cs | 47 +++ .../Messages/GetDscAction.cs | 35 ++ src/TugDSC.Abstractions/Messages/GetModule.cs | 64 +++ .../Messages/GetReports.cs | 46 +++ .../Messages/ModelBinding/ModelBinding.cs | 56 +++ .../Messages/RegisterDscAgent.cs | 42 ++ .../Messages/SendReport.cs | 29 ++ .../Model/ActionDetailsItem.cs | 21 + .../Model/AgentInformation.cs | 28 ++ .../Model/CertificateInformation.cs | 65 +++ .../Model/ClientStatusItem.cs | 40 ++ .../Model/GetDscActionRequestBody.cs | 17 + .../Model/GetDscActionResponseBody.cs | 20 + .../Model/GetReportsResponseBody.cs | 17 + src/TugDSC.Abstractions/Model/ModelCommon.cs | 65 +++ .../Model/RegisterDscAgentRequestBody.cs | 34 ++ .../Model/RegistrationInformation.cs | 24 ++ .../Model/SendReportBody.cs | 235 +++++++++++ src/TugDSC.Abstractions/README.md | 3 + .../TugDSC.Abstractions.csproj | 35 ++ .../Util/ExceptionExtensions.cs | 17 + src/TugDSC.Abstractions/Util/ExtDataBase.cs | 23 ++ .../Util/ExtDataExtensions.cs | 47 +++ .../Util/ExtDataIndexerBase.cs | 18 + src/TugDSC.Abstractions/Util/IExtData.cs | 19 + .../ActionStatus.cs | 19 + .../Configuration/AppSettings.cs | 19 + .../Configuration/AuthzSettings.cs | 27 ++ .../Configuration/ChecksumSettings.cs | 16 + .../Configuration/ExtSettings.cs | 22 ++ .../Configuration/HandlerSettings.cs | 25 ++ .../Configuration/LogSettings.cs | 30 ++ .../DscHandlerConfig.cs | 15 + src/TugDSC.Server.Abstractions/FileContent.cs | 21 + .../Filters/DscRegKeyAuthzFilter.cs | 354 +++++++++++++++++ .../Filters/DscRegKeyAuthzFilterAlt.cs | 216 ++++++++++ .../Filters/InspectAuthzFilter.cs | 49 +++ .../Filters/StrictInputFilter.cs | 98 +++++ .../Filters/VeryStrictInputFilter.cs | 92 +++++ .../IChecksumAlgorithm.cs | 20 + .../IChecksumAlgorithmProvider.cs | 116 ++++++ src/TugDSC.Server.Abstractions/IDscHandler.cs | 57 +++ .../IDscHandlerProvider.cs | 83 ++++ .../Mvc/ModelResult.cs | 148 +++++++ src/TugDSC.Server.Abstractions/README.md | 3 + .../TugDSC.Server.Abstractions.csproj | 33 ++ .../Util/ChecksumHelper.cs | 80 ++++ .../Util/DscHandlerHelper.cs | 78 ++++ src/TugDSC.Server.WebAppHost/AppLog.cs | 56 +++ .../Controllers/DscController.cs | 155 ++++++++ .../Controllers/DscReportingController.cs | 108 +++++ src/TugDSC.Server.WebAppHost/Program.cs | 341 ++++++++++++++++ .../Providers/BasicDscHandler.cs | 371 ++++++++++++++++++ .../Providers/BasicDscHandlerProvider.cs | 99 +++++ .../Providers/Sha256ChecksumAlgorithm.cs | 82 ++++ .../Sha256ChecksumAlgorithmProvider.cs | 38 ++ src/TugDSC.Server.WebAppHost/README.md | 26 ++ src/TugDSC.Server.WebAppHost/Startup.cs | 246 ++++++++++++ src/TugDSC.Server.WebAppHost/StartupLogger.cs | 43 ++ .../TugDSC.Server.WebAppHost.csproj | 59 +++ .../appsettings.Development.json | 10 + src/TugDSC.Server.WebAppHost/appsettings.json | 54 +++ src/shared/SharedAssemblyInfo.cs | 6 +- src/shared/SharedAssemblyVersionInfo.cs | 2 +- 83 files changed, 5708 insertions(+), 34 deletions(-) create mode 100644 TugDSC.sharedasm.props create mode 100644 TugDSC.sln create mode 100644 src/TugDSC.Abstractions/Configuration/ReadOnlyConfiguration.cs create mode 100644 src/TugDSC.Abstractions/Configuration/ReadOnlyConfigurationSection.cs create mode 100644 src/TugDSC.Abstractions/Ext/IProvider.cs create mode 100644 src/TugDSC.Abstractions/Ext/IProviderManager.cs create mode 100644 src/TugDSC.Abstractions/Ext/IProviderProduct.cs create mode 100644 src/TugDSC.Abstractions/Ext/ProviderInfo.cs create mode 100644 src/TugDSC.Abstractions/Ext/ProviderParameterInfo.cs create mode 100644 src/TugDSC.Abstractions/Ext/Util/MefExtensions.cs create mode 100644 src/TugDSC.Abstractions/Ext/Util/ProviderExtensions.cs create mode 100644 src/TugDSC.Abstractions/Ext/Util/ProviderManagerBase.cs create mode 100644 src/TugDSC.Abstractions/Ext/Util/ServiceProviderExportDescriptorProvider.cs create mode 100644 src/TugDSC.Abstractions/Messages/DscRequest.cs create mode 100644 src/TugDSC.Abstractions/Messages/DscResponse.cs create mode 100644 src/TugDSC.Abstractions/Messages/GetConfiguration.cs create mode 100644 src/TugDSC.Abstractions/Messages/GetDscAction.cs create mode 100644 src/TugDSC.Abstractions/Messages/GetModule.cs create mode 100644 src/TugDSC.Abstractions/Messages/GetReports.cs create mode 100644 src/TugDSC.Abstractions/Messages/ModelBinding/ModelBinding.cs create mode 100644 src/TugDSC.Abstractions/Messages/RegisterDscAgent.cs create mode 100644 src/TugDSC.Abstractions/Messages/SendReport.cs create mode 100644 src/TugDSC.Abstractions/Model/ActionDetailsItem.cs create mode 100644 src/TugDSC.Abstractions/Model/AgentInformation.cs create mode 100644 src/TugDSC.Abstractions/Model/CertificateInformation.cs create mode 100644 src/TugDSC.Abstractions/Model/ClientStatusItem.cs create mode 100644 src/TugDSC.Abstractions/Model/GetDscActionRequestBody.cs create mode 100644 src/TugDSC.Abstractions/Model/GetDscActionResponseBody.cs create mode 100644 src/TugDSC.Abstractions/Model/GetReportsResponseBody.cs create mode 100644 src/TugDSC.Abstractions/Model/ModelCommon.cs create mode 100644 src/TugDSC.Abstractions/Model/RegisterDscAgentRequestBody.cs create mode 100644 src/TugDSC.Abstractions/Model/RegistrationInformation.cs create mode 100644 src/TugDSC.Abstractions/Model/SendReportBody.cs create mode 100644 src/TugDSC.Abstractions/README.md create mode 100644 src/TugDSC.Abstractions/TugDSC.Abstractions.csproj create mode 100644 src/TugDSC.Abstractions/Util/ExceptionExtensions.cs create mode 100644 src/TugDSC.Abstractions/Util/ExtDataBase.cs create mode 100644 src/TugDSC.Abstractions/Util/ExtDataExtensions.cs create mode 100644 src/TugDSC.Abstractions/Util/ExtDataIndexerBase.cs create mode 100644 src/TugDSC.Abstractions/Util/IExtData.cs create mode 100644 src/TugDSC.Server.Abstractions/ActionStatus.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/AppSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/AuthzSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/ChecksumSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/ExtSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/HandlerSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/Configuration/LogSettings.cs create mode 100644 src/TugDSC.Server.Abstractions/DscHandlerConfig.cs create mode 100644 src/TugDSC.Server.Abstractions/FileContent.cs create mode 100644 src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilter.cs create mode 100644 src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilterAlt.cs create mode 100644 src/TugDSC.Server.Abstractions/Filters/InspectAuthzFilter.cs create mode 100644 src/TugDSC.Server.Abstractions/Filters/StrictInputFilter.cs create mode 100644 src/TugDSC.Server.Abstractions/Filters/VeryStrictInputFilter.cs create mode 100644 src/TugDSC.Server.Abstractions/IChecksumAlgorithm.cs create mode 100644 src/TugDSC.Server.Abstractions/IChecksumAlgorithmProvider.cs create mode 100644 src/TugDSC.Server.Abstractions/IDscHandler.cs create mode 100644 src/TugDSC.Server.Abstractions/IDscHandlerProvider.cs create mode 100644 src/TugDSC.Server.Abstractions/Mvc/ModelResult.cs create mode 100644 src/TugDSC.Server.Abstractions/README.md create mode 100644 src/TugDSC.Server.Abstractions/TugDSC.Server.Abstractions.csproj create mode 100644 src/TugDSC.Server.Abstractions/Util/ChecksumHelper.cs create mode 100644 src/TugDSC.Server.Abstractions/Util/DscHandlerHelper.cs create mode 100644 src/TugDSC.Server.WebAppHost/AppLog.cs create mode 100644 src/TugDSC.Server.WebAppHost/Controllers/DscController.cs create mode 100644 src/TugDSC.Server.WebAppHost/Controllers/DscReportingController.cs create mode 100644 src/TugDSC.Server.WebAppHost/Program.cs create mode 100644 src/TugDSC.Server.WebAppHost/Providers/BasicDscHandler.cs create mode 100644 src/TugDSC.Server.WebAppHost/Providers/BasicDscHandlerProvider.cs create mode 100644 src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithm.cs create mode 100644 src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithmProvider.cs create mode 100644 src/TugDSC.Server.WebAppHost/README.md create mode 100644 src/TugDSC.Server.WebAppHost/Startup.cs create mode 100644 src/TugDSC.Server.WebAppHost/StartupLogger.cs create mode 100644 src/TugDSC.Server.WebAppHost/TugDSC.Server.WebAppHost.csproj create mode 100644 src/TugDSC.Server.WebAppHost/appsettings.Development.json create mode 100644 src/TugDSC.Server.WebAppHost/appsettings.json diff --git a/.vscode/launch.json b/.vscode/launch.json index d9b34fc..4bcc2f7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,17 @@ { "version": "0.2.0", "configurations": [ + { + "name": "TugDSC.Server.WebAppHost - .NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + //"preLaunchTask": "build", + "program": "${workspaceRoot}/src/TugDSC.Server.WebAppHost/bin/Debug/netcoreapp2.0/TugDSC.Server.WebAppHost.dll", + "args": ["/h:host_foo:some:nested:value=host_bar", "/c:app_foo=app_bar"], + "cwd": "${workspaceRoot}/src/TugDSC.Server.WebAppHost", + "stopAtEntry": false, + "console": "internalConsole" + }, { "name": "Tug.Client - .NET Core Launch (console)", "type": "coreclr", @@ -54,7 +65,7 @@ "name": ".NET Core Attach", "type": "coreclr", "request": "attach", - "processId": "${command.pickProcess}" + "processId": "${command:pickProcess}" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 20af2f6..ff2ac5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ // Place your settings in this file to overwrite default and user settings. { + "cSpell.words": [ + "asms", + "assms", + "authz", + "csum", + "dotnetfw", + "ext", + "rtasm" + ] } \ No newline at end of file diff --git a/TugDSC.common.props b/TugDSC.common.props index 2743942..2cdc3eb 100644 --- a/TugDSC.common.props +++ b/TugDSC.common.props @@ -1,11 +1,21 @@ - - $(MSBuildThisFileDirectory)/src/shared - netstandard2.0 - netcoreapp2.0 - net461 - + + + + $(MSBuildThisFileDirectory)/src/shared + netstandard2.0 + netcoreapp2.0 + + + + + $(DefineConstants);DOTNET_CORE @@ -20,28 +30,16 @@ - - en-US - github.com/PowerShellOrg/tug/graphs/contributors - - CS0169;CS0649 - - - - false - false - false - false - false - false - false - false - false - + + en-US + github.com/PowerShellOrg/tug/graphs/contributors + + CS0169;CS0649 + diff --git a/TugDSC.sharedasm.props b/TugDSC.sharedasm.props new file mode 100644 index 0000000..8eacc42 --- /dev/null +++ b/TugDSC.sharedasm.props @@ -0,0 +1,54 @@ + + + + + + + + + + 0 + 0.7.0.$(BuildNum) + $(CommonVersion) + $(CommonVersion) + $(CommonVersion)-ea + + + + + + + + + + + + + true + true + + + + false + false + false + false + + + + true + true + true + true + + + diff --git a/TugDSC.sln b/TugDSC.sln new file mode 100644 index 0000000..d3bb479 --- /dev/null +++ b/TugDSC.sln @@ -0,0 +1,69 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BA29CE93-33EE-419B-A855-DA934573FA83}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TugDSC.Abstractions", "src\TugDSC.Abstractions\TugDSC.Abstractions.csproj", "{97DECA03-C0C3-4F41-A0FE-F51CA949D501}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TugDSC.Server.Abstractions", "src\TugDSC.Server.Abstractions\TugDSC.Server.Abstractions.csproj", "{8BAD111C-0371-4F65-BAD7-1B88FADC84E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TugDSC.Server.WebAppHost", "src\TugDSC.Server.WebAppHost\TugDSC.Server.WebAppHost.csproj", "{77F0AAAE-3518-46A7-A0E2-E09A36630300}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|x64.ActiveCfg = Debug|x64 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|x64.Build.0 = Debug|x64 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|x86.ActiveCfg = Debug|x86 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Debug|x86.Build.0 = Debug|x86 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|Any CPU.Build.0 = Release|Any CPU + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|x64.ActiveCfg = Release|x64 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|x64.Build.0 = Release|x64 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|x86.ActiveCfg = Release|x86 + {97DECA03-C0C3-4F41-A0FE-F51CA949D501}.Release|x86.Build.0 = Release|x86 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|x64.ActiveCfg = Debug|x64 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|x64.Build.0 = Debug|x64 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|x86.ActiveCfg = Debug|x86 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Debug|x86.Build.0 = Debug|x86 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|Any CPU.Build.0 = Release|Any CPU + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|x64.ActiveCfg = Release|x64 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|x64.Build.0 = Release|x64 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|x86.ActiveCfg = Release|x86 + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0}.Release|x86.Build.0 = Release|x86 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|x64.ActiveCfg = Debug|x64 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|x64.Build.0 = Debug|x64 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|x86.ActiveCfg = Debug|x86 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Debug|x86.Build.0 = Debug|x86 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|Any CPU.Build.0 = Release|Any CPU + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|x64.ActiveCfg = Release|x64 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|x64.Build.0 = Release|x64 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|x86.ActiveCfg = Release|x86 + {77F0AAAE-3518-46A7-A0E2-E09A36630300}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {97DECA03-C0C3-4F41-A0FE-F51CA949D501} = {BA29CE93-33EE-419B-A855-DA934573FA83} + {8BAD111C-0371-4F65-BAD7-1B88FADC84E0} = {BA29CE93-33EE-419B-A855-DA934573FA83} + {77F0AAAE-3518-46A7-A0E2-E09A36630300} = {BA29CE93-33EE-419B-A855-DA934573FA83} + EndGlobalSection +EndGlobal diff --git a/appveyor.yml b/appveyor.yml index d606ee5..53283ae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,17 +2,24 @@ version: 0.6.0.{build} branches: only: - ci + - ci2 - migrate-sdk104 skip_tags: true init: - ps: >- ## Need to delete .NET Core 1.1 SDK components to roll back to 1.0 + ## because of some incompatible behavior (such as during dotnet test) + del -Recurse -Force "C:\Program Files\dotnet\sdk\1.0.0-preview2-1-003177" + del -Recurse -Force "C:\Program Files\dotnet\shared\Microsoft.NETCore.App\1.1.0" + del -Recurse -Force 'C:\Program Files\dotnet\swidtag\Microsoft .NET Core 1.1.0 - SDK 1.0.0 Preview 2.1-003177 (x64).swidtag' + ## Check if we should Enable RDP access + if ([int]$((Resolve-DnsName blockrdp.tug-ci.tug.bkkr.us -Type TXT).Text)) { ## As per: https://www.appveyor.com/docs/how-to/rdp-to-build-worker/ iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) @@ -28,79 +35,124 @@ hosts: install: - ps: >- ## We want to setup a local PowerShell DSC Pull Server to support + ## some of the unit tests that validate protocol compatibility + Install-WindowsFeature PowerShell,PowerShell-ISE,DSC-Service + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module xWebAdministration -Force + Install-Module xPSDesiredStateConfiguration -Force + Start-Service W3SVC ## This is normally not running on AV + ## Run it once to create new cert + .\tools\ci\DSC\DscPullServer.dsc.ps1 -RegistrationKey c3ea5066-ce5a-4d12-a42a-850be287b2d8 + ## Run it a second time to install cert + .\tools\ci\DSC\DscPullServer.dsc.ps1 -RegistrationKey c3ea5066-ce5a-4d12-a42a-850be287b2d8 + ## Publish a Test MOF used by unit tests + .\tools\ci\DSC\TestConfig1.dsc.ps1 + ## Copy over exactly as found + .\tools\ci\DSC\StaticTestConfig-copy.ps1 + .\tools\ci\DSC\xPSDesiredStateConfiguration-copy.ps1 build_script: - ps: >- ## With a little help from: + ## https://github.com/StevenLiekens/dotnet-core-appveyor/blob/master/appveyor.yml + #$Env:LABEL = "CI" + $Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0") + appveyor-retry dotnet restore -v Minimal + dotnet build "src\Tug.Base" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "src\Tug.Client" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "src\Tug.Server.Base" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "src\Tug.Server" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "src\Tug.Server.Providers.Ps5DscHandler" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "test\Tug.Ext-tests" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "test\Tug.Ext-tests-aux" #-c %CONFIGURATION% --no-dependencies --version-suffix %LABEL% + dotnet build "test\client\Tug.Client-tests" + dotnet build "test\server\Tug.Server-itests" test_script: - ps: >- ## We're only testing .NET Framework for now because there are some + ## inconsistencies on AV for .NET Core that are failing dynamic loading + dotnet test "test\Tug.Ext-tests\Tug.Ext-tests.csproj" -f net452 + dotnet test "test\client\Tug.Client-tests\Tug.Client-tests.csproj" + ## Only testing .NET Framework for now -- this test assembly is based on + ## 4.6.2 because it's the minimum needed to support ASP.NET Core TestHost + dotnet test "test\server\Tug.Server-itests\Tug.Server-itests.csproj" -f net462 deploy: off on_success: - ps: >- ## If builds and tests are successful, package up + ## some pre-configured bundles and publish them + $bundlePath = '.\src\bundles\Tug.Server-ps5' + $modulePath = $bundlePath + '\bin\posh-modules\Tug.Server-ps5' + dotnet build $bundlePath + & "$bundlePath\build-posh-module.cmd" + ## Push bundle to a pre-release Nuget Repo + $nugetUrl = $env:NUGET_TUGPRE_URL + $nugetKey = $env:NUGET_TUGPRE_API_KEY + Update-ModuleManifest -Path $modulePath\Tug.Server-ps5.psd1 -ModuleVersion $env:APPVEYOR_BUILD_VERSION + Register-PSRepository -Name tug-pre -PackageManagementProvider nuget -SourceLocation $nugetUrl -PublishLocation $nugetUrl + Publish-Module -Path $modulePath -Repository tug-pre -NuGetApiKey $nugetKey on_finish: - ps: >- ## Check if we should Enable RDP access + if ([int]$((Resolve-DnsName blockrdp.tug-ci.tug.bkkr.us -Type TXT).Text)) { ## As per: https://www.appveyor.com/docs/how-to/rdp-to-build-worker/ $blockRdp = $true diff --git a/src/TugDSC.Abstractions/Configuration/ReadOnlyConfiguration.cs b/src/TugDSC.Abstractions/Configuration/ReadOnlyConfiguration.cs new file mode 100644 index 0000000..cb39145 --- /dev/null +++ b/src/TugDSC.Abstractions/Configuration/ReadOnlyConfiguration.cs @@ -0,0 +1,85 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace TugDSC.Configuration +{ + /// + /// A configuration implementation + // that wraps another instance and prevents modifications to the wrapped instance. + /// + /// + /// When wrapping another instance, you can optionally indicate whether write + /// attempts generate exceptions or are silently ignored. + ///

+ /// Upon first access, nested configuration elements are themselves wrapped in read-only + /// implementations before being returned. + ///

+ ///
+ public class ReadOnlyConfiguration : IConfiguration + { + private IConfiguration _inner; + private bool _throwOnWrite; + + private Dictionary _children; + + /// true by default which indicates + /// exceptions will be thrown for any write attempts + public ReadOnlyConfiguration(IConfiguration inner, bool throwOnWrite = true) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + _inner = inner; + _throwOnWrite = throwOnWrite; + } + + public string this[string key] + { + get { return _inner[key]; } + set + { + if (_throwOnWrite) + { + throw new InvalidOperationException( + /*SR*/"Attempt to write a read-only configuration"); + } + } + } + + public IConfigurationSection GetSection(string key) + { + var cs = _inner.GetSection(key); + return cs == null ? null : GetReadOnlySection(key); + } + + public IEnumerable GetChildren() + { + foreach (var cs in _inner.GetChildren()) + { + yield return GetReadOnlySection(cs.Key); + } + } + + public IChangeToken GetReloadToken() + { + return _inner.GetReloadToken(); + } + + private ReadOnlyConfigurationSection GetReadOnlySection(string key) + { + if (_children == null) + _children = new Dictionary(); + + if (!_children.ContainsKey(key)) + _children.Add(key, new ReadOnlyConfigurationSection(_inner.GetSection(key))); + + return _children[key]; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Configuration/ReadOnlyConfigurationSection.cs b/src/TugDSC.Abstractions/Configuration/ReadOnlyConfigurationSection.cs new file mode 100644 index 0000000..d38e262 --- /dev/null +++ b/src/TugDSC.Abstractions/Configuration/ReadOnlyConfigurationSection.cs @@ -0,0 +1,106 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace TugDSC.Configuration +{ + /// + /// A configuration section implementation + // that wraps another instance and prevents modifications to the wrapped instance. + /// + /// + /// When wrapping another instance, you can optionally indicate whether write + /// attempts generate exceptions or are silently ignored. + ///

+ /// Upon first access, nested configuration elements are themselves wrapped in read-only + /// implementations before being returned. + ///

+ ///
+ public class ReadOnlyConfigurationSection : IConfigurationSection + { + private IConfigurationSection _inner; + private bool _throwOnWrite; + + private Dictionary _children; + + /// true by default which indicates + /// exceptions will be thrown for any write attempts + public ReadOnlyConfigurationSection(IConfigurationSection inner, bool throwOnWrite = true) + { + if (inner == null) + throw new ArgumentNullException(nameof(inner)); + _inner = inner; + _throwOnWrite = throwOnWrite; + } + + public string this[string key] + { + get { return _inner[key]; } + set + { + if (_throwOnWrite) + { + throw new InvalidOperationException( + /*SR*/"Attempt to write a read-only configuration"); + } + } + } + + public string Key + { + get { return _inner.Key; } + } + + public string Path + { + get { return _inner.Path; } + } + + public string Value + { + get { return _inner.Value; } + set + { + if (_throwOnWrite) + { + throw new InvalidOperationException( + /*SR*/"Attempt to write a read-only configuration"); + } + } + } + + public IConfigurationSection GetSection(string key) + { + var cs = _inner.GetSection(key); + return cs == null ? null : GetReadOnlySection(cs.Key); + } + + public IEnumerable GetChildren() + { + foreach (var cs in _inner.GetChildren()) + yield return GetReadOnlySection(cs.Key); + } + + public IChangeToken GetReloadToken() + { + return _inner.GetReloadToken(); + } + + private ReadOnlyConfigurationSection GetReadOnlySection(string key) + { + if (_children == null) + _children = new Dictionary(); + + if (!_children.ContainsKey(key)) + _children.Add(key, new ReadOnlyConfigurationSection(_inner.GetSection(key))); + + return _children[key]; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/IProvider.cs b/src/TugDSC.Abstractions/Ext/IProvider.cs new file mode 100644 index 0000000..8cb98b8 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/IProvider.cs @@ -0,0 +1,21 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; + +namespace TugDSC.Ext +{ + public interface IProvider + where TProd : IProviderProduct + { + ProviderInfo Describe(); + + IEnumerable DescribeParameters(); + + void SetParameters(IDictionary productParams); + + TProd Produce(); + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/IProviderManager.cs b/src/TugDSC.Abstractions/Ext/IProviderManager.cs new file mode 100644 index 0000000..07012c5 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/IProviderManager.cs @@ -0,0 +1,19 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; + +namespace TugDSC.Ext +{ + public interface IProviderManager + where TProv : IProvider + where TProd : IProviderProduct + { + IEnumerable FoundProvidersNames + { get; } + + TProv GetProvider(string name); + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/IProviderProduct.cs b/src/TugDSC.Abstractions/Ext/IProviderProduct.cs new file mode 100644 index 0000000..f580dd2 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/IProviderProduct.cs @@ -0,0 +1,19 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; + +namespace TugDSC.Ext +{ + // Alternative Names: + // public interface IProviderYield + // public interface IProviderResult + // public interface IProviderOutput + public interface IProviderProduct : IDisposable + { + bool IsDisposed + { get; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/ProviderInfo.cs b/src/TugDSC.Abstractions/Ext/ProviderInfo.cs new file mode 100644 index 0000000..f6e24a0 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/ProviderInfo.cs @@ -0,0 +1,27 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Ext +{ + public class ProviderInfo + { + public ProviderInfo(string name, + string label = null, string description = null) + { + Name = name; + Label = label; + Description = description; + } + + public string Name + { get; private set; } + + public string Label + { get; private set; } + + public string Description + { get; private set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/ProviderParameterInfo.cs b/src/TugDSC.Abstractions/Ext/ProviderParameterInfo.cs new file mode 100644 index 0000000..b3dd45a --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/ProviderParameterInfo.cs @@ -0,0 +1,34 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Ext +{ + public class ProviderParameterInfo + { + public ProviderParameterInfo(string name, + bool isRequired = false, + string label = null, + string description = null) + { + Name = name; + + IsRequired = false; + Label = label; + Description = description; + } + + public string Name + { get; private set; } + + public bool IsRequired + { get; private set; } + + public string Label + { get; private set; } + + public string Description + { get; private set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/Util/MefExtensions.cs b/src/TugDSC.Abstractions/Ext/Util/MefExtensions.cs new file mode 100644 index 0000000..b417e5b --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/Util/MefExtensions.cs @@ -0,0 +1,103 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.Composition.Convention; +using System.Composition.Hosting; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace TugDSC.Ext.Util +{ + /// + /// Defines extension methods on components of the MEF 2 framework. + /// + public static class MefExtensions + { + public static readonly IEnumerable DEFAULT_PATTERNS = new string[] { "*.dll" }; + + /// + /// Adds one or more directory paths to be searched for assemblies + /// that are to be inspected for any matching assemblies when resolving + /// for components. + /// + /// the configuration to add paths to + /// one or more paths to include in the search + /// specifies to search either for top-level directories + /// only or to descend into child dirs too + /// one or more wildcard patterns to search for; + /// defaults to '*.dll' + /// + /// + /// Based on: + /// http://weblogs.asp.net/ricardoperes/using-mef-in-net-core + /// + public static ContainerConfiguration WithAssembliesInPaths( + this ContainerConfiguration configuration, + IEnumerable paths, + AttributedModelProvider conventions = null, + SearchOption searchOption = SearchOption.TopDirectoryOnly, + IEnumerable patterns = null) + { + if (patterns == null) + patterns = DEFAULT_PATTERNS; + + foreach (var p in paths) + { + foreach (var r in patterns) + { + var assemblies = Directory + .GetFiles(p, r, searchOption) + .Select(LoadFromAssembly) + .Where(x => x != null) + .ToList(); + + configuration.WithAssemblies(assemblies, conventions); + } + } + + return configuration; + } + + public static Assembly LoadFromAssembly(string path) + { + return Assembly.LoadFile(path); + } + + // Stolen from the guts of MEF ConventionBuilder code, + // implements the default type selection logic of + // ConventionBuilder.ForTypesDerivedFrom() + internal static bool IsDescendentOf(Type type, Type baseType) + { + if (type == baseType || type == typeof(object) || type == null) + return false; + + TypeInfo typeInfo1 = type.GetTypeInfo(); + TypeInfo typeInfo2 = baseType.GetTypeInfo(); + if (typeInfo1.IsGenericTypeDefinition) + return MefExtensions.IsGenericDescendentOf(typeInfo1, typeInfo2); + return typeInfo2.IsAssignableFrom(typeInfo1); + } + + // Stolen from the guts of MEF ConventionBuilder code, + // supports the default type selection logic of + // ConventionBuilder.ForTypesDerivedFrom() + internal static bool IsGenericDescendentOf(TypeInfo openType, TypeInfo baseType) + { + if (openType.BaseType == null) + return false; + if (openType.BaseType == baseType.AsType()) + return true; + foreach (Type type in openType.ImplementedInterfaces) + { + if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == baseType.AsType()) + return true; + } + return MefExtensions.IsGenericDescendentOf(IntrospectionExtensions.GetTypeInfo(openType.BaseType), baseType); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/Util/ProviderExtensions.cs b/src/TugDSC.Abstractions/Ext/Util/ProviderExtensions.cs new file mode 100644 index 0000000..410e878 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/Util/ProviderExtensions.cs @@ -0,0 +1,222 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace TugDSC.Ext.Util +{ + public static class ProviderExtensions + { + /// + /// This extension function provides a utiliity to support application of parameter + /// values to an instance of a Provider Product. + /// + /// the target product instance to apply settings to + /// the collection of parameter details to search for + /// a dictionary of values to be applied as parameters, + /// keyed by the unique name given in the parameter details + /// if true, will throw an error if there are + /// parameter values specified for unknown parameter names + /// if true, when matching parameter + /// values to the corresponding parameter, if the property type of the + /// parameter on an instance is a collection of elements compatible with + /// the supplied value type, the value will be assigned after being wrapped + /// in a compatible collection + /// if true, when the value type is not + /// directly assignable to the property type of a parameter, a type conversion + /// will be attempted + /// if true, will throw an exception if any of + /// the required parameters are missing from the given values + /// an optional function to invoke upon any found parameters + /// which would return a tuple indicating if the parameter should be applied + /// and if so, an opportunity to transform the supplied value + /// + /// Thrown if there are any failures to apply the supplied parameter values + /// to the target product instance in the context of the supplied paramter details. + /// + /// + /// Returns the same argument product instance that was provided as an argument + /// in support of a fluid interface. + /// + /// + /// This routine would typically be used by a Provider + /// Implementation to apply settings to product instances that it produces. + /// + /// If there are any failures to apply any parameter values, an ArgumentException + /// is thrown and it will contain Data populated + /// with the parameter names that caused the failure(s). In the associated Data collection + /// the following keys will be populated with an enumeration of strings of parameter names + /// or exceptions parameter that were associated with the corresponding error category: + /// + /// + /// missingParams + /// parameter names that were missing from the supplied parameter values; + /// these will only be checked if requiredEnforced is true + /// + /// + /// unknownParams + /// parameter names that were supplied in the parameter values but were not + /// found in the supplied parameter info details; these will only be checked if + /// strictNames is true + /// + /// + /// missingProps + /// parameter names that could not be mapped to properties on the target + /// product type; parameter names are case-sensitive and likewise matched against + /// property names + /// + /// + /// applyFailed + /// parameters that could not be applied and generated exceptions; unlike + /// the other Data entries, this collection holds instances of ArgumentExceptions + /// that provides details of the parameter name and the exception encountered when + /// the attempt was made to assign to the mapped property + /// + /// + /// + /// + public static Prod ApplyParameters(this Prod prod, + IEnumerable prodParams, + IDictionary paramValues, + bool strictNames = false, + bool requiredEnforced = true, + bool widenToCollections = true, + bool tryConversion = true, + Func> filter = null) + where Prod : IProviderProduct + { + var prodTypeInfo = typeof(Prod).GetTypeInfo(); + + var missingParams = new List(); + var unknownParams = new List(); + var unknownProps = new List(); + var applyFailed = new List(); + + int foundParamValues = 0; + foreach (var p in prodParams) + { + if (!paramValues.ContainsKey(p.Name)) + { + if (requiredEnforced && p.IsRequired) + missingParams.Add(p.Name); + continue; + } + + // We keep a count so we know if we may + // have had any unexpected params + ++foundParamValues; + + // Start with the value given + var value = paramValues[p.Name]; + + // See if a filter was supplied + if (filter != null) + { + var f = filter(p, value); + + // Skip the value if the filter indicated so + if (!f.Item1) + continue; + + // Get the possibly transformed value + value = f.Item2; + } + + var prop = prodTypeInfo.GetProperty(p.Name, BindingFlags.Public | BindingFlags.Instance); + var propType = prop.PropertyType; + var valueType = value?.GetType(); + + if (prop == null) + { + unknownProps.Add(p.Name); + continue; + } + + if (valueType != null && !propType.IsAssignableFrom(valueType)) + { + // Check if we can wrap the value as a collection + if (widenToCollections) + { + // Test for compatible value array + if (propType.IsArray && propType.GetElementType().IsAssignableFrom(valueType)) + { + var arr = Array.CreateInstance(valueType, 1); + valueType = arr.GetType(); + arr.SetValue(value, 0); + value = arr; + } + // Test for compatible generic collection + else if (propType.IsAssignableFrom(typeof(ICollection<>) + .MakeGenericType(valueType))) + { + var list = Activator.CreateInstance(typeof(List<>) + .MakeGenericType(valueType)); + valueType = list.GetType(); + valueType.GetMethod("Add", BindingFlags.Public | BindingFlags.Instance) + .Invoke(list, new[] { value }); + value = list; + } + // Test for untyped collection + else if (propType.IsAssignableFrom(typeof(ICollection))) + { + var list = new ArrayList(1); + valueType = list.GetType(); + list.Add(value); + value = list; + } + } + + // Check if we should/can try to convert the value + if (!propType.IsAssignableFrom(valueType) && tryConversion) + { + var typeConv = TypeDescriptor.GetConverter(prop.PropertyType); + value = typeConv.ConvertFrom(value); + valueType = value?.GetType(); + } + } + + try + { + // Best effort to assign the value + prop.SetValue(prod, value); + } + catch (Exception ex) + { + applyFailed.Add(new ArgumentException(ex.Message, p.Name, ex)); + } + } + + if (strictNames && foundParamValues < paramValues.Count) + { + // Uh oh, there are some parameters we didn't know about + var paramNames = prodParams.Select(x => x.Name); + unknownParams.AddRange(paramValues.Keys.Where(x => !paramNames.Contains(x))); + } + + if (missingParams.Count > 0 || unknownParams.Count > 0 + || unknownProps.Count > 0 || applyFailed.Count > 0) + { + var ex = new ArgumentException("one or more parameters cannot be applied"); + if (missingParams.Count > 0) + ex.Data.Add(nameof(missingParams), missingParams); + if (unknownParams.Count > 0) + ex.Data.Add(nameof(unknownParams), unknownParams); + if (unknownProps.Count > 0) + ex.Data.Add(nameof(unknownProps), unknownProps); + if (applyFailed.Count > 0) + ex.Data.Add(nameof(applyFailed), applyFailed); + + throw ex; + } + + return prod; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/Util/ProviderManagerBase.cs b/src/TugDSC.Abstractions/Ext/Util/ProviderManagerBase.cs new file mode 100644 index 0000000..52061f8 --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/Util/ProviderManagerBase.cs @@ -0,0 +1,298 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.Composition.Convention; +using System.Composition.Hosting; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Ext.Util +{ + /// + /// Defines a base implementation of + /// that supports dynamic extension provider discovery and loading from + /// built-in assemblies and file paths using the Managed Extensibility Framework + /// (MEF) version 2, or otherwise known as the light edition of MEF. + /// + public abstract class ProviderManagerBase : IProviderManager + where TProv : IProvider + where TProd : IProviderProduct + { + private ILogger _logger; + + private ServiceProviderExportDescriptorProvider _adapter; + + private List _BuiltInAssemblies = new List(); + + private List _SearchAssemblies = new List(); + + private List _SearchPaths = new List(); + + private TProv[] _foundProviders = null; + + /// + /// Constructs a base Provider Manager with default configuration settings. + /// + /// logger to be used internally + /// an optional adapter to allow MEF dependencies + /// to be resolved by external DI sources + /// + /// The default configuration of the base Manager adds the assemblies + /// containing the generic typic parameters (TProv, TProd) to be added + /// as built-in assemblies, and to include all other loaded and + /// active assemblies as searchable assemblies (and no search paths). + /// + /// Additionally if an adapter is provided it will be added to the internal + /// MEF resolution process as a last step in resolving dependencies. + /// + /// + public ProviderManagerBase(ILogger managerLogger, + ServiceProviderExportDescriptorProvider adapter = null) + { + _logger = managerLogger; + _adapter = adapter; + + Init(); + } + + protected virtual void Init() + { + // By default we include the assemblies containing the + // principles a part of the built-ins and every other + // assembly in context a part of the search scope + _logger.LogInformation("Adding BUILTINS"); + AddBuiltIns( + typeof(TProv).GetTypeInfo().Assembly, + typeof(TProd).GetTypeInfo().Assembly); + + var asms = AppDomain.CurrentDomain.GetAssemblies(); + _logger.LogInformation("Adding [{asmCount}] resolved Search Assemblies", asms.Length); + AddSearchAssemblies(asms); + } + + /// + /// Lists the built-in assemblies that will be + /// searched first for matching providers. + /// + public IEnumerable BuiltInAssemblies + { + get { return _BuiltInAssemblies; } + } + + /// + /// Lists the built-in assemblies that will be + /// searched first for matching providers. + /// + public IEnumerable SearchAssemblies + { + get { return _SearchAssemblies; } + } + + /// + /// Lists the built-in assemblies that will be + /// searched first for matching providers. + /// + public IEnumerable SearchPaths + { + get { return _SearchPaths; } + } + + /// + /// Returns all the matching provider implementations that + /// have previously been discovered. If necessary, invokes + /// the resolution process to find matching providers. + /// + public IEnumerable FoundProvidersNames + { + get + { + if (_foundProviders == null) + { + _logger.LogInformation("providers have not yet been resolved -- resolving"); + this.FindProviders(); + } + return _foundProviders.Select(p => p.Describe().Name); + } + } + + public TProv GetProvider(string name) + { + return _foundProviders.FirstOrDefault(p => name.Equals(p.Describe().Name)); + } + + /// + /// Resets the list of built-in assemblies to be searched. + /// + protected ProviderManagerBase ClearBuiltIns() + { + _BuiltInAssemblies.Clear(); + return this; + } + + /// + /// Adds one or more built-in assemblies to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddBuiltIns(params Assembly[] assemblies) + { + return AddBuiltIns((IEnumerable)assemblies); + } + + /// + /// Adds one or more built-in assemblies to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddBuiltIns(IEnumerable assemblies) + { + foreach (var a in assemblies) + if (!_BuiltInAssemblies.Contains(a)) + _BuiltInAssemblies.Add(a); + + return this; + } + + /// + /// Resets the list of external assemblies to be searched. + /// + protected ProviderManagerBase ClearSearchAssemblies() + { + _SearchAssemblies.Clear(); + return this; + } + + /// + /// Adds one or more external assemblies to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddSearchAssemblies(params Assembly[] assemblies) + { + return AddSearchAssemblies((IEnumerable)assemblies); + } + + /// + /// Adds one or more external assemblies to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddSearchAssemblies(IEnumerable assemblies) + { + foreach (var a in assemblies) + if (!_BuiltInAssemblies.Contains(a) && !_SearchAssemblies.Contains(a)) + _SearchAssemblies.Add(a); + + return this; + } + + protected static AssemblyName GetAssemblyName(string asmName) + { + return AssemblyName.GetAssemblyName(asmName); + } + + protected static Assembly GetAssembly(AssemblyName asmName) + { + return Assembly.Load(asmName); + } + + /// + /// Resets the list of directory paths to be searched. + /// + protected ProviderManagerBase ClearSearchPaths() + { + _SearchPaths.Clear(); + return this; + } + + /// + /// Adds one or more directory paths to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddSearchPath(params string[] paths) + { + return AddSearchPath((IEnumerable)paths); + } + + /// + /// Adds one or more directory paths to be searched for matching provider + /// implementations. + /// + protected ProviderManagerBase AddSearchPath(IEnumerable paths) + { + foreach (var p in paths) + if (!_SearchPaths.Contains(p)) + _SearchPaths.Add(p); + + return this; + } + + /// + /// Evaluates whether a candidate type is a provider type. + /// + /// + /// The default implementation simply tests if the candidate type + /// is a qualified descendent of the TProv provider type. + /// + /// Subclasses may add, or replace with, other conditions such as testing + /// for the presence of a particular class-level custom attribute or + /// testing for the presence of other features of the class definition + /// such as a qualifying constructor signature. + /// + /// + protected bool MatchProviderType(Type candidate) + { + return MefExtensions.IsDescendentOf(candidate, typeof(TProv)); + } + + /// + /// Each time this is invoked, the search paths and patterns + /// (built-in assemblies and directory paths + patterns) are + /// searched to resolve matching components. The results are + /// cached and available in . + /// + protected virtual IEnumerable FindProviders() + { + try + { + + _logger.LogInformation("resolving providers"); + + var conventions = new ConventionBuilder(); + // conventions.ForTypesDerivedFrom() + conventions.ForTypesMatching(MatchProviderType) + .Export() + .Shared(); + + var existingPaths = _SearchPaths.Where(x => Directory.Exists(x)); + var configuration = new ContainerConfiguration() + .WithAssemblies(_BuiltInAssemblies, conventions) + .WithAssemblies(_SearchAssemblies, conventions) + .WithAssembliesInPaths(existingPaths.ToArray(), conventions); + + if (_adapter != null) + configuration.WithProvider(_adapter); + + using (var container = configuration.CreateContainer()) + { + _foundProviders = container.GetExports().ToArray(); + } + + return _foundProviders; + + } + catch (System.Reflection.ReflectionTypeLoadException ex) + { + Console.Error.WriteLine(">>>>>> Load Exceptions:"); + foreach (var lex in ex.LoaderExceptions) + { + Console.Error.WriteLine(">>>>>> >>>>" + lex); + } + throw ex; + } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Ext/Util/ServiceProviderExportDescriptorProvider.cs b/src/TugDSC.Abstractions/Ext/Util/ServiceProviderExportDescriptorProvider.cs new file mode 100644 index 0000000..4acf73a --- /dev/null +++ b/src/TugDSC.Abstractions/Ext/Util/ServiceProviderExportDescriptorProvider.cs @@ -0,0 +1,61 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.Composition.Hosting.Core; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Ext.Util +{ + /// + /// Custom MEF provider to resolve dependencies against the native DI framework. + /// + /// + /// This class acts as an adapter that bridges the dependency resolution mechanism + /// of MEF to be able to resolve against the services provided by the .NET Core + /// native dependency injection (DI) facility. + /// + public class ServiceProviderExportDescriptorProvider : ExportDescriptorProvider + { + public const string ORIGIN_NAME = "DI-ServiceProvider"; + + ILogger _logger; + private IServiceProvider _serviceProvider; + + public ServiceProviderExportDescriptorProvider(ILogger logger, IServiceProvider sp) + { + _logger = logger; + _serviceProvider = sp; + } + + public override IEnumerable GetExportDescriptors( + CompositionContract contract, DependencyAccessor descriptorAccessor) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug($"Getting Export Descriptors:" + + $" contractName=[{contract.ContractName}]" + + $" contractType=[{contract.ContractType.FullName}]"); + + var svc = _serviceProvider.GetService(contract.ContractType); + if (svc == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug($"No DI service found for" + + $" contractType=[{contract.ContractType.FullName}]"); + yield break; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug($"Resolved DI service for" + + $" contractType=[{contract.ContractType.FullName}]" + + $" service=[{svc}]"); + + CompositeActivator ca = (ctx, op) => svc; + yield return new ExportDescriptorPromise(contract, ORIGIN_NAME, true, + NoDependencies, deps => ExportDescriptor.Create(ca, NoMetadata)); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/DscRequest.cs b/src/TugDSC.Abstractions/Messages/DscRequest.cs new file mode 100644 index 0000000..d976298 --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/DscRequest.cs @@ -0,0 +1,110 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace TugDSC.Messages +{ + /// + /// Base class for DSC request input model defining common input + /// elements found in the request URL or from request headers for + /// any request revolves around a specific Agent. + /// + + public abstract class DscRequest + { + public const string PROTOCOL_VERSION_HEADER = "ProtocolVersion"; + + public const string X_MS_DATE_HEADER = "x-ms-date"; + + // e.g. x-ms-date: 2016-08-15T21:25:51.8654321Z + // Should be applied to a UTC time, based on the "Round-trip date/time pattern" + // format specifier ("O") as defined here: + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Roundtrip + public const string X_MS_DATE_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff'Z'"; + + [FromHeader(Name = "Content-type")] + public string ContentTypeHeader + { get; set; } + + [FromHeader(Name = "Accept")] + public string AcceptHeader + { get; set; } + + /// + /// https://msdn.microsoft.com/en-us/library/mt590240.aspx + /// + [FromHeader(Name = "Authorization")] + public string AuthorizationHeader + { get; set; } + + [FromHeader(Name = X_MS_DATE_HEADER)] + public string MsDateHeader + { get; set; } + + [FromHeader(Name = PROTOCOL_VERSION_HEADER)] + [Required] + public string ProtocolVersionHeader + { get; set; } + + /// + /// Returns the Agent ID passed in the request message, however it + /// may have been conveyed. If the request does not receive an Agent ID + /// then returns null. + /// + /// + /// Unlike most of the other fields the Agent ID may be conveyed as part + /// of the route or an HTTP request header field. This method allows a + /// caller to consistently retrieve the ID regardless. + /// + public virtual Guid? GetAgentId() => null; + + /// + /// For the purposes of input validation, this method is used to indicate + /// whether the body of the DSC Request message is well-defined and + /// should be strictly enforced. If not, then the form of the body may + /// take on different shapes under different conditions and cannot + /// by systematically validated. + /// + public virtual bool HasStrictBody() => true; + + /// + /// Returns the body content object captured in the request message. If + /// the request does not capture an object representing the body content + /// then returns null. + /// + public virtual object GetBody() => null; + } + + /// + /// Base class for DSC request input model defining common input + /// elements found in the request URL or from request headers for + /// any request revolves around a specific Agent. + /// + public abstract class DscAgentRequest : DscRequest + { + [FromRoute] + [Required] + public Guid? AgentId + { get; set; } + + [FromRoute] + public Guid ConfigurationId + { get; set; } + + /// + /// A version string represented as either an empty string representing + /// no value or a 2-part or 4-part numeric specification separated by + /// dots, e.g. 1.2 or 1.2.3.4. + [FromRoute] + [RegularExpression("(\\d+\\.\\d+(\\.\\d+\\.\\d+)?)?")] + public string ModuleVersion + { get; set; } + + public override Guid? GetAgentId() => AgentId; + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/DscResponse.cs b/src/TugDSC.Abstractions/Messages/DscResponse.cs new file mode 100644 index 0000000..874122c --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/DscResponse.cs @@ -0,0 +1,20 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using TugDSC.Messages.ModelBinding; + +namespace TugDSC.Messages +{ + public class DscResponse + { + public const string PROTOCOL_VERSION_HEADER = "ProtocolVersion"; + public const string PROTOCOL_VERSION_VALUE = "2.0"; + + + [ToHeader(Name = PROTOCOL_VERSION_HEADER)] + public string ProtocolVersionHeader + { get; set; } = PROTOCOL_VERSION_VALUE; + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/GetConfiguration.cs b/src/TugDSC.Abstractions/Messages/GetConfiguration.cs new file mode 100644 index 0000000..775612f --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/GetConfiguration.cs @@ -0,0 +1,47 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Messages.ModelBinding; + +namespace TugDSC.Messages +{ + public class GetConfigurationRequest : DscAgentRequest + { + public static readonly HttpMethod VERB = HttpMethod.Get; + + public const string ROUTE = "Nodes(AgentId='{AgentId}')/Configurations(ConfigurationName='{ConfigurationName}')/ConfigurationContent"; + public const string ROUTE_NAME = nameof(GetConfigurationRequest); + + [FromRoute] + public string ConfigurationName + { get; set; } + + /// + /// TODO: Resolve how this relates to the same parameter name in the URI. + /// https://msdn.microsoft.com/en-us/library/mt181633.aspx + /// + [FromHeader(Name = "ConfigurationName")] + public string ConfigurationNameHeader + { get; set; } + } + + public class GetConfigurationResponse : DscResponse + { + [ToHeader(Name = "Checksum")] + public string ChecksumHeader + { get; set; } + + [ToHeader(Name = "ChecksumAlgorithm")] + public string ChecksumAlgorithmHeader + { get; set; } + + [ToResult] + public Stream Configuration + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/GetDscAction.cs b/src/TugDSC.Abstractions/Messages/GetDscAction.cs new file mode 100644 index 0000000..520668b --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/GetDscAction.cs @@ -0,0 +1,35 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Messages.ModelBinding; +using TugDSC.Model; + +namespace TugDSC.Messages +{ + public class GetDscActionRequest : DscAgentRequest + { + public static readonly HttpMethod VERB = HttpMethod.Post; + + public const string ROUTE = "Nodes(AgentId='{AgentId}')/GetDscAction"; + public const string ROUTE_NAME = nameof(GetDscActionRequest); + + [FromBody] + [Required] + public GetDscActionRequestBody Body + { get; set; } + + public override object GetBody() => Body; + } + + public class GetDscActionResponse : DscResponse + { + [ToResult] + public GetDscActionResponseBody Body + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/GetModule.cs b/src/TugDSC.Abstractions/Messages/GetModule.cs new file mode 100644 index 0000000..91d7f63 --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/GetModule.cs @@ -0,0 +1,64 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Messages.ModelBinding; + +namespace TugDSC.Messages +{ + public class GetModuleRequest : DscRequest + { + public static readonly HttpMethod VERB = HttpMethod.Get; + + public const string ROUTE = "Modules(ModuleName='{ModuleName}',ModuleVersion='{ModuleVersion}')/ModuleContent"; + public const string ROUTE_NAME = nameof(GetModuleRequest); + + // Apparently this *has* to be a string when binding it from a + // header field otherwise, it just gets skipped over for some + // reason -- not sure if this is a bug in MVC model binding??? + [FromHeader(Name = "AgentId")] + [Required] + public string AgentId + { get; set; } + + [FromRoute] + [Required] + public string ModuleName + { get; set; } + + [FromRoute] + [Required] + public string ModuleVersion + { get; set; } + + public override Guid? GetAgentId() + { + Guid agentId; + if (Guid.TryParse(AgentId, out agentId)) + return agentId; + else + return null; + } + } + + public class GetModuleResponse : DscResponse + { + [ToHeaderAttribute(Name = "Checksum")] + public string ChecksumHeader + { get; set; } + + [ToHeader(Name = "ChecksumAlgorithm")] + public string ChecksumAlgorithmHeader + { get; set; } + + [ToResult] + public Stream Module + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/GetReports.cs b/src/TugDSC.Abstractions/Messages/GetReports.cs new file mode 100644 index 0000000..0c3849b --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/GetReports.cs @@ -0,0 +1,46 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Messages.ModelBinding; +using TugDSC.Model; + +namespace TugDSC.Messages +{ + public class GetReportsRequest : DscAgentRequest + { + public static readonly HttpMethod VERB = HttpMethod.Get; + + public const string ROUTE_SINGLE = "Nodes(AgentId='{AgentId}')/Reports(JobId='{JobId}')"; + public const string ROUTE_SINGLE_NAME = nameof(GetReportsRequest) + "Single"; + + public const string ROUTE_ALL = "Nodes(AgentId='{AgentId}')/Reports()"; + public const string ROUTE_ALL_NAME = nameof(GetReportsRequest) + "All"; + + public const string ROUTE_ALL_ALT = "Nodes(AgentId='{AgentId}')/Reports"; + public const string ROUTE_ALL_ALT_NAME = nameof(GetReportsRequest) + "AllAlt"; + + + [FromRoute] + public Guid? JobId + { get; set; } + } + + public class GetReportsSingleResponse : DscResponse + { + [ToResult] + public SendReportBody Body + { get; set; } + } + + public class GetReportsAllResponse : DscResponse + { + [ToResult] + public GetReportsAllResponseBody Body + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/ModelBinding/ModelBinding.cs b/src/TugDSC.Abstractions/Messages/ModelBinding/ModelBinding.cs new file mode 100644 index 0000000..e5fa8c9 --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/ModelBinding/ModelBinding.cs @@ -0,0 +1,56 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace TugDSC.Messages.ModelBinding +{ + /// + /// Specifies that a property of a model class should be bound to a response header, + /// when used in concert with the Model + /// extension method for MVC Controllers. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class ToHeaderAttribute : Attribute, IModelNameProvider + { + public ToHeaderAttribute() + { } + + public string Name { get; set; } + + public bool Replace + { get; set; } + } + + /// + /// Specifies that a property of a model class should be bound to a response content + /// body or action result, when used in concert with the Model extension method for MVC Controllers. + /// + /// + /// The return type of the property on which this attribute is decorated will be + /// inspected and will be used to determine the type of action result or content + /// type that is generated. The following result types are understood: + /// + /// IActionResult + /// byte[] + /// Stream + /// FileInfo + /// string + /// + /// If the property type is not one of the special types listed, then it will + /// treated as a model class that will be serialized and returned as JSON. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class ToResultAttribute : Attribute/*, IBindingSourceMetadata*/ + { + public ToResultAttribute() + { } + + public string ContentType + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/RegisterDscAgent.cs b/src/TugDSC.Abstractions/Messages/RegisterDscAgent.cs new file mode 100644 index 0000000..11082c0 --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/RegisterDscAgent.cs @@ -0,0 +1,42 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Messages.ModelBinding; +using TugDSC.Model; + +namespace TugDSC.Messages +{ + public class RegisterDscAgentRequest : DscAgentRequest + { + public static readonly HttpMethod VERB = HttpMethod.Put; + + public const string ROUTE = "Nodes(AgentId='{AgentId}')"; + public const string ROUTE_NAME = nameof(RegisterDscAgentRequest); + + [FromBody] + [Required] + public RegisterDscAgentRequestBody Body + { get; set; } + + public override object GetBody() => Body; + } + + public class RegisterDscAgentResponse : DscResponse + { + /// + /// We only need a single instance since there are + /// no mutable elements in the object graph. + /// + public static readonly RegisterDscAgentResponse INSTANCE = + new RegisterDscAgentResponse(); + + [ToResult] + public NoContentResult Body + { get; } = new NoContentResult(); + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Messages/SendReport.cs b/src/TugDSC.Abstractions/Messages/SendReport.cs new file mode 100644 index 0000000..e82c59e --- /dev/null +++ b/src/TugDSC.Abstractions/Messages/SendReport.cs @@ -0,0 +1,29 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using TugDSC.Model; + +namespace TugDSC.Messages +{ + public class SendReportRequest : DscAgentRequest + { + public static readonly HttpMethod VERB = HttpMethod.Post; + + public const string ROUTE = "Nodes(AgentId='{AgentId}')/SendReport"; + public const string ROUTE_NAME = nameof(SendReportRequest); + + [FromBody] + [Required(AllowEmptyStrings = true)] + public SendReportBody Body + { get; set; } + + public override bool HasStrictBody() => false; + + public override object GetBody() => Body; + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/ActionDetailsItem.cs b/src/TugDSC.Abstractions/Model/ActionDetailsItem.cs new file mode 100644 index 0000000..dd54fe3 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/ActionDetailsItem.cs @@ -0,0 +1,21 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class ActionDetailsItem : Util.ExtDataIndexerBase + { + [Required(AllowEmptyStrings = true)] + public string ConfigurationName + { get; set; } = string.Empty; + + [Required] + [EnumDataTypeAttribute(typeof(DscActionStatus))] + public DscActionStatus Status + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/AgentInformation.cs b/src/TugDSC.Abstractions/Model/AgentInformation.cs new file mode 100644 index 0000000..d7ebe68 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/AgentInformation.cs @@ -0,0 +1,28 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class AgentInformation : Util.ExtDataIndexerBase + { + // NOTE: DO NOT CHANGE THE ORDER OF THESE PROPERTIES!!! + // Apparently the order of these properties is important + // to successfully fulfill the RegKey authz requirements + + [Required] + public string LCMVersion + { get; set; } + + [Required] + public string NodeName + { get; set; } + + [Required(AllowEmptyStrings = true)] + public string IPAddress + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/CertificateInformation.cs b/src/TugDSC.Abstractions/Model/CertificateInformation.cs new file mode 100644 index 0000000..99d0c41 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/CertificateInformation.cs @@ -0,0 +1,65 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class CertificateInformation : Util.ExtDataIndexerBase + { + public CertificateInformation() + { } + + public CertificateInformation(CertificateInformation copyFrom) + { + this.FriendlyName = copyFrom.FriendlyName; + this.Issuer = copyFrom.Issuer; + this.NotAfter = copyFrom.NotAfter; + this.NotBefore = copyFrom.NotBefore; + this.Subject = copyFrom.Subject; + this.PublicKey = copyFrom.PublicKey; + this.Thumbprint = copyFrom.Thumbprint; + } + + // NOTE: DO NOT CHANGE THE ORDER OF THESE PROPERTIES!!! + // Apparently the order of these properties is important + // to successfully fulfill the RegKey authz requirements + + [Required] + public string FriendlyName + { get; set; } + + [Required] + public string Issuer + { get; set; } + + [Required] + public string NotAfter + { get; set; } + + [Required] + public string NotBefore + { get; set; } + + [Required] + public string Subject + { get; set; } + + [Required] + public string PublicKey + { get; set; } + + [Required] + public string Thumbprint + { get; set; } + + // This *MUST* be an int or RegisterDscAction will fail with a + // 401 Unauthorized error and eroneously report an invalid + // Registration Key -- as HOURS of debugging has proven! + [Required] + public int Version + { get; set; } + } +} diff --git a/src/TugDSC.Abstractions/Model/ClientStatusItem.cs b/src/TugDSC.Abstractions/Model/ClientStatusItem.cs new file mode 100644 index 0000000..ed4989b --- /dev/null +++ b/src/TugDSC.Abstractions/Model/ClientStatusItem.cs @@ -0,0 +1,40 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace TugDSC.Model +{ + public class ClientStatusItem : Util.ExtDataIndexerBase + { + // NOTE: DO NOT CHANGE THE ORDER OF THESE PROPERTIES!!! + // Apparently the order of these properties is important + // to successfully satisfy the strict input validation + + // Based on testing and observation, this property + // is completely omitted when it has no value + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string ConfigurationName + { get; set; } + + [Required(AllowEmptyStrings = true)] + public string Checksum + { get; set; } + + [Required] + [CustomValidation(typeof(ClientStatusItem), + nameof(ValidateChecksumAlgorithm))] + public string ChecksumAlgorithm + { get; set; } + + public static ValidationResult ValidateChecksumAlgorithm(string value) + { + return "SHA-256" == value + ? ValidationResult.Success + : new ValidationResult("unsupported or unknown checksum algorithm"); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/GetDscActionRequestBody.cs b/src/TugDSC.Abstractions/Model/GetDscActionRequestBody.cs new file mode 100644 index 0000000..8cf2fdf --- /dev/null +++ b/src/TugDSC.Abstractions/Model/GetDscActionRequestBody.cs @@ -0,0 +1,17 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class GetDscActionRequestBody : Util.ExtDataIndexerBase + { + [Required] + [MinLengthAttribute(1)] + public ClientStatusItem[] ClientStatus + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/GetDscActionResponseBody.cs b/src/TugDSC.Abstractions/Model/GetDscActionResponseBody.cs new file mode 100644 index 0000000..8ac84ac --- /dev/null +++ b/src/TugDSC.Abstractions/Model/GetDscActionResponseBody.cs @@ -0,0 +1,20 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class GetDscActionResponseBody : Util.ExtDataIndexerBase + { + [Required] + [EnumDataTypeAttribute(typeof(DscActionStatus))] + public DscActionStatus NodeStatus + { get; set; } + + public ActionDetailsItem[] Details + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/GetReportsResponseBody.cs b/src/TugDSC.Abstractions/Model/GetReportsResponseBody.cs new file mode 100644 index 0000000..e11d1e9 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/GetReportsResponseBody.cs @@ -0,0 +1,17 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace TugDSC.Model +{ + public class GetReportsAllResponseBody : Util.ExtDataIndexerBase + { + [JsonProperty(PropertyName = "value")] + public IEnumerable Value + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/ModelCommon.cs b/src/TugDSC.Abstractions/Model/ModelCommon.cs new file mode 100644 index 0000000..e0e3250 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/ModelCommon.cs @@ -0,0 +1,65 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Model +{ + public static class CommonValues + { + public static readonly string[] EMPTY_STRINGS = new string[0]; + } + + /// + /// Defines a collection of constants representing MIME content types + /// that are in use by the DSCPM protocol specification. + /// + public static class DscContentTypes + { + public const string OCTET_STREAM = "application/octet-stream"; + public const string JSON = "application/json"; + } + + /// + /// An enumeration that is commensurate with a boolean type. + /// + /// + /// In a few places in the DSCPM message specifications, an element + /// takes on semantics of a boolean value, but instead of using a + /// JSON boolean, the specification uses a string enumeration. + /// + public enum DscTrueFalse + { + False, + True, + } + + /// + /// An enumeration that defines the various operation modes + /// that are available for an LCM node. + /// + public enum DscRefreshMode + { + Push, + Pull, + } + + /// + /// An enumeration that defines the various statuses that indicate + /// a node's disposition for needing to update its configuration. + /// + public enum DscActionStatus + { + OK, + RETRY, + GetConfiguration, + UpdateMetaConfiguration, + } + + public static class CommonRegistrationMessageTypes + { + public const string ConfigurationRepository = "ConfigurationRepository"; + public const string ResourceRepository = "ResourceRepository"; + public const string ReportServer = "ReportServer"; + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/RegisterDscAgentRequestBody.cs b/src/TugDSC.Abstractions/Model/RegisterDscAgentRequestBody.cs new file mode 100644 index 0000000..adf6b2b --- /dev/null +++ b/src/TugDSC.Abstractions/Model/RegisterDscAgentRequestBody.cs @@ -0,0 +1,34 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace TugDSC.Model +{ + /// + /// https://msdn.microsoft.com/en-us/library/dn365245.aspx + /// + public class RegisterDscAgentRequestBody : Util.ExtDataIndexerBase + { + // NOTE: DO NOT CHANGE THE ORDER OF THESE PROPERTIES!!! + // Apparently the order of these properties is important + // to successfully fulfill the RegKey authz requirements + + [Required] + public AgentInformation AgentInformation + { get; set; } + + // Based on testing and observation, this property + // is completely omitted when it has no value + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string[] ConfigurationNames + { get; set; } + + [Required] + public RegistrationInformation RegistrationInformation + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/RegistrationInformation.cs b/src/TugDSC.Abstractions/Model/RegistrationInformation.cs new file mode 100644 index 0000000..b9b22fa --- /dev/null +++ b/src/TugDSC.Abstractions/Model/RegistrationInformation.cs @@ -0,0 +1,24 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Model +{ + public class RegistrationInformation : Util.ExtDataIndexerBase + { + // NOTE: DO NOT CHANGE THE ORDER OF THESE PROPERTIES!!! + // Apparently the order of these properties is important + // to successfully fulfill the RegKey authz requirements + + [Required] + public CertificateInformation CertificateInformation + { get; set; } + + [Required] + public string RegistrationMessageType + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Model/SendReportBody.cs b/src/TugDSC.Abstractions/Model/SendReportBody.cs new file mode 100644 index 0000000..7d43f30 --- /dev/null +++ b/src/TugDSC.Abstractions/Model/SendReportBody.cs @@ -0,0 +1,235 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace TugDSC.Model +{ +/* + { + "title": "SendReport request schema", + "type": "object", + "properties": { + "JobId": { + "type": [ + "string", + "null" + ], + "required": "true" + }, + "OperationType": { + "type": [ + "string", + "null" + ] + }, + "RefreshMode": { + "enum": [ + "Push", + "Pull" + ] + }, + "Status": { + "type": [ + "string", + "null" + ] + }, + "LCMVersion": { + "type": [ + "string", + "null" + ] + }, + "ReportFormatVersion": { + "type": [ + "string", + "null" + ] + }, + "ConfigurationVersion": { + "type": [ + "string", + "null" + ] + }, + "NodeName": { + "type": [ + "string", + "null" + ] + }, + "IpAddress": { + "type": [ + "string", + "null" + ] + }, + "StartTime": { + "type": [ + "string", + "null" + ] + }, + "EndTime": { + "type": [ + "string", + "null" + ] + }, + "RebootRequested": { + "enum": [ + "True", + "False" + ] + }, + "Errors": { + "type": [ + "string", + "null" + ] + }, + "StatusData": { + "type": [ + "string", + "null" + ] + }, + "AdditionalData": { + "type": "array", + "required": false, + "items": [ + { + "type": "object", + "required": true, + "properties": { + "Key": { + "type": "string", + "required": true + }, + "Value": { + "type": "string", + "required": true + } + } + } + ] + } + } + } + */ + + + /* + Response: + { + "odata.metadata":"http://10.50.1.5:8080/PSDSCPullServer.svc/$metadata#Edm.String", + "value":"SavedReport" + } + */ + + // NOTE: the naming convention of this class is a bit different + // because it is not strictly used by the DSC Request class + public class SendReportBody : Util.ExtDataIndexerBase + { + public const string REPORT_DATE_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz"; + + [Required] + public Guid JobId + { get; set; } + + // Appears to be one of these values (maybe turn this into an Enum?): + // * Initial + // * LocalConfigurationManager + // * Consistency + [Required] + public string OperationType + { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DscRefreshMode? RefreshMode + { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string Status + { get; set; } + + // IN TESTING AND OBSERVATION THIS FIELD DOES + // NOT ALWAYS GET SENT EVEN BY THE SAME NODE + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string NodeName + { get; set; } + + /// + /// This is assigned as a comma-separated list of IPv4 and IPv6 + /// addresses. + /// + // IN TESTING AND OBSERVATION THIS FIELD DOES + // NOT ALWAYS GET SENT EVEN BY THE SAME NODE + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string IpAddress + { get; set; } + + // IN TESTING AND OBSERVATION THIS FIELD DOES + // NOT ALWAYS GET SENT EVEN BY THE SAME NODE + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string LCMVersion + { get; set; } + + [Required] + public string ReportFormatVersion + { get; set; } + + // IN TESTING AND OBSERVATION THIS FIELD DOES + // NOT ALWAYS GET SENT EVEN BY THE SAME NODE + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string ConfigurationVersion + { get; set; } + + // START TIME IS ALWAYS PRESENT (END TIME IS NOT) + // Example: 2016-08-15T15:21:08.9530000-07:00 + [Required] + public string StartTime + { get; set; } + + // END TIME IS SOMETIMES OMITTED (START TIME IS NOT) + // Example: 2017-01-20T06:20:36.6950000-05:00 + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string EndTime + { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DscTrueFalse? RebootRequested + { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string[] Errors + { get; set; } = CommonValues.EMPTY_STRINGS; + + // THIS TYPICALLY HAS A SINGLE STRING ENTRY + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string[] StatusData + { get; set; } = CommonValues.EMPTY_STRINGS; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AdditionalDataItem[] AdditionalData + { get; set; } = AdditionalDataItem.EMPTY_ITEMS; + + public class AdditionalDataItem : Util.ExtDataIndexerBase + { + public static readonly AdditionalDataItem[] EMPTY_ITEMS = new AdditionalDataItem[0]; + + [Required] + public string Key + { get; set; } + + [Required] + public string Value + { get; set; } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/README.md b/src/TugDSC.Abstractions/README.md new file mode 100644 index 0000000..5f2909e --- /dev/null +++ b/src/TugDSC.Abstractions/README.md @@ -0,0 +1,3 @@ +# README - TugDSC Abstractions + +This library defines abstractions and components that are common to both clients and servers of the DSC platform. diff --git a/src/TugDSC.Abstractions/TugDSC.Abstractions.csproj b/src/TugDSC.Abstractions/TugDSC.Abstractions.csproj new file mode 100644 index 0000000..3eefd92 --- /dev/null +++ b/src/TugDSC.Abstractions/TugDSC.Abstractions.csproj @@ -0,0 +1,35 @@ + + + + + + + $(NETStandardMoniker);$(NETFrameworkMoniker) + $(NoWarn);$(CommonNoWarn) + TugDSC + + + + TugDSC Abstractions + Common Abstractions & Components + + + + + + + + + + + + + diff --git a/src/TugDSC.Abstractions/Util/ExceptionExtensions.cs b/src/TugDSC.Abstractions/Util/ExceptionExtensions.cs new file mode 100644 index 0000000..6151c46 --- /dev/null +++ b/src/TugDSC.Abstractions/Util/ExceptionExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace TugDSC.Util +{ + public static class ExceptionExtensions + { + /// This extension method provides a fluent method of appending various + /// meta data to an exception, useful when debugging and trying to + /// isolate more contextual information from a thrown exception. + public static T WithData(this T exception, object key, object value) + where T : Exception + { + exception.Data.Add(key, value); + return exception; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Util/ExtDataBase.cs b/src/TugDSC.Abstractions/Util/ExtDataBase.cs new file mode 100644 index 0000000..43974cc --- /dev/null +++ b/src/TugDSC.Abstractions/Util/ExtDataBase.cs @@ -0,0 +1,23 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace TugDSC.Util +{ + /// Base class implementation supporting extension data with JSON serialization. + public abstract class ExtDataBase : IExtData + { + [JsonExtensionData] + protected IDictionary _extData = new Dictionary(); + + IDictionary IExtData.GetExtData() + { + return _extData; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Util/ExtDataExtensions.cs b/src/TugDSC.Abstractions/Util/ExtDataExtensions.cs new file mode 100644 index 0000000..3fc0daf --- /dev/null +++ b/src/TugDSC.Abstractions/Util/ExtDataExtensions.cs @@ -0,0 +1,47 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace TugDSC.Util +{ + /// Various extension methods that make working with extension data + /// implementations easier and more fluid. + public static class ExtDataExtensions + { + public static int GetExtDataCount(this IExtData extData) + { + return extData.GetExtData().Count; + } + + public static IEnumerable GetExtDataKeys(this IExtData extData) + { + return extData.GetExtData().Keys; + } + + public static bool ContainsExtData(this IExtData extData, string key) + { + return extData.GetExtData().ContainsKey(key); + } + + public static object GetExtData(this IExtData extData, string key, object ifNotFound = null) + { + return extData.GetExtData().ContainsKey(key) + ? extData.GetExtData()[key] + : ifNotFound; + } + + public static void SetExtData(this IExtData extData, string key, object value) + { + extData.GetExtData()[key] = JToken.FromObject(value); + } + + public static void RemoveExtData(this IExtData extData, string key) + { + extData.GetExtData().Remove(key); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Util/ExtDataIndexerBase.cs b/src/TugDSC.Abstractions/Util/ExtDataIndexerBase.cs new file mode 100644 index 0000000..af2b82c --- /dev/null +++ b/src/TugDSC.Abstractions/Util/ExtDataIndexerBase.cs @@ -0,0 +1,18 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Util +{ + /// Extends the base extension data implementation with support for an + /// indexer to access extension data properties. + public abstract class ExtDataIndexerBase : ExtDataBase + { + public object this[string key] + { + get { return ((IExtData)this).GetExtData(key); } + set { ((IExtData)this).SetExtData(key, value); } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Abstractions/Util/IExtData.cs b/src/TugDSC.Abstractions/Util/IExtData.cs new file mode 100644 index 0000000..777442e --- /dev/null +++ b/src/TugDSC.Abstractions/Util/IExtData.cs @@ -0,0 +1,19 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace TugDSC.Util +{ + /// Model entities that implement this interface support "extension data" in the context of + /// serialization to/from JSON. Extension data allows us to add additional pieces of data + /// to an entity that was not included in the initial entity definition. However, we use + /// mostly to catch possible mismatch between client/server representations of data classes. + public interface IExtData + { + IDictionary GetExtData(); + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/ActionStatus.cs b/src/TugDSC.Server.Abstractions/ActionStatus.cs new file mode 100644 index 0000000..9ca430f --- /dev/null +++ b/src/TugDSC.Server.Abstractions/ActionStatus.cs @@ -0,0 +1,19 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using TugDSC.Model; + +namespace TugDSC.Server +{ + public class ActionStatus + { + public DscActionStatus NodeStatus + { get; set; } + + public IEnumerable ConfigurationStatuses + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/AppSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/AppSettings.cs new file mode 100644 index 0000000..1cfb138 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/AppSettings.cs @@ -0,0 +1,19 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Server.Configuration +{ + public class AppSettings + { + public ChecksumSettings Checksum + { get; set; } + + public AuthzSettings Authz + { get; set; } + + public HandlerSettings Handler + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/AuthzSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/AuthzSettings.cs new file mode 100644 index 0000000..e6ab120 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/AuthzSettings.cs @@ -0,0 +1,27 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; + +namespace TugDSC.Server.Configuration +{ + public class AuthzSettings + { + // TODO: Need to think about the extensibility model for + // authorization, if we want to use the Provider mechanism + + // public ExtSettings Ext + // { get; set; } + + // [Required] + // public string Provider + // { get; set; } + + // This has to be concrete class, not interface to + // be able to construct during deserialization + public Dictionary Params + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/ChecksumSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/ChecksumSettings.cs new file mode 100644 index 0000000..719e245 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/ChecksumSettings.cs @@ -0,0 +1,16 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Server.Configuration +{ + public class ChecksumSettings + { + public ExtSettings Ext + { get; set; } + + public string Default + { get; set; } = "SHA-256"; + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/ExtSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/ExtSettings.cs new file mode 100644 index 0000000..b7ab634 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/ExtSettings.cs @@ -0,0 +1,22 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +namespace TugDSC.Server.Configuration +{ + public class ExtSettings + { + public bool ReplaceExtAssemblies + { get; set; } + + public string[] SearchAssemblies + { get; set; } + + public bool ReplaceExtPaths + { get; set; } + + public string[] SearchPaths + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/HandlerSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/HandlerSettings.cs new file mode 100644 index 0000000..83351f2 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/HandlerSettings.cs @@ -0,0 +1,25 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace TugDSC.Server.Configuration +{ + public class HandlerSettings + { + public ExtSettings Ext + { get; set; } + + [Required] + public string Provider + { get; set; } = "basic"; + + // This has to be concrete class, not interface to + // be able to construct during deserialization + public Dictionary Params + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Configuration/LogSettings.cs b/src/TugDSC.Server.Abstractions/Configuration/LogSettings.cs new file mode 100644 index 0000000..2a2335a --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Configuration/LogSettings.cs @@ -0,0 +1,30 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; + +namespace TugDSC.Server.Configuration +{ + public class LogSettings + { + public LogType LogType + { get; set; } + + public bool DebugLog + { get; set; } + } + + [Flags] + public enum LogType + { + None = 0x0, + + Console = 0x1, + + NLog = 0x2, + + All = Console | NLog, + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/DscHandlerConfig.cs b/src/TugDSC.Server.Abstractions/DscHandlerConfig.cs new file mode 100644 index 0000000..c4e8474 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/DscHandlerConfig.cs @@ -0,0 +1,15 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; + +namespace TugDSC.Server +{ + public class DscHandlerConfig + { + public IDictionary InitParams + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/FileContent.cs b/src/TugDSC.Server.Abstractions/FileContent.cs new file mode 100644 index 0000000..8390ceb --- /dev/null +++ b/src/TugDSC.Server.Abstractions/FileContent.cs @@ -0,0 +1,21 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.IO; + +namespace TugDSC.Server +{ + public class FileContent + { + public string ChecksumAlgorithm + { get; set; } + + public string Checksum + { get; set; } + + public Stream Content + { get; set; } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilter.cs b/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilter.cs new file mode 100644 index 0000000..631735d --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilter.cs @@ -0,0 +1,354 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using TugDSC.Messages; +using TugDSC.Server.Configuration; +using TugDSC.Util; + +namespace TugDSC.Server.Filters +{ + /// + /// An Action Filter that implements + /// authorization logic according to the DSC Pull Server "Registration + /// Key Authorization" mechanism. + /// + /// + /// Despite the name, this filter is implemented as an MVC Action Filter, + /// not an Authorization Filter due to the need for access to input data + /// elements that are processed and available just before invoking the + /// resolved Controller Action. + /// + public class DscRegKeyAuthzFilter : IAuthorizationFilter, IActionFilter + { + public const string SHARED_AUTHORIZATION_PREFIX = "Shared "; + + private const string HTTP_CONTEXT_ITEM_AGENT_REG_KEY = nameof(DscRegKeyAuthzFilter) + + ":AgentRegKey"; + + private ILogger _logger; + + private IAuthzStorageHandler _handler; + + public DscRegKeyAuthzFilter(ILogger logger, + IAuthzStorageHandler handler) + { + _logger = logger; + _handler = handler; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var routeName = context?.ActionDescriptor?.AttributeRouteInfo?.Name; + var body = context?.HttpContext?.Request?.Body; + + if (RegisterDscAgentRequest.ROUTE_NAME == routeName) + { + // The Register DSC message is where we try to validate + // the request's body against a valid Registration Key + // so we're going to pre-compute the hash of the body + // early in the request pipeline so that we can make + // use of it later on to compute the full HMAC sigs + + if (body == null) + { + _logger.LogError("agent registration request did not provide a request body"); + context.Result = new BadRequestResult(); + return; + } + + var authzHeader = (string)context.HttpContext.Request?.Headers[ + nameof(HttpRequestHeaders.Authorization)]; + var msDateHeader = (string)context.HttpContext.Request?.Headers[ + DscRequest.X_MS_DATE_HEADER]; + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("received authorization header [{authzHeader}]", authzHeader); + _logger.LogDebug("received x-ms-date header [{msDateHeader}]", msDateHeader); + } + + if (string.IsNullOrEmpty(authzHeader)) + { + _logger.LogError("agent registration request did not provide an authorization header"); + context.Result = new UnauthorizedResult(); + return; + } + + if (string.IsNullOrEmpty(msDateHeader)) + { + _logger.LogError("agent registration request did not provide an MS Date"); + context.Result = new BadRequestResult(); + return; + } + + // Make sure the MS Date is in the proper format + var msDateValue = DateTime.ParseExact(msDateHeader, DscRequest.X_MS_DATE_FORMAT, + CultureInfo.CurrentCulture); + // Make sure the MS Date is reasonbly recent -- TODO: app setting? + var msDateEpoch = DateTime.UtcNow; + var msDateDiff = msDateEpoch.Subtract(msDateValue); + var minTimeSpan = TimeSpan.FromSeconds(-30); + var maxTimeSpan = TimeSpan.FromSeconds(30); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug($"min=[{minTimeSpan}]; msDateDiff=[{msDateDiff}]; max=[{maxTimeSpan}]"); + if (msDateDiff < minTimeSpan || msDateDiff > maxTimeSpan) + { + _logger.LogError("agent registration request provided an out-of-range MS Date", + DscRequest.X_MS_DATE_HEADER); + context.Result = new BadRequestResult(); + return; + } + + using (var ms = new MemoryStream()) + { + body.CopyTo(ms); + + var bodyBytes = ms.ToArray(); + var agentRegKey = ValidateRegKeySignature(authzHeader, msDateHeader, + _handler.RegistrationKeys, bodyBytes); + + if (string.IsNullOrEmpty(agentRegKey)) + { + _logger.LogError("agent registration request failed to match any registration key"); + context.Result = new UnauthorizedResult(); + return; + } + + // Remember the reg key for later in the request pipeline (see down below) + context.HttpContext.Items[HTTP_CONTEXT_ITEM_AGENT_REG_KEY] = agentRegKey; + + // Finally, since we *ate* up the body in order to compute the + // signature we need to replace it with another copy, so that + // it can be model-bound before invoking the action method + context.HttpContext.Request.Body = new MemoryStream(bodyBytes); + } + } + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var routeName = context?.ActionDescriptor?.AttributeRouteInfo?.Name; + var input = (context.ActionArguments?.FirstOrDefault())?.Value; + var dscRequ = input as DscRequest; + + if (dscRequ == null) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("action does not resolve any DscRequest arguments; SKIPPING"); + return; + } + + var agentId = dscRequ.GetAgentId(); + if (agentId == null || agentId == Guid.Empty) + { + _logger.LogError("DSC request Agent ID is missing or invalid"); + context.Result = new BadRequestResult(); + return; + } + + if (RegisterDscAgentRequest.ROUTE_NAME == routeName) + { + // The Register DSC message is where we try to validate + // the request's body against a valid Registration Key + var regRequ = (RegisterDscAgentRequest)dscRequ; + var agentRegKey = context.HttpContext.Items[HTTP_CONTEXT_ITEM_AGENT_REG_KEY] + as string; + + if (regRequ == null) + { + _logger.LogError("agent registration request is missing expected request input"); + context.Result = new BadRequestResult(); + return; + } + + if (string.IsNullOrEmpty(agentRegKey)) + { + _logger.LogError("agent registration request did not match any valid registration key"); + context.Result = new UnauthorizedResult(); + return; + } + + // At this point authorization was successful, so let's remember the Agent ID for future calls + _handler.StoreAgentAuthorization(agentId.Value, agentRegKey, + JsonConvert.SerializeObject(regRequ.Body)); + } + else + { + // For all other requests, if they are valid DSC + // messages we just need to validate that they are + // associated with a previously registered Agent ID + // so validate that the Agent ID has been registered + if (!_handler.IsAgentAuthorized(agentId.Value)) + { + _logger.LogWarning("failed RegKey authorization for Agent ID [{agentId}]", agentId); + context.Result = new UnauthorizedResult(); + } + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { } + + public static string ValidateRegKeySignature(string authzHeader, string msDateHeader, + IEnumerable regKeys, byte[] bodyBytes) + { + if (string.IsNullOrEmpty(authzHeader) || !authzHeader.StartsWith( + SHARED_AUTHORIZATION_PREFIX, StringComparison.CurrentCultureIgnoreCase)) + throw new InvalidDataException( + /*SR*/"registration header is invalid") + .WithData(nameof(authzHeader), authzHeader); + + authzHeader = authzHeader.Replace(SHARED_AUTHORIZATION_PREFIX, "").Trim(); + + using (var sha = SHA256.Create()) + { + var hash = sha.ComputeHash(bodyBytes); + var hashB64 = Convert.ToBase64String(hash); + var toBeSigned = $"{hashB64}\n{msDateHeader}"; + var toBeSignedBytes = Encoding.UTF8.GetBytes(toBeSigned); + + foreach (var rk in regKeys) + { + var regKey = rk.Trim(); + var regKeyBytes = Encoding.UTF8.GetBytes(regKey); + using (var hmac = new HMACSHA256(regKeyBytes)) + { + var sig = Convert.ToBase64String(hmac.ComputeHash(toBeSignedBytes)); + if (sig == authzHeader) + { + return rk; + } + } + } + } + + return null; + } + + /// + /// Defines an interface that provides necessary persistence-related + /// services required by the authorization filter. + /// + public interface IAuthzStorageHandler + { + IEnumerable RegistrationKeys + { get; } + + void StoreAgentAuthorization(Guid agentId, string regKey, string regDetails); + + bool IsAgentAuthorized(Guid agentId); + } + + /// + /// Default implementation of a handler that is based on local disk-based + /// storage services. + /// + public class LocalAuthzStorageHandler : IAuthzStorageHandler + { + public const string REG_KEY_PATH = "RegistrationKeyPath"; + public const string REG_SAVE_PATH = "RegistrationSavePath"; + + public const string REG_KEY_DEFAULT_FILENAME = "RegistrationKeys.txt"; + public const char REG_KEY_FILE_COMMENT_START = '#'; + + private ILogger _logger; + private AuthzSettings _settings; + + private string _regKeyFilePath; + private string _regSavePath; + + + public LocalAuthzStorageHandler(ILogger logger, + IOptions settings) + { + _logger = logger; + _settings = settings.Value; + + if (!(_settings.Params?.ContainsKey(REG_KEY_PATH)).GetValueOrDefault()) + throw new InvalidOperationException( + /*SR*/"missing required Registration Key Path setting"); + if (!(_settings.Params?.ContainsKey(REG_SAVE_PATH)).GetValueOrDefault()) + throw new InvalidOperationException( + /*SR*/"missing required Registration Save Path setting"); + + _regKeyFilePath = Path.GetFullPath(_settings.Params[REG_KEY_PATH].ToString()); + _regSavePath = Path.GetFullPath(_settings.Params[REG_SAVE_PATH].ToString()); + + if (!File.Exists(_regKeyFilePath) && Directory.Exists(_regKeyFilePath)) + { + _regKeyFilePath = Path.Combine(_regKeyFilePath, REG_KEY_DEFAULT_FILENAME); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("resolved reg key file path as [{regKeyFilePath}]", _regKeyFilePath); + _logger.LogDebug("resolved reg save path as [{regSavePath}]", _regSavePath); + } + + if (!File.Exists(_regKeyFilePath)) + throw new InvalidOperationException( + /*SR*/"could not find registration key file") + .WithData(nameof(_regKeyFilePath), _regKeyFilePath); + + if (!Directory.Exists(_regSavePath)) + { + _logger.LogInformation("registartion save path not found, trying to create"); + var dirInfo = Directory.CreateDirectory(_regSavePath); + if (!dirInfo.Exists) + throw new InvalidOperationException( + /*SR*/"could not create registration save directory") + .WithData(nameof(_regSavePath), _regSavePath); + } + } + + public IEnumerable RegistrationKeys => + // Resolve reg keys from file as non-blank lines after optional comments + // (starting with a '#') and any surround whitespace have been stripped + File.ReadAllLines(_regKeyFilePath) + .Select(x => x.Split(REG_KEY_FILE_COMMENT_START)[0].Trim()) + .Where(x => x.Length > 0); + + public void StoreAgentAuthorization(Guid agentId, string regKey, string regDetails) + { + var regKeySavePath = Path.Combine(_regSavePath, $"{agentId}.regkey"); + var detailSavePath = Path.Combine(_regSavePath, $"{agentId}.json"); + File.WriteAllText(regKeySavePath, regKey); + File.WriteAllText(detailSavePath, regDetails); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("saved registration key to [{savePath}]", regKeySavePath); + _logger.LogDebug("saved registration details to [{savePath}]", detailSavePath); + } + } + + public bool IsAgentAuthorized(Guid agentId) + { + var savePath = Path.Combine(_regSavePath, $"{agentId}.json"); + var isValid = File.Exists(savePath); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("looking for registration details at [{savePath}]", savePath); + _logger.LogDebug("reg key validation for Agent ID [{agentId}] = [{isValid}]", agentId, isValid); + } + + return isValid; + } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilterAlt.cs b/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilterAlt.cs new file mode 100644 index 0000000..5896325 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Filters/DscRegKeyAuthzFilterAlt.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using TugDSC.Messages; +using TugDSC.Server.Configuration; +using TugDSC.Util; + +namespace TugDSC.Server.Filters +{ + /// + /// An Action Filter that implements + /// authorization logic according to the DSC Pull Server "Registration + /// Key Authorization" mechanism. + /// + /// + ///

NOTE: This is the original implementation of the RegKey Authz + /// filter which performs its validation in an alternate method. it + /// is preserved as an alternative-implementation filter.

+ /// + /// Despite the name, this filter is implemented as an MVC Action Filter, + /// not an Authorization Filter due to the need for access to input data + /// elements that are processed and available just before invoking the + /// resolved Controller Action. + ///
+ public class DscRegKeyAuthzFilterAlt : IActionFilter + { + public const string REG_KEY_PATH = "RegistrationKeyPath"; + public const string REG_SAVE_PATH = "RegistrationSavePath"; + + public const string REG_KEY_DEFAULT_FILENAME = "RegistrationKeys.txt"; + public const char REG_KEY_FILE_COMMENT_START = '#'; + + public const string SHARED_AUTHORIZATION_PREFIX = "Shared "; + + private ILogger _logger; + private AuthzSettings _settings; + + private string _regKeyFilePath; + private string _regSavePath; + + public DscRegKeyAuthzFilterAlt(ILogger logger, + IOptions settings) + { + _logger = logger; + _settings = settings.Value; + + if (!(_settings.Params?.ContainsKey(REG_KEY_PATH)).GetValueOrDefault()) + throw new InvalidOperationException( + /*SR*/"missing required Registration Key Path setting"); + if (!(_settings.Params?.ContainsKey(REG_SAVE_PATH)).GetValueOrDefault()) + throw new InvalidOperationException( + /*SR*/"missing required Registration Save Path setting"); + + _regKeyFilePath = Path.GetFullPath(_settings.Params[REG_KEY_PATH].ToString()); + _regSavePath = Path.GetFullPath(_settings.Params[REG_SAVE_PATH].ToString()); + + if (!File.Exists(_regKeyFilePath) && Directory.Exists(_regKeyFilePath)) + { + _regKeyFilePath = Path.Combine(_regKeyFilePath, REG_KEY_DEFAULT_FILENAME); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("resolved reg key file path as [{regKeyFilePath}]", _regKeyFilePath); + _logger.LogDebug("resolved reg save path as [{regSavePath}]", _regSavePath); + } + + if (!File.Exists(_regKeyFilePath)) + throw new InvalidOperationException( + /*SR*/"could not find registration key file") + .WithData(nameof(_regKeyFilePath), _regKeyFilePath); + + if (!Directory.Exists(_regSavePath)) + { + _logger.LogInformation("registartion save path not found, trying to create"); + var dirInfo = Directory.CreateDirectory(_regSavePath); + if (!dirInfo.Exists) + throw new InvalidOperationException( + /*SR*/"could not create registration save directory") + .WithData(nameof(_regSavePath), _regSavePath); + } + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var inputArg = context.ActionArguments?.FirstOrDefault(); + + // Skip this filter if there is not at least one input argument + if (inputArg == null) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("action does not resolve any arguments; SKIPPING"); + return; + } + + var input = inputArg.Value.Value as DscRequest; + if (input == null) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("action does not resolve any DscRequest arguments; SKIPPING"); + return; + } + + var isValid = false; + var agentId = input.GetAgentId(); + + // We have 2 scenarios to test for... + + // Scenario #1 - a RegisterDscAgent message in which + // case we have to validate the request against a valid + // RegKey and "remember" the Agent ID for future calls + if (input is RegisterDscAgentRequest) + { + var requ = (RegisterDscAgentRequest)input; + var authz = requ.AuthorizationHeader; + var xmsdate = requ.MsDateHeader; + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("received authorization header [{authzHeader}]", authz); + _logger.LogDebug("received x-ms-date header [{msDateHeader}]", xmsdate); + } + + // NOTE: we repeat the following process on every lookup instead + // of caching it as a fast and dirty way of picking up any + // changes to the file. + // TODO: in future preload the file into an array and reload after + // listening for and detecting any file changes + + // Resolve reg keys from file as non-blank lines after optional comments + // (starting with a '#') and any surround whitespace have been stripped + var regKeys = File.ReadAllLines(_regKeyFilePath) + .Select(x => x.Split(REG_KEY_FILE_COMMENT_START)[0].Trim()) + .Where(x => x.Length > 0); + var bodyJson = JsonConvert.SerializeObject(requ.Body); + var bodyBytes = Encoding.UTF8.GetBytes(bodyJson); + isValid = ValidateRegKeySignature(authz, xmsdate, regKeys, bodyBytes); + + if (isValid) + { + // At this point authorization was successful, so let's remember the Agent ID for future calls + var savePath = Path.Combine(_regSavePath, $"{requ.AgentId}.json"); + File.WriteAllText(savePath, JsonConvert.SerializeObject(requ.Body)); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("saved registration details to [{savePath}]", savePath); + } + } + else + { + // In subsequent calls after the initial reg, we just + // need to validate that the Agent ID has been registered + var savePath = Path.Combine(_regSavePath, $"{agentId}.json"); + isValid = File.Exists(savePath); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("looking for registration details at [{savePath}]", savePath); + _logger.LogDebug("reg key validation for Agent ID [{agentId}] = [{isValid}]", agentId, isValid); + } + } + + if (!isValid) + { + _logger.LogWarning("failed RegKey authorization for Agent ID [{agentId}]", agentId); + context.Result = new UnauthorizedResult(); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { } + + public static bool ValidateRegKeySignature(string authzHeader, string msDateHeader, + IEnumerable regKeys, byte[] bodyBytes) + { + if (string.IsNullOrEmpty(authzHeader) || !authzHeader.StartsWith( + SHARED_AUTHORIZATION_PREFIX, StringComparison.CurrentCultureIgnoreCase)) + throw new InvalidDataException( + /*SR*/"registration header is invalid") + .WithData(nameof(authzHeader), authzHeader); + + authzHeader = authzHeader.Replace(SHARED_AUTHORIZATION_PREFIX, "").Trim(); + + using (var sha = SHA256.Create()) + { + var hash = sha.ComputeHash(bodyBytes); + var hashB64 = Convert.ToBase64String(hash); + var toBeSigned = $"{hashB64}\n{msDateHeader}"; + var toBeSignedBytes = Encoding.UTF8.GetBytes(toBeSigned); + + foreach (var line in regKeys) + { + var regKey = line.Trim(); + var regKeyBytes = Encoding.UTF8.GetBytes(regKey); + using (var hmac = new HMACSHA256(regKeyBytes)) + { + var sig = Convert.ToBase64String(hmac.ComputeHash(toBeSignedBytes)); + if (sig == authzHeader) + { + return true; + } + } + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Filters/InspectAuthzFilter.cs b/src/TugDSC.Server.Abstractions/Filters/InspectAuthzFilter.cs new file mode 100644 index 0000000..758178e --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Filters/InspectAuthzFilter.cs @@ -0,0 +1,49 @@ +using System.IO; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Server.Filters +{ + /// + /// An authorization filter that's used to test and inspect the various elements + /// of the filter/request context -- this is not meant to be used in a production + /// capacity and offers no real functionality other than writing details to the console. + /// + public class InspectAuthzFilter : IAuthorizationFilter + { + private ILogger _logger; + + public InspectAuthzFilter(ILogger logger) + { + _logger = logger; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var cad = context.ActionDescriptor as ControllerActionDescriptor; + _logger.LogInformation($" Action[{context.ActionDescriptor.Id}] = [{context.ActionDescriptor.DisplayName}]"); + _logger.LogInformation($" Route[{context.ActionDescriptor.AttributeRouteInfo.Name}]"); + + System.Console.WriteLine($" Action.......[{context.ActionDescriptor.Id}] = [{context.ActionDescriptor.DisplayName}]"); + System.Console.WriteLine($" ActionName...[{cad?.ActionName}]"); + System.Console.WriteLine($" RouteName....[{context.ActionDescriptor.AttributeRouteInfo.Name}]"); + + byte[] body; + using (var ms = new MemoryStream()) + { + context.HttpContext.Request.Body.CopyTo(ms); + body = ms.ToArray(); + } + + if (body != null) + { + // We need to replace the body that we previously read so that + // it can be processed by the action, i.e. bound to an input model + context.HttpContext.Request.Body = new MemoryStream(body); + } + + System.Console.WriteLine($" Body.Length = {body.Length}"); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Filters/StrictInputFilter.cs b/src/TugDSC.Server.Abstractions/Filters/StrictInputFilter.cs new file mode 100644 index 0000000..c145ebb --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Filters/StrictInputFilter.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using TugDSC.Util; + +namespace TugDSC.Server.Filters +{ + /// + /// An Action Filter that validates the request + /// input content to make sure it strictly conforms to the associated model class. + /// + /// + /// This filter inspects each resolved action argument and foreach one that + /// defines member properties that implement the External + /// Data interface, it computes recursively if there are any external data + /// values set. If so, that means that there was input data that did not conform + /// strictly to the associated data model and therefore will result in a + /// Bad Request (400) response, aborting any subsequent action invocation. + /// + public class StrictInputFilter : IActionFilter + { + protected ILogger _logger; + + public StrictInputFilter(ILogger logger) + { + _logger = logger; + } + + public virtual void OnActionExecuting(ActionExecutingContext context) + { + int extDataCount = 0; + foreach (var arg in context.ActionArguments) + { + int argExtDataCount = GetExtDataCount(arg.Value); + if (argExtDataCount > 0) + { + _logger.LogWarning("Found action argument [{arg}] with [{argExtDataCount}] extra data elements", + arg.Key, argExtDataCount); + } + extDataCount += argExtDataCount; + } + + if (extDataCount > 0) + { + context.Result = new BadRequestResult(); + } + } + + public virtual void OnActionExecuted(ActionExecutedContext context) + { } + + /// + /// Recursively identifies any properties that implement the IExtData + /// interface and comutes the total count of ext data elements. + /// + protected int GetExtDataCount(params object[] values) + { + var extDataCount = 0; + var extDataProps = 0; + if (values != null && values.Length > 0) + { + foreach (var value in values) + { + var valueType = value.GetType(); + foreach (var prop in valueType.GetTypeInfo().GetProperties()) + { + if (typeof(IExtData).IsAssignableFrom(prop.PropertyType)) + { + ++extDataProps; + var extData = (IExtData)prop.GetValue(value); + if (extData != null) + { + extDataCount += extData.GetExtDataCount(); + extDataCount += GetExtDataCount(extData); + } + } + else if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) + { + ++extDataProps; + var extDataCollection = (IEnumerable)prop.GetValue(value); + if (extDataCollection != null) + { + foreach (var item in extDataCollection) + { + extDataCount += item.GetExtDataCount(); + extDataCount += GetExtDataCount(item); + } + } + } + } + } + } + return extDataCount; + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Filters/VeryStrictInputFilter.cs b/src/TugDSC.Server.Abstractions/Filters/VeryStrictInputFilter.cs new file mode 100644 index 0000000..f5f5fdc --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Filters/VeryStrictInputFilter.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using TugDSC.Messages; + +namespace TugDSC.Server.Filters +{ + /// + /// An Action Filter that validates the request + /// input content to make sure it (very) strictly conforms to the associated model class. + /// + /// + /// This filter builds upon the filter and adds + /// additional strict validation checks such as making sure that each JSON body + /// payload is serialized and deserialized in an exact and predictable form + /// as dictated by action's associated DSC Request message model class. This + /// would include the exact order and element type of each JSON properties. + /// + public class VeryStrictInputFilter : StrictInputFilter, IAuthorizationFilter + { + public VeryStrictInputFilter(ILogger logger) + : base(logger) + { } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var body = context?.HttpContext?.Request?.Body; + + if (body != null) + { + using (var ms = new MemoryStream()) + { + body.CopyTo(ms); + var bodyBytes = ms.ToArray(); + context.HttpContext.Items["bodyBytes"] = bodyBytes; + context.HttpContext.Request.Body = new MemoryStream(bodyBytes); + } + } + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + base.OnActionExecuting(context); + + var input = (context.ActionArguments?.FirstOrDefault())?.Value; + var dscRequ = input as DscRequest; + var bodyBytes = context.HttpContext.Items["bodyBytes"] as byte[]; + var bodyBytesLen = (int)bodyBytes?.Length; + + if (dscRequ != null && dscRequ.HasStrictBody()) + { + var dscRequBody = dscRequ.GetBody(); + if (dscRequBody == null && bodyBytesLen == 0) + { + // No body found and no body expected, we're good + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("No body content received and none expected in DSC Request"); + } + else if (bodyBytesLen == 0) + { + _logger.LogWarning("Expected DSC Request input body, but found nothing"); + context.Result = new BadRequestResult(); + return; + } + else if (dscRequBody == null) + { + _logger.LogWarning("Unexpected input body content found"); + context.Result = new BadRequestResult(); + } + else + { + var bodyDeser = Encoding.UTF8.GetString(bodyBytes); + var inputSer = JsonConvert.SerializeObject(dscRequBody); + if (bodyDeser != inputSer) + { + _logger.LogWarning("Re-serialized representation does not match actual input"); + if (_logger.IsEnabled(LogLevel.Trace)) + { + _logger.LogTrace("Expected: {expected}", inputSer); + _logger.LogTrace("Actual: {actual}", bodyDeser); + } + context.Result = new BadRequestResult(); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/IChecksumAlgorithm.cs b/src/TugDSC.Server.Abstractions/IChecksumAlgorithm.cs new file mode 100644 index 0000000..4f1c057 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/IChecksumAlgorithm.cs @@ -0,0 +1,20 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.IO; +using TugDSC.Ext; + +namespace TugDSC +{ + public interface IChecksumAlgorithm : IProviderProduct + { + string AlgorithmName + { get; } + + string ComputeChecksum(byte[] bytes); + + string ComputeChecksum(Stream stream); + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/IChecksumAlgorithmProvider.cs b/src/TugDSC.Server.Abstractions/IChecksumAlgorithmProvider.cs new file mode 100644 index 0000000..e4eae2e --- /dev/null +++ b/src/TugDSC.Server.Abstractions/IChecksumAlgorithmProvider.cs @@ -0,0 +1,116 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TugDSC.Ext; +using TugDSC.Ext.Util; +using TugDSC.Server.Configuration; + +namespace TugDSC +{ + public interface IChecksumAlgorithmProvider : IProvider + { } + + public class ChecksumAlgorithmManager + : ProviderManagerBase + { + public ChecksumAlgorithmManager( + ILogger logger, + ILogger spLogger, + IOptions settings, + IServiceProvider sp) + : base(logger, new ServiceProviderExportDescriptorProvider(spLogger, sp)) + { + var extAssms = settings.Value?.Ext?.SearchAssemblies; + var extPaths = settings.Value?.Ext?.SearchPaths; + + // Add assemblies to search context + if ((settings.Value?.Ext?.ReplaceExtAssemblies).GetValueOrDefault()) + { + logger.LogInformation("Resetting default Search Assemblies"); + ClearSearchAssemblies(); + } + + if (extAssms?.Length > 0) + { + logger.LogInformation("Adding Search Assemblies"); + AddSearchAssemblies( + extAssms.Select(x => + { + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" * Adding [{x}]"); + + var an = GetAssemblyName(x); + if (an == null) + throw new ArgumentException("invalid assembly name"); + + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" o Resolved as AsmName [{an}]{Directory.GetCurrentDirectory()}:{an}"); + return an; + }).Select(x => + { + var asm = GetAssembly(x); + if (asm == null) + throw new InvalidOperationException("unable to resolve assembly from name"); + + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" o [{x.FullName}]"); + + return asm; + })); + } + + // Add dir paths to search context + if ((settings.Value?.Ext?.ReplaceExtPaths).GetValueOrDefault()) + { + logger.LogInformation("Resetting default search paths"); + ClearSearchPaths(); + } + + if (extPaths?.Length > 0) + { + logger.LogInformation("Adding Search Paths"); + AddSearchPath(extPaths.Select(x => + { + var y = Path.GetFullPath(x); + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" * [{y}]"); + return y; + })); + } + + base.Init(); + } + + protected override void Init() + { + // Skipping the initialization till + // after constructor parameters are applied + } + + protected override IEnumerable FindProviders() + { + try + { + return base.FindProviders(); + } + catch (System.Reflection.ReflectionTypeLoadException ex) + { + Console.Error.WriteLine(">>>>>> Load Exceptions:"); + foreach (var lex in ex.LoaderExceptions) + { + Console.Error.WriteLine(">>>>>> >>>>" + lex); + } + throw ex; + } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/IDscHandler.cs b/src/TugDSC.Server.Abstractions/IDscHandler.cs new file mode 100644 index 0000000..39af05f --- /dev/null +++ b/src/TugDSC.Server.Abstractions/IDscHandler.cs @@ -0,0 +1,57 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using TugDSC.Ext; +using TugDSC.Model; + +namespace TugDSC.Server +{ + /// + /// Defines the operations and primitives needed to be handled + /// by a DSC implementation. + /// + /// + /// The operations are defined based on the Pull Server + /// protocol specification as found + /// here. + /// + public interface IDscHandler : IProviderProduct + { + /// + /// https://msdn.microsoft.com/en-us/library/mt590247.aspx + /// + void RegisterDscAgent(Guid agentId, + RegisterDscAgentRequestBody detail); + + /// + /// https://msdn.microsoft.com/en-us/library/mt766279.aspx + /// + ActionStatus GetDscAction(Guid agentId, + GetDscActionRequestBody detail); + + /// + /// https://msdn.microsoft.com/en-us/library/mt766328.aspx + /// + FileContent GetConfiguration(Guid agentId, string configName); + + /// + /// https://msdn.microsoft.com/en-us/library/mt766336.aspx + /// + FileContent GetModule(Guid? agentId, string moduleName, string moduleVersion); + + /// + /// https://msdn.microsoft.com/en-us/library/mt766272.aspx + /// + void SendReport(Guid agentId, SendReportBody detail); + + /// + /// https://msdn.microsoft.com/en-us/library/mt766283.aspx + /// + IEnumerable GetReports(Guid agentId, Guid? jobId); + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/IDscHandlerProvider.cs b/src/TugDSC.Server.Abstractions/IDscHandlerProvider.cs new file mode 100644 index 0000000..1ce48f2 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/IDscHandlerProvider.cs @@ -0,0 +1,83 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TugDSC.Ext; +using TugDSC.Ext.Util; +using TugDSC.Server.Configuration; + +namespace TugDSC.Server +{ + public interface IDscHandlerProvider : IProvider + { } + + public class DscHandlerManager + : ProviderManagerBase + { + public DscHandlerManager( + ILogger logger, + ILogger spLogger, + IOptions settings, + IServiceProvider sp) + : base(logger, new ServiceProviderExportDescriptorProvider(spLogger, sp)) + { + var extAssms = settings.Value?.Ext?.SearchAssemblies; + var extPaths = settings.Value?.Ext?.SearchPaths; + + // Add assemblies to search context + if ((settings.Value?.Ext?.ReplaceExtAssemblies).GetValueOrDefault()) + ClearSearchAssemblies(); + if (extAssms?.Length > 0) + { + logger.LogInformation("Adding Search Assemblies"); + AddSearchAssemblies( + extAssms.Select(x => + { + var an = GetAssemblyName(x); + if (an == null) + throw new ArgumentException("invalid assembly name"); + return an; + }).Select(x => + { + var asm = GetAssembly(x); + if (asm == null) + throw new InvalidOperationException("unable to resolve assembly from name"); + + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" * [{x.FullName}]"); + + return asm; + })); + } + + // Add dir paths to search context + if ((settings.Value?.Ext?.ReplaceExtPaths).GetValueOrDefault()) + ClearSearchPaths(); + if (extPaths?.Length > 0) + { + logger.LogInformation("Adding Search Paths"); + AddSearchPath(extPaths.Select(x => + { + var y = Path.GetFullPath(x); + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($" * [{y}]"); + return y; + })); + } + + base.Init(); + } + + protected override void Init() + { + // Skipping the initialization till + // after constructor parameters are applied + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Mvc/ModelResult.cs b/src/TugDSC.Server.Abstractions/Mvc/ModelResult.cs new file mode 100644 index 0000000..99288cf --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Mvc/ModelResult.cs @@ -0,0 +1,148 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.ComponentModel; +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using TugDSC.Messages.ModelBinding; + +namespace TugDSC.Server.Mvc +{ + public static class ModelResultExt + { +// private static readonly ILogger LOG = AppLog.Create(typeof(ModelResultExt)); + + public const string CONTENT_TYPE_TEXT = "text/plain"; + public const string CONTENT_TYPE_JSON = "application/json"; + public const string CONTENT_TYPE_OCTET_STREAM = "application/octet-stream"; + + /// + /// Defines an extension method for MVC Controllers that supports returning + /// an Action Result that is defined by + /// a response model class. + /// + /// + /// This analogous to the default model binding behavior that MVC defines + /// for controllers during the request processing stage, but is the + /// complementary behavior to support the response processing stage. + /// + /// Just like default MVC model binding, this routine works in concert + /// with an attribute-decorated model POCO. At this time it has special + /// support for, and understands the following attributes: + /// + /// ToHeader + /// ToResult + /// + /// + /// + [NonAction] // Not really necessary on an ext method, but in case we ever move it to a Controller or Controll base class + public static IActionResult Model(this ControllerBase c, object model) + { + PropertyInfo toResultProperty = null; // Used to detect more than one result property + IActionResult result = null; + + var props = model.GetType().GetTypeInfo().GetProperties(); + + foreach (var p in props) + { + var toHeader = p.GetCustomAttribute(typeof(ToHeaderAttribute)) + as ToHeaderAttribute; + if (toHeader != null) + { + var headerName = toHeader.Name; + if (string.IsNullOrEmpty(headerName)) + headerName = p.Name; + +// if (LOG.IsEnabled(LogLevel.Debug)) +// LOG.LogDebug($"Adding Header[{headerName}] replace=[{toHeader.Replace}]"); + + // TODO: Add support for string[]??? + var headerValue = ConvertTo(p.GetValue(model, null)); + if (toHeader.Replace) + c.Response.Headers[headerName] = headerValue; + else + c.Response.Headers.Add(headerName, headerValue); + + continue; + } + + var toResult = p.GetCustomAttribute(typeof(ToResultAttribute)) + as ToResultAttribute; + + if (toResult != null) + { + if (toResultProperty != null) + throw new InvalidOperationException("multiple Result-mapping attributes found"); + + toResultProperty = p; + var toResultType = p.PropertyType; + + if (typeof(IActionResult).IsAssignableFrom(toResultType)) + { + result = (IActionResult)p.GetValue(model, null); + continue; + } + + var contentType = toResult.ContentType; + + if (toResultType == typeof(byte[])) + { + var resultValue = (byte[])p.GetValue(model, null); + result = new FileContentResult(resultValue, + contentType ?? CONTENT_TYPE_OCTET_STREAM); + continue; + } + + if (typeof(Stream).IsAssignableFrom(toResultType)) + { + var resultValue = (Stream)p.GetValue(model, null); + result = new FileStreamResult(resultValue, + contentType ?? CONTENT_TYPE_OCTET_STREAM); + continue; + } + + if (typeof(FileInfo).IsAssignableFrom(toResultType)) + { + var resultValue = (FileInfo)p.GetValue(model, null); + result = new PhysicalFileResult(resultValue.FullName, + contentType ?? CONTENT_TYPE_OCTET_STREAM); + continue; + } + + if (typeof(string) == toResultType) + { + var resultValue = (string)p.GetValue(model, null); + result = new ContentResult + { + Content = resultValue, + ContentType = contentType + }; + continue; + } + + var pValue = p.GetValue(model, null); + if (pValue != null) + { + result = new JsonResult(pValue); + } + } + } + + if (result == null) + result = new OkResult(); + + return result; + } + + public static T ConvertTo(object value) + { + var tc = TypeDescriptor.GetConverter(typeof(T)); + return (T)tc.ConvertFrom(value); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/README.md b/src/TugDSC.Server.Abstractions/README.md new file mode 100644 index 0000000..5a66f8b --- /dev/null +++ b/src/TugDSC.Server.Abstractions/README.md @@ -0,0 +1,3 @@ +# README - TugDSC Server Abstractions + +This library defines abstractions and components that are common to different implementations of servers of the DSC platform. diff --git a/src/TugDSC.Server.Abstractions/TugDSC.Server.Abstractions.csproj b/src/TugDSC.Server.Abstractions/TugDSC.Server.Abstractions.csproj new file mode 100644 index 0000000..7e9ffa4 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/TugDSC.Server.Abstractions.csproj @@ -0,0 +1,33 @@ + + + + + + + netstandard2.0 + TugDSC.Server + + + + TugDSC Server Abstractions + Server-specific Abstractions & Components + + + + + + + + + + + + + + + diff --git a/src/TugDSC.Server.Abstractions/Util/ChecksumHelper.cs b/src/TugDSC.Server.Abstractions/Util/ChecksumHelper.cs new file mode 100644 index 0000000..7b1bd6d --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Util/ChecksumHelper.cs @@ -0,0 +1,80 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TugDSC.Server.Configuration; + +namespace TugDSC.Server.Util +{ + /// + /// Helper class to manage usage of Checksum Algorithm Providers and + /// implementation classes as indicated by user configuration settings. + /// + public class ChecksumHelper + { + private ILogger _logger; + private ChecksumSettings _settings; + private ChecksumAlgorithmManager _csumManager; + private string _defaultName; + private IChecksumAlgorithmProvider _defaultProvider; + + public ChecksumHelper(ILogger logger, + IOptions settings, + ChecksumAlgorithmManager csumManager) + { + _logger = logger; + _settings = settings.Value; + _csumManager = csumManager; + + Init(); + } + + public string DefaultAlgorithmName + { + get { return _defaultName; } + } + + public IChecksumAlgorithm GetAlgorithm(string name = null) + { + if (name == null) + name = _defaultName; + return _csumManager.GetProvider(name).Produce(); + } + + private void Init() + { + // _logger.LogInformation("constructing Checksum Algorithm Provider Manager"); + // var csumManager = new ChecksumAlgorithmManager(); + // services.AddSingleton(csumManager); + + _logger.LogInformation("resolved the following Checksum Providers:"); + foreach (var fpn in _csumManager.FoundProvidersNames) + _logger.LogInformation($" * [{fpn}]"); + + _logger.LogInformation("resolving default Checksum Algorithm:"); + if (!string.IsNullOrEmpty(_settings?.Default)) + { + _defaultName = _settings.Default; + _logger.LogInformation(" resolved as [{defaultProviderName}]", _defaultName); + _defaultProvider = _csumManager.GetProvider(_settings.Default); + if (_defaultProvider == null) + throw new ArgumentException("invalid, missing or unresolved Provider name"); + // services.AddSingleton(csumProvider); + } + else + { + _logger.LogWarning(" no explicit Default Checksum algorithm specified"); + var first = _csumManager.FoundProvidersNames.FirstOrDefault(); + if (string.IsNullOrEmpty(first)) + throw new InvalidOperationException("unable to resolve first provider"); + _logger.LogInformation(" defaulting to first {firstCsum}", first); + _defaultName = first; + } + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.Abstractions/Util/DscHandlerHelper.cs b/src/TugDSC.Server.Abstractions/Util/DscHandlerHelper.cs new file mode 100644 index 0000000..926b435 --- /dev/null +++ b/src/TugDSC.Server.Abstractions/Util/DscHandlerHelper.cs @@ -0,0 +1,78 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TugDSC.Server.Configuration; + +namespace TugDSC.Server.Util +{ + public class DscHandlerHelper + { + private ILogger _logger; + private HandlerSettings _settings; + private DscHandlerManager _dscManager; + + private IDscHandlerProvider _defaultDscProvider; + private IDscHandler _defaultDscHandler; + + public DscHandlerHelper(ILogger logger, + IOptions settings, + DscHandlerManager dscManager) + { + _logger = logger; + _settings = settings.Value; + _dscManager = dscManager; + + Init(); + } + + public IDscHandler DefaultHandler + { + get { return _defaultDscHandler; } + } + + private void Init() + { + // _logger.LogInformation("constructing DSC Handler Provider Manager"); + // var dscManager = new DscHandlerManager(); + _logger.LogInformation("resolved the following DSC Handler Providers:"); + foreach (var fpn in _dscManager.FoundProvidersNames) + _logger.LogInformation($" * [{fpn}]"); + + _logger.LogInformation("resolving target Provider"); + _defaultDscProvider = _dscManager.GetProvider(_settings.Provider); + if (_defaultDscProvider == null) + throw new ArgumentException("invalid, missing or unresolved Provider name"); + + _logger.LogInformation("applying optional DSC Handler parameters"); + if (_settings.Params?.Count > 0) + _defaultDscProvider.SetParameters(_settings.Params); + + _logger.LogInformation("producing DSC Handler"); + _defaultDscHandler = _defaultDscProvider.Produce(); + if (_defaultDscHandler == null) + throw new InvalidOperationException("failed to construct DSC Handler"); + + // services.AddSingleton(dscHandler); + + + + // _logger.LogInformation($"Resolving DSC Handler Provider for [{settings?.Provider}]"); + // var handlerProviderType = Type.GetType(settings?.Provider); + // if (handlerProviderType == null) + // throw new Exception("Unable to resolve DSC Handler Provider type (is the type specified fully?)"); + + // services.AddSingleton(new DscHandlerConfig + // { + // InitParams = settings?.Params, + // }); + + // services.AddSingleton( + // typeof(IDscHandlerProvider), handlerProviderType); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/AppLog.cs b/src/TugDSC.Server.WebAppHost/AppLog.cs new file mode 100644 index 0000000..7440aa4 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/AppLog.cs @@ -0,0 +1,56 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Server.WebAppHost +{ + /// + /// Implements a static access pattern for ILoggerFactory. + /// + /// + /// This pattern is based on this static ApplicationLogging class approach. + /// This allows us to introduce logging to static classes (such as Extension + /// Method classes) that cannot participate in dependency-injected services. + /// + public static class AppLog + { + private static LoggerFactory _preLoggerFactory; + + static AppLog() + { + // We set this up to log any events that take place before the + // ultimate logging configuration is finalized and realized + _preLoggerFactory = new LoggerFactory(); + // Here we configure the hard-coded settings of the pre-logger with + // anything we want before the runtime logging config is resolved + _preLoggerFactory.AddConsole(); + + // This will be the final runtime logger factory + Factory = new LoggerFactory(); + } + + public static ILogger CreatePreLogger() + { + return _preLoggerFactory.CreateLogger(); + } + + public static ILoggerFactory Factory + { get; } + + public static ILogger Create(Type t) + { + return Factory.CreateLogger(t); + } + + public static ILogger Create() + { + return Factory.CreateLogger(); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/Controllers/DscController.cs b/src/TugDSC.Server.WebAppHost/Controllers/DscController.cs new file mode 100644 index 0000000..893ae57 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Controllers/DscController.cs @@ -0,0 +1,155 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Logging; +using TugDSC.Messages; +using TugDSC.Model; +using TugDSC.Server.Mvc; +using TugDSC.Server.Util; + +namespace TugDSC.Server.WebAppHost.Controllers +{ + /// + /// A controller that implements the core v2 requests for a + /// DSC Pull Server, including registration, status checking, + /// configuration retrieval and module retrieval. + /// + public class DscController : Controller + { + private ILogger _logger; + private DscHandlerHelper _dscHelper; + private IDscHandler _dscHandler; + + public DscController(ILogger logger, + DscHandlerHelper dscHelper) + { + _logger = logger; + _dscHelper = dscHelper; + _dscHandler = _dscHelper.DefaultHandler; + } + + [HttpPut] + [Route(RegisterDscAgentRequest.ROUTE, + Name = RegisterDscAgentRequest.ROUTE_NAME)] + [ActionName(nameof(RegisterDscAgent))] + public IActionResult RegisterDscAgent(RegisterDscAgentRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(RegisterDscAgent)}: {RegisterDscAgentRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"AgentId=[{input.AgentId}]"); + _dscHandler.RegisterDscAgent(input.AgentId.Value, input.Body); + + return this.Model(RegisterDscAgentResponse.INSTANCE); + } + + return base.BadRequest(ModelState); + } + + [HttpPost] + [Route(GetDscActionRequest.ROUTE, + Name = GetDscActionRequest.ROUTE_NAME)] + [ActionName(nameof(GetDscAction))] + public IActionResult GetDscAction(GetDscActionRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(GetDscAction)}: {GetDscActionRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"AgentId=[{input.AgentId}]"); + + var actionInfo = _dscHandler.GetDscAction(input.AgentId.Value, input.Body); + if (actionInfo == null) + return NotFound(); + + var response = new GetDscActionResponse + { + Body = new GetDscActionResponseBody + { + NodeStatus = actionInfo.NodeStatus, + Details = actionInfo.ConfigurationStatuses?.ToArray(), + } + }; + + return this.Model(response); + } + + return base.BadRequest(ModelState); + } + + + [HttpGet] + [Route(GetConfigurationRequest.ROUTE, + Name = GetConfigurationRequest.ROUTE_NAME)] + [ActionName(nameof(GetConfiguration))] + public IActionResult GetConfiguration(GetConfigurationRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(GetConfiguration)}: {GetConfigurationRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"AgentId=[{input.AgentId}] Configuration=[{input.ConfigurationName}]"); + + var configContent = _dscHandler.GetConfiguration(input.AgentId.Value, + // TODO: + // Strictly speaking, this may not be how the DSCPM + // protocol is supposed to resolve the config name + input.ConfigurationName ?? input.ConfigurationNameHeader); + if (configContent == null) + return NotFound(); + + var response = new GetConfigurationResponse + { + ChecksumAlgorithmHeader = configContent.ChecksumAlgorithm, + ChecksumHeader = configContent.Checksum, + Configuration = configContent.Content, + }; + + return this.Model(response); + } + + return BadRequest(ModelState); + } + + [HttpGet] + [Route(GetModuleRequest.ROUTE, + Name = GetModuleRequest.ROUTE_NAME)] + [ActionName(nameof(GetModule))] + public IActionResult GetModule(GetModuleRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(GetModule)}: {GetModuleRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"Module name=[{input.ModuleName}] Version=[{input.ModuleVersion}]"); + + var moduleContent = _dscHandler.GetModule(input.GetAgentId(), + input.ModuleName, input.ModuleVersion); + if (moduleContent == null) + return NotFound(); + + var response = new GetModuleResponse + { + ChecksumAlgorithmHeader = moduleContent.ChecksumAlgorithm, + ChecksumHeader = moduleContent.Checksum, + Module = moduleContent.Content, + }; + + return this.Model(response); + } + + return BadRequest(ModelState); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/Controllers/DscReportingController.cs b/src/TugDSC.Server.WebAppHost/Controllers/DscReportingController.cs new file mode 100644 index 0000000..9a1d188 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Controllers/DscReportingController.cs @@ -0,0 +1,108 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using TugDSC.Messages; +using TugDSC.Server.Mvc; +using TugDSC.Server.Util; + +namespace TugDSC.Server.WebAppHost.Controllers +{ + public class DscReportingController : Controller + { + private ILogger _logger; + private DscHandlerHelper _dscHelper; + private IDscHandler _dscHandler; + + public DscReportingController(ILogger logger, + DscHandlerHelper dscHelper) + { + _logger = logger; + _dscHelper = dscHelper; + _dscHandler = _dscHelper.DefaultHandler; + } + + [HttpPost] + [Route(SendReportRequest.ROUTE, + Name = SendReportRequest.ROUTE_NAME)] + [ActionName(nameof(SendReport))] + public IActionResult SendReport(SendReportRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(SendReport)}: {SendReportRequest.VERB}"); + + if (ModelState.IsValid) + { + // This validation of the date elements will throw a FormatException + // and result in a 500 error if the dates are invalid which matches + // the observed and tested behavior of the Classic DSC Pull Server + if (!string.IsNullOrEmpty(input.Body.StartTime)) + DateTime.Parse(input.Body.StartTime); + if (!string.IsNullOrEmpty(input.Body.EndTime)) + DateTime.Parse(input.Body.EndTime); + + _logger.LogDebug($"AgentId=[{input.AgentId}]"); + _dscHandler.SendReport(input.AgentId.Value, input.Body); + return Ok(); + } + + return BadRequest(ModelState); + } + + [HttpGet] + [Route(GetReportsRequest.ROUTE_SINGLE, + Name = GetReportsRequest.ROUTE_SINGLE_NAME)] + [ActionName(nameof(GetReportsSingle))] + public IActionResult GetReportsSingle(GetReportsRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(GetReportsSingle)}: {GetReportsRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"AgentId=[{input.AgentId}]"); + var sr = _dscHandler.GetReports(input.AgentId.Value, input.JobId); + + return this.Model(new GetReportsSingleResponse + { + Body = sr.FirstOrDefault(), + }); + } + + return BadRequest(ModelState); + } + + [HttpGet] + [Route(GetReportsRequest.ROUTE_ALL, + Name = GetReportsRequest.ROUTE_ALL_NAME)] + [Route(GetReportsRequest.ROUTE_ALL_ALT, + Name = GetReportsRequest.ROUTE_ALL_ALT_NAME)] + [ActionName(nameof(GetReportsAll))] + public IActionResult GetReportsAll(GetReportsRequest input) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace($"{nameof(GetReportsAll)}: {GetReportsRequest.VERB}"); + + if (ModelState.IsValid) + { + _logger.LogDebug($"AgentId=[{input.AgentId}]"); + var sr = _dscHandler.GetReports(input.AgentId.Value, null); + + return this.Model(new GetReportsAllResponse + { + Body = new Model.GetReportsAllResponseBody + { + Value = sr, + }, + }); + } + + return BadRequest(ModelState); + } + } +} diff --git a/src/TugDSC.Server.WebAppHost/Program.cs b/src/TugDSC.Server.WebAppHost/Program.cs new file mode 100644 index 0000000..3f5286e --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Program.cs @@ -0,0 +1,341 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Server.WebAppHost +{ + /// + /// Main entry point for ASP.NET Core MVC-based Tug DSC server. + /// More details about ASP.NET Core hosting can be found here: + /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/hosting + /// + /// + /// The entry point builds and configures a Web Host for the + /// Tug DSC server. + /// + /// There are two sets of configurations that drive behavior of the server. The hosting + /// configuration is used to setup the Web Host directly and also influences behavior before + /// startup and during the early phase of startup. The application configuration is used + /// to drive the normal runtime behavior of the late phase of startup and runtime operation + /// after startup. + /// + /// + /// TODO: Provide more details about configuration + /// + /// + public class Program + { + #region -- Constants -- + + /// + /// File name of an optional JSON file used to configure the Web Host. + /// + public const string HOST_CONFIG_FILENAME = "hosting.json"; + + /// + /// Prefix used to identify environment variables that can override Web Host + /// configuration. + /// + public const string HOST_CONFIG_ENV_PREFIX = "TUG_HOST_"; + + public const string HOST_CONFIG_CLI_PREFIX = "/h:"; + + public const string SKIP_BANNER_CLI_ARG = "--skip-banner"; + public const string SKIP_DIAGNOSTICS_CLI_ARG = "--skip-diag"; + public const string RUN_AS_SERVICE_CLI_ARG = "--service"; + + #endregion -- Constants -- + + #region -- Fields -- + + // Always points to the current logger for this class. + // Upon construction, we initialize this to a temporary pre-logger + // that is hard-coded to simply write to the console but eventually we replace + // this with a logger that is manufactured according to configuration specs. + protected static ILogger _logger; + + // Defines the hard-coded default configuration settings for the WebHostBuilder + // these can be overridden via env vars and CLI switches or by IIS Integration + private static IDictionary _hostingDefaultSettings = + new Dictionary + { + // These default settings can be overridden by using environment + // variables prefixed with 'TUG-HOST_' or by specifing on the CLI + // e.g. --urls "http://*:4321/" + // The list of host settings can be found here: + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/hosting#configuring-a-host + + ["applicationName"] = "TugDSC-Server-WebAppHost", + ["environment"] = "PRODUCTION", + ["captureStartupErrors"] = false.ToString(), + ["contentRoot"] = Directory.GetCurrentDirectory(), + ["detailedErrors"] = false.ToString(), + ["urls"] = "http://*:5000", // "http://localhost:5080;https://localhost:5443" + + }; + + #endregion -- Fields -- + + #region -- Properties -- + + /// + /// Provides app-wide access to the runtime CLI args. + /// + /// + /// This is an unfortunate kludge because we could not find a clean way to + /// make this accessible to app components using the DI mechanism. + /// + public static IEnumerable CommandLineArgs + { get; private set; } + + public static bool SkipBanner + { get; set; } = false; + + public static bool SkipDiagnostics + { get; set; } = false; + + public static bool RunAsService + { get; set; } = false; + + /// + /// If true, will print to standard out all resolved environment + /// variables upon startup. + /// + /// + public static bool DumpEnvironment + { get; set; } = false; + + public static IConfiguration HostingConfig + { get; private set; } + + protected static IWebHost WebHost + { get; private set; } + + #endregion -- Properties -- + + #region -- Constructors -- + + static Program() + { + // Setup a pre-logger to have a place to write out diagnostics and errors until + // we have a chance to properly setup the final runtime logging configuration + _logger = StartupLogger.CreateLogger(); + _logger.LogInformation("********** Commencing STARTUP LOGGING **********"); + } + + #endregion -- Constructors -- + + #region -- Methods -- + + public static void Main(string[] args) + { + // This is ugly as hell but unfortunately, we could not find another + // way to pass this along from here to other parts of the app via DI + CommandLineArgs = args; + + // Parse out some quick CLI flags + SkipBanner = CommandLineArgs.Contains(SKIP_BANNER_CLI_ARG); + SkipDiagnostics = CommandLineArgs.Contains(SKIP_DIAGNOSTICS_CLI_ARG); + RunAsService = CommandLineArgs.Contains(RUN_AS_SERVICE_CLI_ARG); + + PrintBanner(); + + DumpDiagnostics(); + + _logger.LogInformation("Resolving hosting configuration"); + HostingConfig = ResolveHostingConfig(args); + + _logger.LogInformation("Building Web Host"); + WebHost = BuildWebHost(HostingConfig); + + Run(WebHost); + } + + protected static void PrintBanner() + { + if (SkipBanner) + return; + + var asm = typeof(Program).Assembly; + var asmName = asm.GetName(); + var asmVers = asmName.Version; + var asmInfo = FileVersionInfo.GetVersionInfo(asm.Location); + + // The copyright rune may not print so well on console + var copyright = asmInfo.LegalCopyright?.Replace("©", "(C)"); + + Console.WriteLine($"TugDSC Server WebAppHost v{asmVers} -- starting up"); + //Console.WriteLine(asmInfo.ProductName); + Console.WriteLine(copyright); + Console.WriteLine(); + } + + protected static void DumpDiagnostics() + { + if (SkipDiagnostics) + return; + + Console.WriteLine(); + Console.WriteLine($" .NET Platform = [{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}]"); + Console.WriteLine($" * Runtime Dir = [{System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()}]"); + Console.WriteLine($" * ........CWD = [{Directory.GetCurrentDirectory()}]"); + Console.WriteLine($" * ....CmdLine = [{System.Environment.CommandLine}]"); + Console.WriteLine($" * ....Is64bit = [{System.Environment.Is64BitProcess}]"); + Console.WriteLine($" * .....ClrVer = [{System.Environment.Version}]"); + Console.WriteLine($" * ......OsVer = [{System.Environment.OSVersion}]"); + Console.WriteLine($" * ...UserName = [{System.Environment.UserName}]"); + Console.WriteLine($" * ...Hostname = [{System.Environment.MachineName}]"); + Console.WriteLine(); + + // Export some "runtime" meta data about our server which + // may be referenced by other parts of the system, such as + // logger output file paths or config input file paths + System.Environment.SetEnvironmentVariable("TUG_RT_STARTDIR", Directory.GetCurrentDirectory()); +#if DOTNET_FRAMEWORK + System.Environment.SetEnvironmentVariable("TUG_RT_DOTNETFW", "NETFRAMEWORK"); +#else + System.Environment.SetEnvironmentVariable("TUG_RT_DOTNETFW", "NETCORE"); +#endif + + if (DumpEnvironment) + { + // Useful for debugging and diagnostics + Console.WriteLine($" * Environment:"); + var envKeys = System.Environment.GetEnvironmentVariables() + .Keys.Cast().OrderBy(x => x); + foreach (var e in envKeys) + { + Console.WriteLine($" o [{e}]=[{System.Environment.GetEnvironmentVariable((string)e)}]"); + } + Console.WriteLine(); + } + } + + protected static IConfiguration ResolveHostingConfig(string[] args) + { + // var config = new ConfigurationBuilder() + // .AddCommandLine(args) + // .AddEnvironmentVariables(prefix: "ASPNETCORE_") + // .Build(); + + var configArgs = args.Where(x => x.StartsWith(HOST_CONFIG_CLI_PREFIX)) + .Select(x => x.Substring(HOST_CONFIG_CLI_PREFIX.Length)) + .ToArray(); + + var basePath = Directory.GetCurrentDirectory(); + if (RunAsService) + basePath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + + var hostingConfigBuilder = new ConfigurationBuilder() + // Base path for any file-based config sources + .SetBasePath(basePath) + // Default to a set of pre-defined default settings + .AddInMemoryCollection(_hostingDefaultSettings) + // Allow to optionally override with a JSON file + .AddJsonFile(HOST_CONFIG_FILENAME, optional: true) + // Allow to optionally override with env vars + .AddEnvironmentVariables(prefix: HOST_CONFIG_ENV_PREFIX) + // Allow to optionally override with CLI args + .AddCommandLine(configArgs); + + return hostingConfigBuilder.Build(); + } + + protected static IWebHost BuildWebHost(IConfiguration hostingConfig) + { + var hostBuilder = new WebHostBuilder() + .UseConfiguration(hostingConfig) + .ConfigureLogging((builderContext, loggingBuilder) => + { + loggingBuilder.AddConsole(); + }) + // Use Kestrel as a web server + .UseKestrel() + // this must come after UseConfiguration because it + // overrides several settings such as port, base path + // and useStartupErrors config settings + .UseIISIntegration() + .UseStartup(); + + return hostBuilder.Build(); + } + + protected static void Run(IWebHost webHost) + { +#if DOTNET_FRAMEWORK + if (RunAsService) + { + _logger.LogInformation("Running as Windows Service"); + TugWebHostService.RunAsService(webHost); + return; + } +#endif // DOTNET_FRAMEWORK + + if (RunAsService) throw new NotImplementedException( + "RunAsService is currently only implemented for .NET Framework on Windows"); + + webHost.Run(); + } + + #endregion -- Methods -- + } + +#if DOTNET_FRAMEWORK + /// Implements support for running as WinService. + /// + /// More details can be found here: + /// https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/windows-service + internal class TugWebHostService : Microsoft.AspNetCore.Hosting.WindowsServices.WebHostService + { + private ILogger _logger; + + public TugWebHostService(IWebHost host) : base(host) + { + var lf = (ILoggerFactory)host.Services.GetService(typeof(ILoggerFactory)); + _logger = lf.CreateLogger(); + _logger.LogInformation("WebHostService created"); + } + + public static IWebHost RunAsService(IWebHost webHost) + { + Run(new TugWebHostService(webHost)); + return webHost; + } + + protected override void OnStarting(string[] args) + { + _logger.LogInformation("Received 'START' request"); + base.OnStarting(args); + } + + protected override void OnStarted() + { + base.OnStarted(); + _logger.LogInformation("Service started."); + } + + protected override void OnStopping() + { + _logger.LogInformation("Received 'STOP' request"); + base.OnStopping(); + } + + protected override void OnStopped() + { + base.OnStopped(); + _logger.LogInformation("Service stopped."); + } + } +#endif // DOTNET_FRAMEWORK + +} diff --git a/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandler.cs b/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandler.cs new file mode 100644 index 0000000..831e499 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandler.cs @@ -0,0 +1,371 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using TugDSC.Model; +using TugDSC.Server.Util; + +namespace TugDSC.Server.Providers +{ + /// + /// Implements a very basic, file-base Pull Server Handler that + /// aligns closely with the default behavior of the xDscWebService. + /// + public class BasicDscHandler : IDscHandler + { + public const string DEFAULT_WORK_FOLDER = "DscService"; + + // These parameters, names and default values, are based on + // the corresponding appSettings defined for the xDscWebService + // Pull Server out of xPSDesiredStateConfiguration + + public ILogger Logger + { get; set; } + + public ChecksumHelper ChecksumHelper + { get; set; } + + public string RegistrationSavePath + { get; set; } = $"{DEFAULT_WORK_FOLDER}\\Registrations"; + + public string ConfigurationPath + { get; set; } = $"{DEFAULT_WORK_FOLDER}\\Configuration"; + + public string ModulePath + { get; set; } = $"{DEFAULT_WORK_FOLDER}\\Modules"; + + public string ReportsPath + { get; set; } = $"{DEFAULT_WORK_FOLDER}\\Reports"; + + public bool IsDisposed + { get; private set; } + + public virtual void Init() + { + Assert(Logger != null, "missing logger"); + Assert(ChecksumHelper != null, "missing checksum helper"); + Assert(!string.IsNullOrWhiteSpace(RegistrationSavePath), + "registration save path not set"); + Assert(!string.IsNullOrWhiteSpace(ConfigurationPath), + "configuration path not set"); + Assert(!string.IsNullOrWhiteSpace(ModulePath), + "module path not set"); + + Logger.LogInformation("All Assertions Passed!"); + Directory.CreateDirectory(RegistrationSavePath); + Directory.CreateDirectory(ConfigurationPath); + Directory.CreateDirectory(ModulePath); + Directory.CreateDirectory(ReportsPath); + Logger.LogInformation("All Directories Created/Confirmed"); + } + + protected virtual void Assert(bool value, string failMessage = null) + { + if (!value) + if (string.IsNullOrEmpty(failMessage)) + throw new Exception("failed assertion"); + else + throw new Exception(); // ($"failed assertion: {message}"); + } + + public virtual void RegisterDscAgent(Guid agentId, + RegisterDscAgentRequestBody detail) + { + var regPath = Path.Combine(RegistrationSavePath, $"{agentId}.json"); + if (File.Exists(regPath)) + { + // TODO: Do nothing? Does the protocol allow unlimited re-registrations? + //throw new Exception("agent ID already registered"); + } + + File.WriteAllText(regPath, JsonConvert.SerializeObject(detail)); + } + + public virtual ActionStatus GetDscAction(Guid agentId, + GetDscActionRequestBody detail) + { + var regPath = Path.Combine(RegistrationSavePath, $"{agentId}.json"); + if (!File.Exists(regPath)) + throw new InvalidOperationException("unknown agent ID"); + + var regDetail = JsonConvert.DeserializeObject( + File.ReadAllText(regPath)); + var configCount = (regDetail.ConfigurationNames?.Length).GetValueOrDefault(); + Logger.LogDebug($"regDetail[{JsonConvert.SerializeObject(regDetail)}]"); + + DscActionStatus nodeStatus = DscActionStatus.OK; + var list = new List(); + + if (configCount == 0) + { + // Nothing to do since we don't know what config name to provide; + // the xDscWebService-compatible behavior is to just return an OK + nodeStatus = DscActionStatus.OK; + Logger.LogWarning($"No configuration names specified during registration for AgentId=[{agentId}]"); + } + else if (configCount == 1) + { + var cn = regDetail.ConfigurationNames[0]; + + // This is the scenario of a single (default) + // named configuration tied to the node + if (detail.ClientStatus?.Length == 1 + && (string.IsNullOrEmpty(detail.ClientStatus[0].ConfigurationName) + || detail.ClientStatus[0].ConfigurationName == cn)) + { + var cs = detail.ClientStatus[0]; + + // Checksum is for the single default configuration of this node + var configPath = Path.Combine(ConfigurationPath, $"SHARED/{cn}.mof"); + if (!File.Exists(configPath)) + { + Logger.LogWarning($"unable to find ConfigurationName=[{cn}] for AgentId=[{agentId}]"); + return null; + } + + + // Assume we have to pull + nodeStatus = DscActionStatus.GetConfiguration; + var dtlItem = new ActionDetailsItem + { + ConfigurationName = cn, + Status = nodeStatus, + }; + list.Add(dtlItem); + + if (!string.IsNullOrEmpty(cs.Checksum)) // Empty Checksum on the first pull + { + using (var csum = ChecksumHelper.GetAlgorithm()) + { + // Make sure we're on the same algorithm and then compute + if (csum.AlgorithmName == cs.ChecksumAlgorithm + && !string.IsNullOrEmpty(cs.Checksum)) + { + using (var fs = File.OpenRead(configPath)) + { + var csumCsum = csum.ComputeChecksum(fs); + if (csumCsum == cs.Checksum) + { + // We've successfully passed all the checks, nothing to do + nodeStatus = DscActionStatus.OK; + dtlItem.Status = nodeStatus; + } + else + { + Logger.LogDebug($"Checksum mismatch " + + "[{csumCsum}]!=[{cs.Checksum}]" + + "for AgentId=[{agentId}]"); + } + } + } + else + { + Logger.LogWarning($"Checksum Algorithm mismatch " + + "[{csum.AlgorithmName}]!=[{cs.ChecksumAlgorithm}] " + + "for AgentId=[{agentId}]"); + } + } + } + else + { + Logger.LogDebug($"First time pull check for AgentId=[{agentId}]"); + } + } + else + { + throw new NotImplementedException("only single/default configuration names are implemented"); + } + } + else + { + Logger.LogWarning($"Found [{regDetail.ConfigurationNames.Length}] config names: {regDetail.ConfigurationNames}"); + throw new NotImplementedException("multiple configuration names are not implemented"); + + // foreach (var cn in regDetail.ConfigurationNames) + // { + // var configPath = Path.Combine(ConfigurationPath, $"SHARED/{cn}.mof"); + // if (!File.Exists(configPath)) + // throw new InvalidOperationException($"missing configuration by name [{cn}]"); + + // using (var csum = ChecksumHelper.GetAlgorithm()) + // { + // if (csum.AlgorithmName != cs.ChecksumAlgorithm) + // { + // Logger.LogError("Checksum Algorithm mismatch!"); + // } + // else + // { + // using (var fs = File.OpenRead(configPath)) + // { + // if (csum.ComputeChecksum(fs) == cs.Checksum) + // continue; + // } + // } + // } + // } + } + /* + foreach (var cs in detail.ClientStatus) + { + if (string.IsNullOrEmpty(cs.ConfigurationName)) + { + var configPath = Path.Combine(ConfigurationPath, $"{agentId}/{agentId}.mof"); + if (!File.Exists(configPath)) + { + nodeStatus = DscActionStatus.RETRY; + list.Add(new GetDscActionResponseBody.DetailsItem + { + Status = DscActionStatus.RETRY, + }); + } + else + { + using (var csum = ChecksumHelper.GetAlgorithm()) + { + if (csum.AlgorithmName != cs.ChecksumAlgorithm) + { + Logger.LogError("Checksum Algorithm mismatch!"); + } + else + { + using (var fs = File.OpenRead(configPath)) + { + if (csum.ComputeChecksum(fs) == cs.Checksum) + continue; + } + } + } + + nodeStatus = DscActionStatus.GetConfiguration; + list.Add(new GetDscActionResponseBody.DetailsItem + { + Status = DscActionStatus.GetConfiguration, + }); + } + } + } + */ + return new ActionStatus + { + NodeStatus = nodeStatus, + ConfigurationStatuses = list.ToArray(), + }; + } + + public virtual FileContent GetConfiguration(Guid agentId, string configName) + { + var configPath = Path.Combine(ConfigurationPath, $"SHARED/{configName}.mof"); + if (!File.Exists(configPath)) + { + Logger.LogWarning($"unable to find ConfigurationName=[{configName}] for AgentId=[{agentId}]"); + return null; + } + + // TODO: Clean this up for performance with caching and stuff + using (var cs = ChecksumHelper.GetAlgorithm()) + using (var fs = File.OpenRead(configPath)) + { + return new FileContent + { + ChecksumAlgorithm = cs.AlgorithmName, + Checksum = cs.ComputeChecksum(fs), + Content = (Stream)File.OpenRead(configPath), + }; + } + } + + public virtual FileContent GetModule(Guid? agentId, string moduleName, string moduleVersion) + { + var modulePath = Path.Combine(ModulePath, $"{moduleName}/{moduleVersion}.zip"); + if (!File.Exists(modulePath)) + return null; + + // TODO: Clean this up for performance with caching and stuff + using (var cs = ChecksumHelper.GetAlgorithm()) + using (var fs = File.OpenRead(modulePath)) + { + return new FileContent + { + ChecksumAlgorithm = cs.AlgorithmName, + Checksum = cs.ComputeChecksum(fs), + Content = (Stream)File.OpenRead(modulePath), + }; + } + } + + public virtual void SendReport(Guid agentId, SendReportBody report) + { + var reportsDir = Path.Combine(ReportsPath, agentId.ToString()); + var reportPath = Path.Combine(ReportsPath, $"{agentId}/{report.JobId}.json"); + + Directory.CreateDirectory(reportsDir); + File.WriteAllText(reportPath, JsonConvert.SerializeObject(report)); + } + + public virtual IEnumerable GetReports(Guid agentId, Guid? jobId) + { + var reportsDir = Path.Combine(ReportsPath, agentId.ToString()); + var reportPath = Path.Combine(ReportsPath, $"{agentId}/{jobId}.json"); + + if (jobId != null) + { + if (!File.Exists(reportPath)) + return null; + + return new[] { JsonConvert.DeserializeObject( + File.ReadAllText(reportPath)) }; + } + else + { + if (!Directory.Exists(reportsDir)) + return null; + + return Directory.EnumerateFiles(reportsDir).Select(x => + JsonConvert.DeserializeObject(File.ReadAllText(x))); + } + } + + + #region -- IDisposable Support -- + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + IsDisposed = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~BasicDscHandler() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + + #endregion -- IDisposable Support -- + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandlerProvider.cs b/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandlerProvider.cs new file mode 100644 index 0000000..3de04f1 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Providers/BasicDscHandlerProvider.cs @@ -0,0 +1,99 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.Logging; +using TugDSC.Ext; +using TugDSC.Server.Util; + +namespace TugDSC.Server.Providers +{ + public class BasicDscHandlerProvider : IDscHandlerProvider + { + protected static ProviderInfo INFO = new ProviderInfo("basic"); + + protected static readonly IEnumerable PARAMS = new[] + { + new ProviderParameterInfo(nameof(BasicDscHandler.RegistrationSavePath)), + new ProviderParameterInfo(nameof(BasicDscHandler.ConfigurationPath)), + new ProviderParameterInfo(nameof(BasicDscHandler.ModulePath)), + new ProviderParameterInfo(nameof(BasicDscHandler.ReportsPath)), + }; + + protected ILogger _pLogger; + protected ILogger _hLogger; + protected ChecksumHelper _checksumHelper; + + protected IDictionary _productParams; + + protected BasicDscHandler _handler; + + public BasicDscHandlerProvider( + ILogger pLogger, + ILogger hLogger, + ChecksumAlgorithmManager checksumManager, + ChecksumHelper checksumHelper) + { + _pLogger = pLogger; + _hLogger = hLogger; + _checksumHelper = checksumHelper; + + _pLogger.LogInformation("Provider Created"); + } + + public virtual ProviderInfo Describe() => INFO; + + public virtual IEnumerable DescribeParameters() => PARAMS; + + public virtual void SetParameters(IDictionary productParams) + { + _productParams = productParams; + } + + public virtual IDscHandler Produce() + { + _pLogger.LogDebug("Resolving Handler"); + if (_handler == null) + { + lock (this) + { + if (_handler == null) + { + _pLogger.LogInformation("Building global Handler instance"); + + _handler = ConstructHandler(); + _handler.Logger = _hLogger; + _handler.ChecksumHelper = _checksumHelper; + + if (_productParams != null) + { + foreach (var p in PARAMS) + { + if (_productParams.ContainsKey(p.Name)) + { + _pLogger.LogInformation($" * Setting init param: [{p.Name}]"); + typeof(BasicDscHandler).GetTypeInfo() + .GetProperty(p.Name, BindingFlags.Public + | BindingFlags.Instance) + .SetValue(_handler, _productParams[p.Name]); + } + } + } + + _handler.Init(); + } + } + } + + return _handler; + } + + protected virtual BasicDscHandler ConstructHandler() + { + return new BasicDscHandler(); + } + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithm.cs b/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithm.cs new file mode 100644 index 0000000..12d45c4 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithm.cs @@ -0,0 +1,82 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using TugDSC.Ext; + +namespace TugDSC.Providers +{ + public class Sha256ChecksumAlgorithm : IChecksumAlgorithm + { + private SHA256 _sha256; + + public Sha256ChecksumAlgorithm() + { + _sha256 = SHA256.Create(); + } + + public string AlgorithmName + { get; } = Sha256ChecksumAlgorithmProvider.PROVIDER_NAME; + + public bool IsDisposed + { get; private set; } + + public string ComputeChecksum(Stream stream) + { + return Escape(_sha256.ComputeHash(stream)); + } + + public string ComputeChecksum(byte[] bytes) + { + return Escape(_sha256.ComputeHash(bytes)); + } + + public static string Escape(byte[] bytes) + { + return BitConverter.ToString(bytes).Replace("-", ""); + } + + #region -- IDisposable Support -- + + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + _sha256.Dispose(); + _sha256 = null; + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + IsDisposed = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~Sha256ChecksumAlgorithm() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + + #endregion -- IDisposable Support -- + + } +} diff --git a/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithmProvider.cs b/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithmProvider.cs new file mode 100644 index 0000000..b3832fe --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Providers/Sha256ChecksumAlgorithmProvider.cs @@ -0,0 +1,38 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licnesed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using TugDSC.Ext; + +namespace TugDSC.Providers +{ + public class Sha256ChecksumAlgorithmProvider : IChecksumAlgorithmProvider + { + public const string PROVIDER_NAME = "SHA-256"; + + private static readonly ProviderInfo INFO = new ProviderInfo(PROVIDER_NAME); + + private static readonly ProviderParameterInfo[] PARAMS = new ProviderParameterInfo[0]; + + private IDictionary _productParams; + + public ProviderInfo Describe() => INFO; + + public IEnumerable DescribeParameters() => PARAMS; + + public void SetParameters(IDictionary productParams = null) + { + _productParams = productParams; + } + + public IChecksumAlgorithm Produce() + { + return new Sha256ChecksumAlgorithm(); + } + } +} diff --git a/src/TugDSC.Server.WebAppHost/README.md b/src/TugDSC.Server.WebAppHost/README.md new file mode 100644 index 0000000..0dc4f99 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/README.md @@ -0,0 +1,26 @@ +# README - TugDSC Server Web App Host + +This is an implementation of a DSC Pull Server implemented as a ASP.NET Core MVC Web App. + +## Running as a Windows Service + +You can run this TugDSC Server implementation as a Windows Service if you following these +guidelines. + +* First, you must be running this TugDSC Server on the .NET Framework 4.6.1 or greater, + so be sure that it's installed on the target host and be sure to run the version of + the TugDSC Server application that has been compiled to target that platform. +* Next, be sure the required `appsettings.json` file is located in the same path as the + binary which will be executed (`TugDSC.Server.WebAppHost.exe`). Additionally, if you + want to provide an optional hosting configuration file (`hosting.json`), it too must + be located in the same path as the binary executable. +* Finally, you need to install the application as a Windows Service that gets invoked with the + `--service` CLI flag, for example: +```batch +sc.exe create TugDSC binPath= "\"c:\path\to\binary\TugDSC.Server.WebAppHost.exe\" --service " +sc.exe start TugDSC +``` + +> NOTE: The above example is assumed to be executed from a Windows command shell (cmd.exe). +> If executing the same set of commands from PowerShell, make sure the use the appropriate +> method of escaping within quoted string (the backtick). diff --git a/src/TugDSC.Server.WebAppHost/Startup.cs b/src/TugDSC.Server.WebAppHost/Startup.cs new file mode 100644 index 0000000..4c299dd --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/Startup.cs @@ -0,0 +1,246 @@ +/* + * Copyright © The DevOps Collective, Inc. All rights reserved. + * Licensed under GNU GPL v3. See top-level LICENSE.txt for more details. + */ + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Converters; +// using NLog.Extensions.Logging; +// using NLog.Web; +using TugDSC.Server.Configuration; +using TugDSC.Server.Filters; +using TugDSC.Server.Util; + +namespace TugDSC.Server.WebAppHost +{ + public class Startup + { + #region -- Constants -- + + /// + /// File name of a required JSON file used to configure the server app. + /// + public const string APP_CONFIG_FILENAME = "appsettings.json"; + + /// + /// File name of an optional JSON file used to override server app configuration. + /// + public const string APP_USER_CONFIG_FILENAME = "appsettings.user.json"; + + /// + /// Prefix used to identify environment variables that can override server app + /// configuration. + /// + public const string APP_CONFIG_ENV_PREFIX = "TUG_CFG_"; + + public const string APP_CONFIG_CLI_PREFIX = "/c:"; + + #endregion -- Constants -- + + #region -- Fields -- + + protected ILogger _logger; + + protected IConfiguration _config; + + #endregion -- Fields -- + + #region -- Constructors -- + + public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory) + { + // Start with a pre-logger till the final + // logging config is finalized down below + _logger = StartupLogger.CreateLogger(); + + // This is ugly as hell but unfortunately, we could not find another + // way to pass this along from Program to other parts of the app via DI + var args = Program.CommandLineArgs?.ToArray(); + + _logger.LogInformation("Resolving final runtime configuration"); + _config = ResolveAppConfig(args); + + // TODO: We may need to adjust based on changes in .NET 2.0 + ConfigureLogging(env, loggerFactory); + } + + #endregion -- Constructors -- + + #region -- Methods -- + + // This method gets called by the runtime. Use this + // method to add services to the container. For more + // information on how to configure your application, + // visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + _logger.LogInformation("Configuring services registry"); + + // Enable and bind to strongly-typed configuration + var appSettings = _config.GetSection(nameof(AppSettings)); + services.AddSingleton(appSettings); + services.AddOptions(); + services.Configure(appSettings); + services.Configure( + appSettings.GetSection(nameof(AppSettings.Checksum))); + services.Configure( + appSettings.GetSection(nameof(AppSettings.Authz))); + services.Configure( + appSettings.GetSection(nameof(AppSettings.Handler))); + + // Register a single instance of each filter type we'll use down below + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Add MVC-supporting services + _logger.LogInformation("Adding MVC services"); + services.AddMvc(options => + { + // Add the filter by service type reference + options.Filters.AddService(typeof(DscRegKeyAuthzFilter)); + options.Filters.AddService(typeof(VeryStrictInputFilter)); + }).AddJsonOptions(options => + { + // This enables converting Enums to/from their string names instead + // of their numerical value, based on: + // * https://www.exceptionnotfound.net/serializing-enumerations-in-asp-net-web-api/ + // * https://siderite.blogspot.com/2016/10/controlling-json-serialization-in-net.html + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + }); + + + // Register the Provider Managers + services.AddSingleton(); + services.AddSingleton(); + + // Register the Helpers + services.AddSingleton(); + services.AddSingleton(); + } + + // This method gets called by the runtime. Use this + // method to configure the HTTP request pipeline. + public void Configure(IServiceProvider serviceProvider, + IApplicationBuilder app, IHostingEnvironment env) + { + // set development option + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + // // begin registering routes for incoming requests + // var routeBuilder = new RouteBuilder(app); + app.UseMvc(routeBuilder => + { + + // Default route welcome message + routeBuilder.MapGet("", context => + { + return context.Response.WriteAsync(@" +

Welcome to TugDSC!

+
  • Version Info
  • +"); + }); + + // Server version info + routeBuilder.MapGet("version", context => + { + var version = GetType().GetTypeInfo().Assembly.GetName().Version; + return context.Response.WriteAsync($@"{{""version"":""{version}""}}"); + }); + }); + + // Resolve some DI classes to make sure they're ready to go when needed and + // forces any possible resolution errors to invoke earlier rather than later + serviceProvider.GetRequiredService(); + serviceProvider.GetRequiredService(); + } + + protected IConfiguration ResolveAppConfig(string[] args = null) + { + var basePath = Directory.GetCurrentDirectory(); + if (Program.RunAsService) + basePath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + + // Resolve the runtime configuration settings + var appConfigBuilder = new ConfigurationBuilder(); + // Base path for any file-based config sources + appConfigBuilder.SetBasePath(basePath); + // Default location for all configuration settings + appConfigBuilder.AddJsonFile(APP_CONFIG_FILENAME, optional: false); + // Optional location for user-specific local overrides + appConfigBuilder.AddJsonFile(APP_USER_CONFIG_FILENAME, optional: true); + // Allows overriding any setting using envVars that being with TUG_CFG_ + appConfigBuilder.AddEnvironmentVariables(prefix: APP_CONFIG_ENV_PREFIX); + // A good place to store secrets for dev/test + appConfigBuilder.AddUserSecrets(); + + if (args != null) + { + var configArgs = args.Where(x => x.StartsWith(APP_CONFIG_CLI_PREFIX)) + .Select(x => x.Substring(APP_CONFIG_CLI_PREFIX.Length)).ToArray(); + // Last but not least, allow overriding with CLI arguments + appConfigBuilder.AddCommandLine(configArgs); + } + + return appConfigBuilder.Build(); + } + + protected void ConfigureLogging(IHostingEnvironment env, ILoggerFactory loggerFactory) + { + var logSettings = _config + ?.GetSection(nameof(LogSettings)) + ?.Get(); + + _logger.LogInformation("Applying logging configuration"); + + if (logSettings != null) + { + if (logSettings.LogType.HasFlag(LogType.Console)) { + _logger.LogInformation(" * enabling Console Logging"); + if (logSettings.DebugLog) { + loggerFactory.AddConsole(LogLevel.Debug); + } else { + loggerFactory.AddConsole(LogLevel.Information); + } + } + + // TODO: Resolve which logger to use + // if (logSettings.LogType.HasFlag(LogType.NLog)) { + // var configPath = Path.Combine(Directory.GetCurrentDirectory(), "nlog.config"); + // _logger.LogInformation($" * enabling NLog with config=[{configPath}]"); + // loggerFactory.AddNLog(); + // env.ConfigureNLog(configPath); + // } + } + + // TODO: We may need to adjust based on changes in .NET 2.0 + + // Initiate and switch to runtime logging + _logger.LogInformation("Instantiating runtime logging"); + _logger.LogInformation("********** Ceasing STARTUP LOGGING **********"); + _logger.LogInformation(""); + + + _logger = loggerFactory.CreateLogger(); + _logger.LogInformation("********** Commencing RUNTIME LOGGING **********"); + } + + #endregion -- Methods -- + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/StartupLogger.cs b/src/TugDSC.Server.WebAppHost/StartupLogger.cs new file mode 100644 index 0000000..9b1d784 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/StartupLogger.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace TugDSC.Server.WebAppHost +{ + /// This helper class allows us to access the logging subsystem + /// before runtime logging is fully configured. It sets up some + /// hard-coded defaults with support for some environment-based + /// configuration to control its behavior. + public static class StartupLogger + { + /// An optional environment variable that points to a full file + /// path that we load to pass on to the Console logging provider. + public const string STARTUP_LOG_CONFIG = "TUG_STARTUP_LOG_CONFIG"; + + private static LoggerFactory _startupLoggerFactory; + + static StartupLogger() + { + var cfgFile = System.Environment.GetEnvironmentVariable(STARTUP_LOG_CONFIG); + + _startupLoggerFactory = new LoggerFactory(); + + if (!string.IsNullOrEmpty(cfgFile)) + { + var cfg = new ConfigurationBuilder() + .AddJsonFile(cfgFile, optional: false) + .Build(); + _startupLoggerFactory.AddConsole(cfg); + } + else + { + _startupLoggerFactory.AddConsole(); + } + } + + public static ILogger CreateLogger(string logName) => + _startupLoggerFactory.CreateLogger(logName); + + public static ILogger CreateLogger() => + _startupLoggerFactory.CreateLogger(); + } +} \ No newline at end of file diff --git a/src/TugDSC.Server.WebAppHost/TugDSC.Server.WebAppHost.csproj b/src/TugDSC.Server.WebAppHost/TugDSC.Server.WebAppHost.csproj new file mode 100644 index 0000000..bed1de6 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/TugDSC.Server.WebAppHost.csproj @@ -0,0 +1,59 @@ + + + + + + + netcoreapp2.0;net461 + + + + TugDSC Server WebHost + ASP.NET Core Hosting & Startup + Tug.Server + + + + + + + + $(DefineConstants);DOTNET_CORE + + + $(DefineConstants);DOTNET_FRAMEWORK + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TugDSC.Server.WebAppHost/appsettings.Development.json b/src/TugDSC.Server.WebAppHost/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/TugDSC.Server.WebAppHost/appsettings.json b/src/TugDSC.Server.WebAppHost/appsettings.json new file mode 100644 index 0000000..37bdc3f --- /dev/null +++ b/src/TugDSC.Server.WebAppHost/appsettings.json @@ -0,0 +1,54 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + }, + + + // "logSettings": { + // "LogType": "nlog", + // "DebugLog": "true" + // }, + "appSettings": { + "checksum": { + "default": "SHA-256" + }, + + "authz": { + "params": { + // This is where we look for the file containing reg keys, + // which is named "RegistrationKeys.txt" by default. In this + // file we look for non-blank lines after removing any comments + // starting with the '#' character and trimming from both ends + "RegistrationKeyPath": "_IGNORE/DscService/Authz", + // We keep our own copy of registrations that is separate from + // those saved by the Pull Handler down below which is strictly + // for authz purposes -- these may overlap but could cause conflicts + "RegistrationSavePath": "_IGNORE/DscService/Authz/Registrations" + } + }, + + // Enable this setup to use the BASIC DSC Handler + "handler": { + // No need for "provider" as it defaults to "basic" + // "provider": "basic", + "params": { + // For testing purposes, we redefine these to make sure they get + // placed under the _IGNORE subfolder so they are ignored by Git + "RegistrationSavePath": "_IGNORE/DscService/Registrations", + "ConfigurationPath": "_IGNORE/DscService/Configuration", + "ModulePath": "_IGNORE/DscService/Modules", + "ReportsPath": "_IGNORE/DscService/Reports" + } + } + } +} \ No newline at end of file diff --git a/src/shared/SharedAssemblyInfo.cs b/src/shared/SharedAssemblyInfo.cs index 6272535..227aeb5 100644 --- a/src/shared/SharedAssemblyInfo.cs +++ b/src/shared/SharedAssemblyInfo.cs @@ -6,6 +6,6 @@ // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyCompany("github.com/PowerShellOrg")] -[assembly: AssemblyProduct("Tug")] -[assembly: AssemblyCopyright("Copyright © The DevOps Collective, Inc. All rights reserved. Licnesed under GNU GPL v3.")] -[assembly: AssemblyTrademark("")] +[assembly: AssemblyProduct("TugDSC")] +[assembly: AssemblyCopyright("Copyright © The DevOps Collective, Inc. All rights reserved. Licensed under GNU GPL v3.")] +[assembly: AssemblyTrademark("TugDSC™")] diff --git a/src/shared/SharedAssemblyVersionInfo.cs b/src/shared/SharedAssemblyVersionInfo.cs index 82ecd4b..a6eb90a 100644 --- a/src/shared/SharedAssemblyVersionInfo.cs +++ b/src/shared/SharedAssemblyVersionInfo.cs @@ -25,5 +25,5 @@ internal static class ASMINFO // DON'T FORGET TO UPDATE APPVEYOR.YML // ReSharper disable once InconsistentNaming - public const string VERSION = "0.6.0"; + public const string VERSION = "0.7.0"; }