diff --git a/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs new file mode 100644 index 0000000000..2e5db108ac --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.RunbooksRun { + [Command("delete-runbookruns", Description = "Deletes a range of runbook runs.")] + public class DeleteRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput + { + List toDelete = new List(); + List wouldDelete = new List(); + DateTime? minDate = null; + DateTime? maxDate = null; + + bool whatIf = false; + + public DeleteRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + var options = Options.For("Deletion"); + options.Add("minCreateDate=", "Earliest (inclusive) create date for the range of runbook runs to delete.", v => minDate = v); + options.Add("maxCreateDate=", "Latest (inclusive) create date for the range of runbooks to delete.", v => maxDate = v); + options.Add("whatIf", "[Optional, Flag] if specified, releases won't actually be deleted, but will be listed as if simulating the command.", v => whatIf = true); + } + + protected override Task ValidateParameters() { + if(!minDate.HasValue) + throw new CommandException("Please specify the earliest (inclusive) create date for the range of runbook runs to delete using the parameter: --minCreateData=2022-01-01"); + if(!maxDate.HasValue) + throw new CommandException("Please specify the latest (inclusive) create date for the range of runbooks to delete using the parameter: --maxCreateDate=2022-01-01"); + + return base.ValidateParameters(); + } + + public override async Task Request() + { + await base.Request(); + commandOutputProvider.Debug($"Finding runbook runs created between {minDate:yyyy-mm-dd} and {maxDate:yyyy-mm-dd} ..."); + + await Repository.RunbookRuns + .Paginate(projectsFilter, + runbooksFilter, + environmentsFilter, + tenantsFilter, + page => { + foreach(var run in page.Items) { + if(run.Created >= minDate.Value && run.Created <= maxDate.Value) { + if(whatIf) { + commandOutputProvider.Information("[WhatIf] Run {RunId:l} created on {CreatedOn:2} would have been deleted", run.Id, run.Created.ToString()); + wouldDelete.Add(run); + } else { + toDelete.Add(run); + commandOutputProvider.Information("Deleting Run {RunId:l} created on {CreatedOn:2}", run.Id, run.Created.ToString()); + } + } + } + return true; // We need to check all runs + }) + .ConfigureAwait(false); + + // Don't do anything else for WhatIf + if (whatIf) return; + + foreach(var run in toDelete) + await Repository.RunbookRuns.Delete(run); + } + + public void PrintDefaultOutput() + { + } + + public void PrintJsonOutput() + { + var affectedRuns = whatIf ? wouldDelete : toDelete; + commandOutputProvider.Json( + affectedRuns.Select(run => new { + Id = run.Id, + Name = run.Name, + Created = run.Created, + Project = new { Id = run.ProjectId, Name = projectsById[run.ProjectId].Name }, + Runbook = new { Id = run.RunbookId, Name = runbooksById[run.RunbookId].Name }, + Environment = new { Id = run.EnvironmentId, Name = environmentsById[run.EnvironmentId].Name }, + Tenant = new { Id = run.TenantId, Name = !string.IsNullOrEmpty(run.TenantId) ? tenantsById[run.TenantId].Name : null }, + FailureEncountered = run.FailureEncountered, + Comments = run.Comments + })); + } + } +} diff --git a/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs new file mode 100644 index 0000000000..c2780dd112 --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octopus.Cli.Model; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.RunbooksRun +{ + [Command("list-runbookruns", Description = "Lists a runbook runs by project and/or runbook")] + public class ListRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput + { + const int DefaultReturnAmount = 30; + int? numberOfResults; + + List runbookRuns = new List(); + + public ListRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + var options = Options.For("Listing"); + options.Add("number=", $"[Optional] number of results to return, default is {DefaultReturnAmount}", v => numberOfResults = v); + } + + public override async Task Request() + { + await base.Request(); + + commandOutputProvider.Debug("Loading runbook runs..."); + + var maxResults = numberOfResults ?? DefaultReturnAmount; + await Repository.RunbookRuns + .Paginate(projectsFilter, + runbooksFilter, + environmentsFilter, + tenantsFilter, + delegate(ResourceCollection page) + { + if (runbookRuns.Count < maxResults) + foreach (var dr in page.Items.Take(maxResults - runbookRuns.Count)) + runbookRuns.Add(dr); + + return true; + }) + .ConfigureAwait(false); + } + + public void PrintDefaultOutput() + { + if (!runbookRuns.Any()) + commandOutputProvider.Information("Did not find any runbook runs matching the search criteria."); + + commandOutputProvider.Debug($"Showing {runbookRuns.Count} results..."); + + foreach(var item in runbookRuns) { + LogrunbookRunInfo(commandOutputProvider, item); + } + + if (numberOfResults.HasValue && numberOfResults != runbookRuns.Count) + commandOutputProvider.Debug($"Please note you asked for {numberOfResults} results, but there were only {runbookRuns.Count} that matched your criteria"); + } + + public void PrintJsonOutput() + { + commandOutputProvider.Json( + runbookRuns.Select(run => new + { + Id = run.Id, + Name = run.Name, + Created = run.Created, + Project = new { Id = run.ProjectId, Name = projectsById[run.ProjectId].Name}, + Runbook = new { Id = run.RunbookId, Name = runbooksById[run.RunbookId].Name}, + Environment = new { Id = run.EnvironmentId, Name = environmentsById[run.EnvironmentId].Name}, + Tenant = new { Id = run.TenantId, Name = !string.IsNullOrEmpty(run.TenantId) ? tenantsById[run.TenantId].Name : null }, + FailureEncountered = run.FailureEncountered, + Comments = run.Comments + })); + } + + private void LogrunbookRunInfo(ICommandOutputProvider outputProvider, + RunbookRunResource runbookRunItem) + { + var nameOfrunbookRunEnvironment = environmentsById[runbookRunItem.EnvironmentId].Name; + var nameOfrunbookRunProject = projectsById[runbookRunItem.ProjectId].Name; + var nameOfrunbook = runbooksById[runbookRunItem.RunbookId].Name; + + outputProvider.Information(" - Id: {Name:1}", runbookRunItem.Id); + outputProvider.Information(" - Name: {Name:1}", runbookRunItem.Name); + outputProvider.Information(" - Project: {Project:l}", nameOfrunbookRunProject); + outputProvider.Information(" - Runbook: {Runbook:l}", nameOfrunbook); + outputProvider.Information(" - Environment: {Environment:l}", nameOfrunbookRunEnvironment); + + if (!string.IsNullOrEmpty(runbookRunItem.TenantId)) + { + var nameOfrunbookRunTenant = tenantsById[runbookRunItem.TenantId].Name; + outputProvider.Information(" - Tenant: {Tenant:l}", nameOfrunbookRunTenant); + } + + outputProvider.Information("\tCreated: {$Date:l}", runbookRunItem.Created); + if (!string.IsNullOrWhiteSpace(runbookRunItem.Comments)) outputProvider.Information("\tComments: {$Comments:l}", runbookRunItem.Comments); + outputProvider.Information("\tFailure Encountered: {FailureEncountered:l}", runbookRunItem.FailureEncountered ? "Yes" : "No"); + + outputProvider.Information(string.Empty); + } + } +} diff --git a/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs b/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs new file mode 100644 index 0000000000..1f613cd26a --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Octopus.Cli.Commands.Runbooks; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.RunbooksRun { + + /// + /// Base class for Runbook related commands with shared logic for all Runbook (and RunbookRun) commands + /// + public abstract class RunbookRunCommandBase : RunbookCommandBase { + protected readonly HashSet environments = new HashSet(StringComparer.OrdinalIgnoreCase); + protected readonly HashSet runbooks = new HashSet(StringComparer.OrdinalIgnoreCase); + protected readonly HashSet tenants = new HashSet(StringComparer.OrdinalIgnoreCase); + protected IDictionary runbooksById; + protected IDictionary environmentsById; + protected IDictionary tenantsById; + + protected string[] runbooksFilter; + protected string[] environmentsFilter; + protected string[] tenantsFilter; + + public RunbookRunCommandBase(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) { + var options = Options.For("Listing"); + options.Add("runbook=", "[Optional] Name of a runbook to filter by. Can be specified many times.", v => runbooks.Add(v), allowsMultiple: true); + options.Add("environment=", "[Optional] Name of an environment to filter by. Can be specified many times.", v => environments.Add(v), allowsMultiple: true); + options.Add("tenant=", "[Optional] Name of a tenant to filter by. Can be specified many times.", v => tenants.Add(v), allowsMultiple: true); + } + + public override async Task Request() { + // Need to run the base implementation first to resolve the projects, which is required by Runbook query. + await base.Request(); + + environmentsById = await LoadEnvironments().ConfigureAwait(false); + environmentsFilter = environmentsById.Any() ? environmentsById.Keys.ToArray() : new string[0]; + runbooksById = await LoadRunbooks().ConfigureAwait(false); + runbooksFilter = runbooksById.Any() ? runbooksById.Keys.ToArray() : new string[0]; + tenantsById = await LoadTenants().ConfigureAwait(false); + tenantsFilter = tenants.Any() ? tenantsById.Keys.ToArray() : new string[0]; + } + + private async Task> LoadEnvironments() { + commandOutputProvider.Information("Loading environments..."); + var environmentQuery = environments.Any() + ? Repository.Environments.FindByNames(environments.ToArray()) + : Repository.Environments.FindAll(); + + var environmentResources = await environmentQuery.ConfigureAwait(false); + + var missingEnvironments = + environments.Except(environmentResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if(missingEnvironments.Any()) + throw new CommandException("Could not find environments: " + string.Join(",", missingEnvironments)); + + return environmentResources.ToDictionary(e => e.Id, e => e); + } + + private async Task> LoadRunbooks() { + commandOutputProvider.Information("Loading runbooks..."); + + Task> runbookQuery; + + if(projectsFilter.Any()) { + runbookQuery = runbooks.Any() + ? Repository.Runbooks.FindMany(rb => projectsFilter.Contains(rb.ProjectId) && runbooks.ToArray().Contains(rb.Name)) + : Repository.Runbooks.FindMany(rb => projectsFilter.Contains(rb.ProjectId)); + } else { + runbookQuery = runbooks.Any() + ? Repository.Runbooks.FindByNames(runbooks.ToArray()) + : Repository.Runbooks.FindAll(); + } + + var runbookResources = await runbookQuery.ConfigureAwait(false); + + var missingRunbooks = + runbooks.Except(runbookResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if(missingRunbooks.Any()) + throw new CommandException("Could not find runbooks: " + string.Join(",", missingRunbooks)); + + return runbookResources.ToDictionary(rb => rb.Id, rb => rb); + } + + private async Task> LoadTenants() { + commandOutputProvider.Information("Loading tenants..."); + + var multiTenancyStatus = await Repository.Tenants.Status().ConfigureAwait(false); + + if(multiTenancyStatus.Enabled) { + var tenantsQuery = tenants.Any() + ? Repository.Tenants.FindByNames(tenants.ToArray()) + : Repository.Tenants.FindAll(); + + var tenantsResources = await tenantsQuery.ConfigureAwait(false); + + var missingTenants = tenants.Except(tenantsResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase).ToArray(); + + if(missingTenants.Any()) + throw new CommandException("Could not find tenants: " + string.Join(",", missingTenants)); + + return tenantsResources.ToDictionary(t => t.Id, t => t); + } else { + return new Dictionary(); + } + } + } +} diff --git a/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs b/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs new file mode 100644 index 0000000000..a6be17a05d --- /dev/null +++ b/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.Runbooks +{ + [Command("list-runbooks", Description = "Lists runbooks by project.")] + public class ListRunbooksCommand : RunbookCommandBase, ISupportFormattedOutput + { + List runbooks; + + public ListRunbooksCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + } + + public override async Task Request() + { + await base.Request(); + + commandOutputProvider.Debug("Loading runbooks..."); + + runbooks = await Repository.Runbooks + .FindMany(x => projectsFilter.Contains(x.ProjectId)) + .ConfigureAwait(false); + } + + public void PrintDefaultOutput() + { + commandOutputProvider.Information("Runbooks: {Count}", runbooks.Count); + foreach (var project in projectsById.Values) + { + commandOutputProvider.Information(" - Project: {Project:l}", project.Name); + + foreach (var runbook in runbooks.Where(x => x.ProjectId == project.Id)) + { + var propertiesToLog = new List(); + propertiesToLog.AddRange(FormatRunbookPropertiesAsStrings(runbook)); + foreach (var property in propertiesToLog) + commandOutputProvider.Information(" {Property:l}", property); + commandOutputProvider.Information(""); + } + } + } + + public void PrintJsonOutput() + { + commandOutputProvider.Json(projectsById.Values.Select(pr => new + { + Project = new { pr.Id, pr.Name }, + Ruunbooks = runbooks.Where(r => r.ProjectId == pr.Id) + .Select(r => new + { + r.Id, + r.Name, + r.Description + }) + })); + } + } +} diff --git a/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs new file mode 100644 index 0000000000..d20c072f3c --- /dev/null +++ b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.Runbooks { + + /// + /// Base class for Runbook related commands with shared logic for all Runbook (and RunbookRun) commands + /// + public abstract class RunbookCommandBase : ApiCommand { + protected readonly HashSet projects = new HashSet(StringComparer.OrdinalIgnoreCase); + protected IDictionary projectsById; + protected string[] projectsFilter; + + public RunbookCommandBase(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) { + var options = Options.For("Listing"); + options.Add("project=", "Name of a project to filter by. Can be specified many times.", v => projects.Add(v), allowsMultiple: true); + } + + protected override Task ValidateParameters() { + if(!projects.Any(p => !string.IsNullOrWhiteSpace(p))) + throw new CommandException("Please specify at least one project name or ID using the parameter: --project=XYZ"); + + return base.ValidateParameters(); + } + + public virtual async Task Request() { + projectsById = await LoadProjects().ConfigureAwait(false); + projectsFilter = projectsById.Any() ? projectsById.Keys.ToArray() : new string[0]; + } + + protected IEnumerable FormatRunbookPropertiesAsStrings(RunbookResource runbook) { + return new string[] + { + "Id: " + runbook.Id, + "Name: " + runbook.Name, + "Description: " + runbook.Description, + }; + } + + private async Task> LoadProjects() { + commandOutputProvider.Information("Loading projects..."); + + var projectResources = await Repository.Projects.FindByNames(projects.ToArray()).ConfigureAwait(false); + + var missingProjects = projects.Except(projectResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase).ToArray(); + + if(missingProjects.Any()) + throw new CommandException("Could not find projects: " + string.Join(",", missingProjects)); + + return projectResources.ToDictionary(p => p.Id, p => p); + } + } +} diff --git a/source/OctopusCli.sln b/source/OctopusCli.sln index d4567f7445..7a31a566f3 100644 --- a/source/OctopusCli.sln +++ b/source/OctopusCli.sln @@ -1,15 +1,16 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.202 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32526.322 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{193F466A-BECE-4809-A369-7CC061C31C82}" ProjectSection(SolutionItems) = preProject - NuGet.Config = NuGet.Config - ..\readme.md = ..\readme.md ..\.gitignore = ..\.gitignore + ..\.github\workflows\build.yml = ..\.github\workflows\build.yml ..\BuildAssets\create-octopuscli-linux-packages.sh = ..\BuildAssets\create-octopuscli-linux-packages.sh + NuGet.Config = NuGet.Config + ..\readme.md = ..\readme.md ..\BuildAssets\test-linux-package.sh = ..\BuildAssets\test-linux-package.sh - ..\.github\workflows\build.yml = ..\.github\workflows\build.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octo", "Octo\Octo.csproj", "{D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}" @@ -21,14 +22,14 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octopus.Cli", "Octopus.Cli\Octopus.Cli.csproj", "{627462FB-2E70-4F9C-ACC9-8CF84BF6C196}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{994AA337-0E3B-44C3-83F8-0E6C5D3AA007}" -ProjectSection(SolutionItems) = preProject - ..\Dockerfiles\Readme.md = ..\Dockerfiles\Readme.md -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\Dockerfiles\Readme.md = ..\Dockerfiles\Readme.md + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Alpine", "Alpine", "{32A3C564-207A-43DB-9588-27AFAF846C1A}" -ProjectSection(SolutionItems) = preProject - ..\Dockerfiles\alpine\Dockerfile = ..\Dockerfiles\alpine\Dockerfile -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\Dockerfiles\alpine\Dockerfile = ..\Dockerfiles\alpine\Dockerfile + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}" EndProject @@ -42,10 +43,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -92,14 +89,22 @@ Global {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|Mixed Platforms.Build.0 = Release|Any CPU {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|x86.ActiveCfg = Release|Any CPU {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|x86.Build.0 = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|x86.Build.0 = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|x86.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FD0E320F-79AC-4489-9508-C62497C629D3} - EndGlobalSection GlobalSection(NestedProjects) = preSolution {32A3C564-207A-43DB-9588-27AFAF846C1A} = {994AA337-0E3B-44C3-83F8-0E6C5D3AA007} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD0E320F-79AC-4489-9508-C62497C629D3} + EndGlobalSection EndGlobal