Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Melinda/clean #100

Merged
merged 11 commits into from
Aug 2, 2023
64 changes: 64 additions & 0 deletions CleanupOptionsBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using AMSMigrate.Contracts;
using System.CommandLine;
using System.CommandLine.Binding;

namespace AMSMigrate
{
internal class CleanupOptionsBinder : BinderBase<CleanupOptions>
{
private readonly Option<string> _sourceAccount = new Option<string>(
aliases: new[] { "--source-account-name", "-n" },
description: "Azure Media Services Account.")
{
IsRequired = true,
Arity = ArgumentArity.ExactlyOne
};

private readonly Option<string?> _filter = new Option<string?>(
aliases: new[] { "--resource-filter", "-f" },
description: @"An ODATA condition to filter the resources only when the source account is for media service.
e.g.: ""name eq 'asset1'"" to match an asset with name 'asset1'.
Visit https://learn.microsoft.com/en-us/azure/media-services/latest/filter-order-page-entities-how-to for more information.")
{
Arity = ArgumentArity.ZeroOrOne
};

private readonly Option<bool> _isForceCleanUpAsset = new(
aliases: new[] {"--force-cleanup", "-x"},
() => false,
description: @"Force the cleanup of the selected input assets no matter what migration status is.")
{
IsRequired = false
};

private readonly Option<bool> _isCleanUpAccount = new(
aliases: new[] {"--cleanup-account", "-ax"},
() => false,
description: @"Delete the whole ams account.")
{
IsRequired = false
};

public CleanupOptions GetValue(BindingContext context) => GetBoundValue(context);

public Command GetCommand(string name, string description)
{
var command = new Command(name, description);
command.AddOption(_sourceAccount);
command.AddOption(_filter);
command.AddOption(_isForceCleanUpAsset);
command.AddOption(_isCleanUpAccount);
return command;
}

protected override CleanupOptions GetBoundValue(BindingContext bindingContext)
{
return new CleanupOptions(
bindingContext.ParseResult.GetValueForOption(_sourceAccount)!,
bindingContext.ParseResult.GetValueForOption(_filter),
bindingContext.ParseResult.GetValueForOption(_isForceCleanUpAsset),
bindingContext.ParseResult.GetValueForOption(_isCleanUpAccount)
);
}
}
}
88 changes: 59 additions & 29 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AMSMigrate.Ams;
using AMSMigrate.ams;
using AMSMigrate.Ams;
using AMSMigrate.Azure;
using AMSMigrate.Contracts;
using AMSMigrate.Local;
Expand Down Expand Up @@ -35,7 +36,8 @@ public static async Task<int> Main(string[] args)
amsmigrate analyze -s <subscriptionid> -g <resourcegroup> -n <account>
This will analyze the given media account and produce a summary report.");
rootCommand.Add(analyzeCommand);
analyzeCommand.SetHandler(async context => {
analyzeCommand.SetHandler(async context =>
{
var analysisOptions = analysisOptionsBinder.GetValue(context.BindingContext);
await AnalyzeAssetsAsync(context, analysisOptions, context.GetCancellationToken());
});
Expand All @@ -54,36 +56,53 @@ amsmigrate assets -s <subscription id> -g <resource group> -n <ams account name>
await MigrateAssetsAsync(context, assetOptions, context.GetCancellationToken());
});

// disable storage migrate option until ready
/*
var storageOptionsBinder = new StorageOptionsBinder();
var storageCommand = storageOptionsBinder.GetCommand("storage", @"Directly migrate the assets from the storage account.
Doesn't require the Azure media services to be running.
Examples:
amsmigrate storage -s <subscription id> -g <resource group> -n <source storage account> -o <output storage account> -t path-template
");
rootCommand.Add(storageCommand);
storageCommand.SetHandler(async context =>
{
var globalOptions = globalOptionsBinder.GetValue(context.BindingContext);
var storageOptions = storageOptionsBinder.GetValue(context.BindingContext);
await MigrateStorageAsync(globalOptions, storageOptions, context.GetCancellationToken());
});
*/

// disable key migrate option until ready
/*
var keyOptionsBinder = new KeyOptionsBinder();
var keysCommand = keyOptionsBinder.GetCommand();
rootCommand.Add(keysCommand);
keysCommand.SetHandler(

var cleanupOptionsBinder = new CleanupOptionsBinder();
var cleanupCommand = cleanupOptionsBinder.GetCommand("cleanup", @"Do the cleanup of AMS account or Storage account
Examples to cleanup account:
cleanup -s <subscriptionid> -g <resourcegroup> -n <account> -ax true
This command forcefully removes the Azure Media Services (AMS) account.
melindawangmsft marked this conversation as resolved.
Show resolved Hide resolved
Examples to cleanup asset:
cleanup -s <subscriptionid> -g <resourcegroup> -n <account> -x true
This command forcefully removes all assets in the given account.");
rootCommand.Add(cleanupCommand);
cleanupCommand.SetHandler(
async context =>
{
var globalOptions = globalOptionsBinder.GetValue(context.BindingContext);
var keyOptions = keyOptionsBinder.GetValue(context.BindingContext);
await MigrateKeysAsync(globalOptions, keyOptions, context.GetCancellationToken());
var cleanupOptions = cleanupOptionsBinder.GetValue(context.BindingContext);
await CleanupAsync(context, cleanupOptions, context.GetCancellationToken());
});
*/

// disable storage migrate option until ready
/*
var storageOptionsBinder = new StorageOptionsBinder();
var storageCommand = storageOptionsBinder.GetCommand("storage", @"Directly migrate the assets from the storage account.
Doesn't require the Azure media services to be running.
Examples:
amsmigrate storage -s <subscription id> -g <resource group> -n <source storage account> -o <output storage account> -t path-template
");
rootCommand.Add(storageCommand);
storageCommand.SetHandler(async context =>
{
var globalOptions = globalOptionsBinder.GetValue(context.BindingContext);
var storageOptions = storageOptionsBinder.GetValue(context.BindingContext);
await MigrateStorageAsync(globalOptions, storageOptions, context.GetCancellationToken());
});
*/

// disable key migrate option until ready
/*
var keyOptionsBinder = new KeyOptionsBinder();
var keysCommand = keyOptionsBinder.GetCommand();
rootCommand.Add(keysCommand);
keysCommand.SetHandler(
async context =>
{
var globalOptions = globalOptionsBinder.GetValue(context.BindingContext);
var keyOptions = keyOptionsBinder.GetValue(context.BindingContext);
await MigrateKeysAsync(globalOptions, keyOptions, context.GetCancellationToken());
});
*/

var parser = new CommandLineBuilder(rootCommand)
.UseDefaults()
Expand Down Expand Up @@ -190,6 +209,17 @@ await ActivatorUtilities.CreateInstance<StorageMigrator>(provider, storageOption
.MigrateAsync(cancellationToken);
}

static async Task CleanupAsync(
InvocationContext context,
CleanupOptions cleanupOptions,
CancellationToken cancellationToken)
{
var provider = context.BindingContext.GetRequiredService<IServiceProvider>();
await ActivatorUtilities.CreateInstance<CleanupCommand>(provider, cleanupOptions)
.MigrateAsync(cancellationToken);
}


static async Task MigrateKeysAsync(
InvocationContext context,
KeyOptions keyOptions,
Expand Down
198 changes: 198 additions & 0 deletions ams/CleanupCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using AMSMigrate.Ams;
using AMSMigrate.Contracts;
using Azure;
using Azure.Core;
using Azure.ResourceManager.Media;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using System.ComponentModel;

namespace AMSMigrate.ams
{
internal class CleanupCommand : BaseMigrator
{
private readonly ILogger _logger;
private readonly CleanupOptions _options;
private readonly IMigrationTracker<BlobContainerClient, AssetMigrationResult> _tracker;

public CleanupCommand(GlobalOptions globalOptions,
CleanupOptions cleanupOptions,
IAnsiConsole console,
TokenCredential credential,
IMigrationTracker<BlobContainerClient, AssetMigrationResult> tracker,
ILogger<CleanupCommand> logger)
: base(globalOptions, console, credential)
{
_options = cleanupOptions;
_logger = logger;
_tracker = tracker;
}
public override async Task MigrateAsync(CancellationToken cancellationToken)
{
var account = await GetMediaAccountAsync(_options.AccountName, cancellationToken);
_logger.LogInformation("Begin cleaning up on account: {name}", account.Data.Name);

if (_options.IsCleanUpAccount)
{
Console.Write($"Do you want to delete the account '{account.Data.Name}'? (y/n): ");
string? userResponse = Console.ReadLine();

if (!(userResponse?.ToLower() == "y"))
{
Console.WriteLine("Account cleanup canceled by user.");
return;
}
}

Dictionary<string, bool> stats = new Dictionary<string, bool>();
var totalAssets = await QueryMetricAsync(
account.Id.ToString(),
"AssetCount",
cancellationToken: cancellationToken);

_logger.LogInformation("The total asset count of the media account is {count}.", totalAssets);
AsyncPageable<MediaAssetResource> assets;

//clean up asset
var resourceFilter = _options.IsCleanUpAccount? null: GetAssetResourceFilter(_options.ResourceFilter, null, null);

var orderBy = "properties/created";
assets = account.GetMediaAssets()
.GetAllAsync(resourceFilter, orderby: orderBy, cancellationToken: cancellationToken);
List<MediaAssetResource>? assetList = await assets.ToListAsync(cancellationToken);
melindawangmsft marked this conversation as resolved.
Show resolved Hide resolved

foreach (var asset in assetList)
{
var result = await CleanUpAssetAsync(_options.IsCleanUpAccount||_options.IsForceCleanUpAsset,account, asset, cancellationToken);
stats.Add(asset.Data.Name, result);
}
WriteSummary(stats, false);

if (_options.IsCleanUpAccount)
{
Dictionary<string, bool> accStats = new Dictionary<string, bool>();
var result = await CleanUpAccountAsync(account, cancellationToken);
accStats.Add(account.Data.Name, result);
WriteSummary(accStats, true);
}

}

private void WriteSummary(IDictionary<string, bool> stats, bool isDeletingAccount)
{
var table = new Table();
if (isDeletingAccount)
{
table.AddColumn("Account");
}
else
{
table.AddColumn("Asset");
}
table.AddColumn("IsDeleted");
foreach (var (key, value) in stats)
{
var status = value ? $"[green]{value}[/]" : $"[red]{value}[/]";
table.AddRow($"[green]{key}[/]", status);
}

_console.Write(table);
}
private async Task<bool> CleanUpAccountAsync(MediaServicesAccountResource account, CancellationToken cancellationToken)
{
try
{
var endpoints = account.GetStreamingEndpoints();
var liveevents = account.GetMediaLiveEvents();
var policies = account.GetContentKeyPolicies();

if (endpoints != null)
{
foreach (var streamingEndpoint in endpoints)
{
await streamingEndpoint.DeleteAsync(WaitUntil.Completed);
}
}
if (policies != null)
{
foreach (var contentKeyPolicy in policies)
{
await contentKeyPolicy.DeleteAsync(WaitUntil.Completed);
}
}
if (liveevents != null)
{
foreach (var liveEvent in liveevents)
{
await liveEvent.DeleteAsync(WaitUntil.Completed);
}
}

var deleteOperation = await account.DeleteAsync(WaitUntil.Completed);

if (deleteOperation.HasCompleted && deleteOperation.GetRawResponse().Status == 200)
{
_logger.LogInformation("The media account {account} has been deleted.", account.Data.Name);
return true;
}
else
{
_logger.LogInformation("The media account {account} deletion failed.", account.Data.Name);
return false;
}

}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete account {name}", account.Data.Name);
return false;
}
}
private async Task<bool> CleanUpAssetAsync(bool isForcedelete,MediaServicesAccountResource account, MediaAssetResource asset, CancellationToken cancellationToken)
{
try
{

var storage = await _resourceProvider.GetStorageAccountAsync(account, 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 false;
}

// 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)
{
await locator.DeleteAsync(WaitUntil.Completed);
}

if (asset != null)
{
await asset.DeleteAsync(WaitUntil.Completed);
}
await container.DeleteAsync();
_logger.LogDebug("locator: {locator}, Migrated asset: {asset} , container: {container} are deleted.", locator?.Data.Name, asset.Data.Name, container?.Name);

Check warning on line 182 in ams/CleanupCommand.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 182 in ams/CleanupCommand.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
return true;
}
else
{
_logger.LogDebug("asset: {asset} does not meet the criteria for deletion.", asset.Data.Name);
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete asset {name}", asset.Data.Name);
return false;
}
}
}
}
14 changes: 14 additions & 0 deletions contracts/CleanupOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace AMSMigrate.Contracts
{
/// <summary>
/// It holds the options for cleanup commands.
/// </summary>
public record CleanupOptions(
string AccountName,
string? ResourceFilter,
bool IsForceCleanUpAsset,
bool IsCleanUpAccount
);

}

Loading