diff --git a/Program.cs b/Program.cs index ab16702..e1329c7 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ -using AMSMigrate.Ams; +using AMSMigrate.ams; +using AMSMigrate.Ams; using AMSMigrate.Azure; using AMSMigrate.Contracts; using AMSMigrate.Local; @@ -75,6 +76,21 @@ This command forcefully removes the Azure Media Services (AMS) account. await CleanupAsync(context, cleanupOptions, context.GetCancellationToken()); }); + var resetOptionsBinder = new ResetOptionsBinder(); + var resetCommand = resetOptionsBinder.GetCommand("reset", @"Reset assets back to their original NotMigrated state +Examples to reset assets in the AMS account: +reset -s -g -n -c all +This command will forcibly revert all assets in source account to their initial NotMigrated state. By default, this parameter is set to ""all"". +reset -s -g -n -c failed +This command will forcibly revert migrated assets that have failed back to their original NotMigrated state."); + rootCommand.Add(resetCommand); + resetCommand.SetHandler( + async context => + { + var resetOptions =resetOptionsBinder.GetValue(context.BindingContext); + await ResetAsync(context, resetOptions, context.GetCancellationToken()); + }); + // disable storage migrate option until ready /* var storageOptionsBinder = new StorageOptionsBinder(); @@ -223,7 +239,15 @@ static async Task CleanupAsync( await ActivatorUtilities.CreateInstance(provider, cleanupOptions) .MigrateAsync(cancellationToken); } - + static async Task ResetAsync( + InvocationContext context, + ResetOptions resetOptions, + CancellationToken cancellationToken) + { + var provider = context.BindingContext.GetRequiredService(); + await ActivatorUtilities.CreateInstance(provider, resetOptions) + .MigrateAsync(cancellationToken); + } static async Task MigrateKeysAsync( InvocationContext context, diff --git a/ResetOptionsBinder.cs b/ResetOptionsBinder.cs new file mode 100644 index 0000000..f8faeed --- /dev/null +++ b/ResetOptionsBinder.cs @@ -0,0 +1,48 @@ +using AMSMigrate.Contracts; +using FFMpegCore.Enums; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Binding; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AMSMigrate +{ + internal class ResetOptionsBinder : BinderBase + { + private readonly Option _sourceAccount = new Option( + aliases: new[] { "--source-account-name", "-n" }, + description: "Azure Media Services Account.") + { + IsRequired = true, + Arity = ArgumentArity.ExactlyOne + }; + + private readonly Option _category = new Option( + aliases: new[] { "--category", "-c" }, + ()=>"all", + description: "Define two categories: \"all\" and \"failed\". The \"all\" category encompasses a complete reset of all assets within the account, regardless of their migration status. By default, this parameter is set to \"all\". The \"failed\" category exclusively pertains to resetting only those assets that have encountered migration failures, reverting them back to their non-migrated state.") + { + IsRequired = false + }; + public ResetOptions GetValue(BindingContext context) => GetBoundValue(context); + + public Command GetCommand(string name, string description) + { + var command = new Command(name, description); + command.AddOption(_sourceAccount); + command.AddOption(_category); + return command; + } + + protected override ResetOptions GetBoundValue(BindingContext bindingContext) + { + return new ResetOptions( + bindingContext.ParseResult.GetValueForOption(_sourceAccount)!, + bindingContext.ParseResult.GetValueForOption(_category)! + ); + } + } +} diff --git a/ams/AssetMigrationTracker.cs b/ams/AssetMigrationTracker.cs index 37849c3..601dbf1 100644 --- a/ams/AssetMigrationTracker.cs +++ b/ams/AssetMigrationTracker.cs @@ -1,4 +1,5 @@ using AMSMigrate.Contracts; +using Azure; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; @@ -64,7 +65,7 @@ internal class AssetMigrationTracker : IMigrationTracker GetMigrationStatusAsync(BlobContainerClient container, CancellationToken cancellationToken) { BlobContainerProperties properties = await container.GetPropertiesAsync(cancellationToken: cancellationToken); diff --git a/ams/CleanupCommand.cs b/ams/CleanupCommand.cs index 1ec3df4..807799c 100644 --- a/ams/CleanupCommand.cs +++ b/ams/CleanupCommand.cs @@ -3,6 +3,7 @@ using Azure; using Azure.Core; using Azure.ResourceManager.Media; +using Azure.ResourceManager.Media.Models; using Azure.Storage.Blobs; using Microsoft.Extensions.Logging; using Spectre.Console; @@ -46,16 +47,10 @@ public override async Task MigrateAsync(CancellationToken cancellationToken) } Dictionary stats = new Dictionary(); - var totalAssets = await QueryMetricAsync( - account.Id.ToString(), - "AssetCount", - cancellationToken: cancellationToken); - - _logger.LogInformation("The total asset count of the media account is {count}.", totalAssets); AsyncPageable assets; - + //clean up asset - var resourceFilter = _options.IsCleanUpAccount? null: GetAssetResourceFilter(_options.ResourceFilter, null, null); + var resourceFilter = _options.IsCleanUpAccount ? null : GetAssetResourceFilter(_options.ResourceFilter, null, null); var orderBy = "properties/created"; assets = account.GetMediaAssets() @@ -64,7 +59,7 @@ public override async Task MigrateAsync(CancellationToken cancellationToken) foreach (var asset in assetList) { - var result = await CleanUpAssetAsync(_options.IsCleanUpAccount||_options.IsForceCleanUpAsset,account, asset, cancellationToken); + var result = await CleanUpAssetAsync(_options.IsCleanUpAccount || _options.IsForceCleanUpAsset, account, asset, cancellationToken); stats.Add(asset.Data.Name, result); } WriteSummary(stats, false); @@ -111,6 +106,10 @@ private async Task CleanUpAccountAsync(MediaServicesAccountResource accoun { foreach (var streamingEndpoint in endpoints) { + if (streamingEndpoint.Data.ResourceState == StreamingEndpointResourceState.Running) + { + await streamingEndpoint.StopAsync(WaitUntil.Completed); + } await streamingEndpoint.DeleteAsync(WaitUntil.Completed); } } @@ -125,10 +124,14 @@ private async Task CleanUpAccountAsync(MediaServicesAccountResource accoun { foreach (var liveEvent in liveevents) { + if (liveEvent.Data.ResourceState == LiveEventResourceState.Running) + { + await liveEvent.StopAsync(WaitUntil.Completed, new LiveEventActionContent() { RemoveOutputsOnStop = true }); + } await liveEvent.DeleteAsync(WaitUntil.Completed); } } - + var deleteOperation = await account.DeleteAsync(WaitUntil.Completed); if (deleteOperation.HasCompleted && deleteOperation.GetRawResponse().Status == 200) @@ -149,7 +152,7 @@ private async Task CleanUpAccountAsync(MediaServicesAccountResource accoun return false; } } - private async Task CleanUpAssetAsync(bool isForcedelete,MediaServicesAccountResource account, MediaAssetResource asset, CancellationToken cancellationToken) + private async Task CleanUpAssetAsync(bool isForcedelete, MediaServicesAccountResource account, MediaAssetResource asset, CancellationToken cancellationToken) { try { @@ -162,11 +165,11 @@ private async Task CleanUpAssetAsync(bool isForcedelete,MediaServicesAccou return false; } - - // The asset container exists, try to check the metadata list first. - - if (isForcedelete||(_tracker.GetMigrationStatusAsync(container, cancellationToken).Result.Status == MigrationStatus.Completed)) - { + + // The asset container exists, try to check the metadata list first. + + if (isForcedelete || (_tracker.GetMigrationStatusAsync(container, cancellationToken).Result.Status == MigrationStatus.Completed)) + { var locator = await account.GetStreamingLocatorAsync(asset, cancellationToken); if (locator != null) { diff --git a/ams/ResetCommand.cs b/ams/ResetCommand.cs new file mode 100644 index 0000000..8421fbd --- /dev/null +++ b/ams/ResetCommand.cs @@ -0,0 +1,95 @@ +using AMSMigrate.Ams; +using AMSMigrate.Contracts; +using Azure; +using Azure.Core; +using Azure.ResourceManager.Media; +using Azure.ResourceManager.Media.Models; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Microsoft.Extensions.Logging; +using Spectre.Console; + +namespace AMSMigrate.ams +{ + internal class ResetCommand : BaseMigrator + { + private readonly ILogger _logger; + private readonly ResetOptions _options; + private readonly IMigrationTracker _tracker; + internal const string AssetTypeKey = "AssetType"; + internal const string MigrateResultKey = "MigrateResult"; + internal const string ManifestNameKey = "ManifestName"; + internal const string OutputPathKey = "OutputPath"; + + public ResetCommand(GlobalOptions globalOptions, + ResetOptions resetOptions, + IAnsiConsole console, + TokenCredential credential, + IMigrationTracker tracker, + ILogger logger) + : base(globalOptions, console, credential) + { + _options = resetOptions; + _logger = logger; + _tracker = tracker; + } + + public override async Task MigrateAsync(CancellationToken cancellationToken) + { + var account = await GetMediaAccountAsync(_options.AccountName, cancellationToken); + _logger.LogInformation("Begin reset assets on account: {name}", account.Data.Name); + + AsyncPageable assets = account.GetMediaAssets() + .GetAllAsync(cancellationToken: cancellationToken); + List? assetList = await assets.ToListAsync(cancellationToken); + int resetedAssetCount = 0; + foreach (var asset in assetList) + { + var (storage, _) = await _resourceProvider.GetStorageAccount(asset.Data.StorageAccountName, cancellationToken); + var container = storage.GetContainer(asset); + if (!await container.ExistsAsync(cancellationToken)) + { + _logger.LogWarning("Container {name} missing for asset {asset}", container.Name, asset.Data.Name); + return; + } + + if (_options.category.Equals("all", StringComparison.OrdinalIgnoreCase) || (_tracker.GetMigrationStatusAsync(container, cancellationToken).Result.Status == MigrationStatus.Failed)) + { + try + { + BlobContainerProperties properties = await container.GetPropertiesAsync(cancellationToken: cancellationToken); + + if (properties?.Metadata != null && properties.Metadata.Count == 0) + { + _logger.LogInformation($"Container '{container.Name}' does not have metadata."); + } + else + { // Clear container metadata + properties?.Metadata?.Remove(MigrateResultKey); + properties?.Metadata?.Remove(AssetTypeKey); + properties?.Metadata?.Remove(OutputPathKey); + properties?.Metadata?.Remove(ManifestNameKey); + var deleteOperation = await container.SetMetadataAsync(properties?.Metadata); + if (deleteOperation.GetRawResponse().Status == 200) + { + _logger.LogInformation($"Meta data in Container '{container.Name}' is deleted successfully."); + resetedAssetCount++; + } + else + { + _logger.LogInformation($"Meta data in Container '{container.Name}' does not exist or was not deleted."); + } + } + + } + catch (Exception ex) + { + _logger.LogError($"An unexpected error occurred: {ex.Message}"); + } + + } + } + _logger.LogDebug($"{resetedAssetCount} out of {assetList.Count} assets has been reseted."); + } + } +} diff --git a/contracts/ResetOptions.cs b/contracts/ResetOptions.cs new file mode 100644 index 0000000..06945aa --- /dev/null +++ b/contracts/ResetOptions.cs @@ -0,0 +1,12 @@ + +namespace AMSMigrate.Contracts +{ + /// + /// It holds the options for cleanup commands. + /// + public record ResetOptions( + string AccountName, + string category + ); + +}