#if DESKTOP
/*******************************************************************************
* Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use
* this file except in compliance with the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file.
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
* *****************************************************************************
*
* AWS Tools for Windows (TM) PowerShell (TM)
*
*/
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Amazon.EC2.Import;
using Amazon.PowerShell.Common;
namespace Amazon.PowerShell.Cmdlets.EC2
{
public class EC2ImportCmdletsBase : AmazonEC2ClientCmdlet
{
///
/// The default parameter set for the cmdlet causes the local artifacts to be uploaded to
/// a named bucket, with optional key prefix, in Amazon S3 and an ImportInstance request be
/// submitted to Amazon EC2. The conversion task object returned by EC2 is output.
///
protected const string ParamSet_Default = "UploadAndImport";
///
/// Use of this parameter set causes the local artifacts to be uploaded to a named
/// bucket, with optional key prefix, in Amazon S3. No import is requested and the
/// key of the uploaded manifest is returned.
///
protected const string ParamSet_UploadOnly = "UploadOnly";
///
/// Use of this parameter set causes an ImportInstance request to be sent to Amazon EC2
/// using the manifest and image file artifacts previously uploaded. The conversion task
/// object returned by EC2 is output.
///
protected const string ParamSet_FromManifest = "ImportFromManifest";
protected const string ProgressActivity = "Importing";
// some common error messages; the newlines are then to try and get the resumption instructions etc
// out in the clear where users can see them
protected const string GeneratingManifestErrorMsg
= "The import operation failed to generate an import manifest."
+ "\r\n"
+ "No artifacts were uploaded that require manual removal."
+ "\r\n";
protected const string UploadingManifestErrorMsg
= "The import operation failed to create the bucket and/or upload the generated import manifest."
+ "\r\n"
+ "Bucket creation can fail if the bucket name is not unique, or a bucket with the name exists and is not owned by you."
+ "\r\n";
protected const string ManifestInspectionErrorMsg
= "The import operation failed to download and re-use the specified import manifest."
+ "\r\n"
+ "To remove uploaded artifacts: "
+ "\r\n"
+ " use the Remove-EC2DiskImage cmdlet supplying the bucket name and use '{0}' for the -ManifestFileKey parameter."
+ "\r\n";
protected const string UploadingImageFileErrorMsg
= "The import operation failed to upload one or more image file parts."
+ "\r\n";
protected const string UploadingImageFileErrorMsg_Retain
= "To continue: re-run the cmdlet with the -Resume switch."
+ "\r\n"
+ "To cancel and remove uploaded artifacts: "
+ "\r\n"
+ " use the Remove-EC2DiskImage cmdlet supplying the bucket name and use '{0}' for the -ManifestFileKey parameter."
+ "\r\n";
protected const string UploadingImageFileErrorMsg_NoRetain
= "The manifest and image file artifacts that had been successfully uploaded have been removed from the S3 bucket."
+ "\r\n";
protected const string ResumeUploadErrorMsg_NoMemo
= "Unable to determine the location of the import manifest for the image file."
+ "\r\n"
+ "Unable to resume upload for this image."
+ "\r\n";
protected const string SendingImportRequestMsg
= "The import operation failed sending the conversion request to EC2."
+ "\r\n"
+ "The import manifest and image file parts still exist in the S3 bucket."
+ "\r\n"
+ "To retry: Check the instance type and launch specification parameters and re-execute the cmdlet with parameter changes,"
+ "\r\n"
+ " remove any -ImageFile parameter and value, add the -ManifestFileKey parameter with value '{0}'."
+ "\r\n"
+ "To cancel and remove uploaded artifacts:"
+ "\r\n"
+ " use the Remove-EC2DiskImage cmdlet supplying the bucket name and use '{1}' for the -ManifestFileKey parameter."
+ "\r\n";
protected const string GenericFailedWithErrors
= "Image artifact upload and/or import failed with errors";
protected const string GenericUploadOnlySucceeded
= "Image artifact upload completed.";
protected const string GenericUploadAndConvertSucceeded =
"Image artifact upload completed, import conversion requested.";
protected static readonly string[] ValidFileFormats = { "VMDK", "RAW", "VHD" };
protected int _urlExpiryInDays = DiskImageImporter.DefaultUrlExpirationInDays;
protected int _uploadThreads = DiskImageImporter.DefaultUploadThreads;
protected virtual void ValidateArguments()
{
}
protected void UpdateImportProgress(string message, int? percentComplete = null)
{
WriteProgressRecord(ProgressActivity, message, percentComplete);
}
protected string ResolveImageFilePath(string imageFile)
{
if (Path.IsPathRooted(imageFile) || string.IsNullOrEmpty(imageFile))
return imageFile;
// note that Environment.CurrentDirectory is unreliably and in some shells,
// shows where we started, not where we are
var referencePath = this.SessionState.Path.CurrentFileSystemLocation.Path;
return Path.GetFullPath(Path.Combine(referencePath, imageFile));
}
///
/// Returns what should be a predictable and unique name for the image file
/// that we can use to hold temporary memo data between cmdlet executions.
/// As it is unlikely that a user will try and upload two images with the same
/// name but different paths at the same time, and both uploads fail, a simple
/// MD5 hash onto the image filename and path should be sufficient.
///
///
/// As we only know the user key prefix at the time we write the memo and not
/// when reading due to detecting the -Resume switch, we cannot include the
/// prefix in the hash.
///
/// The resolved path to the image file
/// The name of the bucket containing the manifest
///
static string ConstructMemoFilename(string imageFilePath, string bucketName)
{
var data = string.Concat(imageFilePath, ".", bucketName).ToLowerInvariant();
var hash = MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(data));
var hex = BitConverter.ToString(hash).Replace("-", string.Empty);
var filename = Path.GetFileName(imageFilePath);
return string.Format("{0}.{1}",
filename,
hex.Length > 8 ? hex.Substring(0, 8) : hex);
}
///
/// Reads the previously stashed S3 object key to a manifest associated
/// with the image file. The key was stored when an upload error was detected.
///
/// The resolved path to the image file
/// The name of the bucket containing the manifest
/// The stored S3 object key
///
/// We leave the memo file present after reading in case a continued network loss
/// means we don't exit the cmdlet through normal handling. The 'clean path'
/// through the import cmdlets will delete the memo if no error occurs.
///
protected string ReadManifestKeyFromMemo(string imageFilePath, string bucketName)
{
string manifestKey = null;
try
{
var awspsAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
AWSPowerShellAppDataSubPath);
if (Directory.Exists(awspsAppDataPath))
{
var memoFile = Path.Combine(awspsAppDataPath, ConstructMemoFilename(imageFilePath, bucketName));
manifestKey = File.ReadAllText(memoFile, Encoding.UTF8);
}
else
{
throw new InvalidOperationException("AppData path for AWSPowerShell does not exist; a memo file could not have been written.");
}
}
catch
{
ThrowExecutionError(ResumeUploadErrorMsg_NoMemo, this);
}
return manifestKey;
}
///
/// Writes a memo-ization file to a subfolder of the user's appdata folder
/// containing the S3 object key to the manifest we just failed to complete
/// upload for. We'll read this file if the -Resume switch is set on next run.
/// to save the user neededing to remember it or paste it manually into the
/// new command.
///
/// The resolved path to the image file
/// The name of the bucket containing the manifest
/// The S3 key of the manifest
///
/// Although it's highly unlikely a user might upload two images with the same
/// base name at the same time, we take a leaf out of Git's book and hash the
/// path+filename of the image to append to the image name we use for the memo
/// file -- this pretty much guarantees we correctly match the image and memo
/// file in resume mode.
///
protected void WriteManifestMemoFile(string imageFilePath,
string bucketName,
string manifestFileKey)
{
try
{
var awspsAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
AWSPowerShellAppDataSubPath);
if (!Directory.Exists(awspsAppDataPath))
Directory.CreateDirectory(awspsAppDataPath);
var memoFile = Path.Combine(awspsAppDataPath, ConstructMemoFilename(imageFilePath, bucketName));
File.WriteAllText(memoFile, manifestFileKey, Encoding.UTF8);
}
catch (Exception)
{
throw;
}
}
///
/// Removes any memo-ization file for an image file on successful completion of processing.
///
/// The resolved path to the image file
/// The name of the bucket containing the manifest
protected void CleanManifestMemoFile(string imageFilePath, string bucketName)
{
try
{
var awspsAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
AWSPowerShellAppDataSubPath);
if (Directory.Exists(awspsAppDataPath))
{
var memoFile = Path.Combine(awspsAppDataPath, ConstructMemoFilename(imageFilePath, bucketName));
if (File.Exists(memoFile))
File.Delete(memoFile);
}
}
catch
{
}
}
///
/// Constructs an appropriate error message depending on where the import
/// error occurred. If rollback is not enabled (so we are leaving whatever
/// artifacts were uploaded to S3 in place), we write a temporary memo file
/// to a folder in the user's appdata that contains the S3 key of the manifest.
/// If the cmdlet is restarted with the -Resume switch, we'll read the manifest
/// key from there, analyze the manifest and continue uploaded.
///
///
///
///
///
protected string HandleImportError(DiskImageImporter importHelper,
DiskImageImporterException exc,
bool rollbackEnabled)
{
var msg = new StringBuilder();
switch (exc.Stage)
{
case DiskImportErrorStage.GeneratingManifest:
msg.Append(GeneratingManifestErrorMsg);
break;
case DiskImportErrorStage.UploadingManifest:
msg.Append(UploadingManifestErrorMsg);
break;
case DiskImportErrorStage.ManifestInspection:
msg.AppendFormat(ManifestInspectionErrorMsg, importHelper.ManifestFileKey);
break;
case DiskImportErrorStage.UploadingImageFile:
msg.Append(UploadingImageFileErrorMsg);
if (rollbackEnabled)
msg.Append(UploadingImageFileErrorMsg_NoRetain);
else
{
WriteManifestMemoFile(importHelper.ImageFilePath,
importHelper.BucketName,
importHelper.ManifestFileKey);
msg.AppendFormat(UploadingImageFileErrorMsg_Retain, importHelper.ManifestFileKey);
}
break;
case DiskImportErrorStage.SendingImportRequest:
msg.AppendFormat(SendingImportRequestMsg, importHelper.ManifestFileKey,
importHelper.ArtifactsKeyPrefix);
break;
}
return msg.ToString();
}
protected string ValidateFileFormat(string imageFile, string specifiedFormat)
{
string fileFormat;
if (string.IsNullOrEmpty(specifiedFormat))
{
var ext = Path.GetExtension(imageFile);
if (string.IsNullOrEmpty(ext))
ThrowArgumentError("The image file has no extension, so the format cannot be determined automatically. Use the -FileFormat parameter.", imageFile);
fileFormat = ext.TrimStart('.');
}
else
fileFormat = specifiedFormat;
var matched = ValidFileFormats.Any(s => s.Equals(fileFormat, StringComparison.OrdinalIgnoreCase));
if (!matched)
{
var msg =
string.Format(
"The image file format, '{0}' (obtained from the -FileFormat parameter or the image file extension) does not "
+ "\r\n"
+ "match the image types known to be compatible with EC2 import.", fileFormat);
DisplayWarning(msg);
}
return fileFormat;
}
internal class ImportCmdletContextBase : ExecutorContext
{
public string ImageFile { get; set; }
public string BucketName { get; set; }
public string KeyPrefix { get; set; }
public string[] ManifestFileKey { get; set; }
public string AvailabilityZone { get; set; }
public string FileFormat { get; set; }
public String Description { get; set; }
public int? VolumeSize { get; set; }
public int UploadThreads { get; set; }
public bool Resume { get; set; }
public bool RollbackOnUploadError { get; set; }
public int UrlExpirationInDays { get; set; }
}
}
}
#endif