diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Common/PSRecoveryServicesVaultExtendedInfoClient.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Common/PSRecoveryServicesVaultExtendedInfoClient.cs index 1020440f6548..8eadcc529135 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Common/PSRecoveryServicesVaultExtendedInfoClient.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Common/PSRecoveryServicesVaultExtendedInfoClient.cs @@ -125,6 +125,27 @@ public ASRVaultCreds GenerateVaultCredential(X509Certificate2 managementCert, AR return asrVaultCreds; } + /// + /// Upload cert to idmgmt + /// + /// certificate to be uploaded + /// vault object + /// Upload Certificate Response + public UploadCertificateResponse UploadCertificate(X509Certificate2 managementCert, ARSVault vault) + { + var certificateArgs = new CertificateArgs(); + certificateArgs.Properties = new Dictionary(); + certificateArgs.Properties.Add("certificate", Convert.ToBase64String(managementCert.GetRawCertData())); + + var response = this.recoveryServicesClient.VaultExtendedInfo.UploadCertificateAsync( + vault.ResouceGroupName, + vault.Name, + certificateArgs, managementCert.FriendlyName, + this.GetRequestHeaders()); + response.Wait(); + return response.Result; + } + /// /// Changes the Vault context /// diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSContracts.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSContracts.cs index f36831037a6b..869ebbc99a5e 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSContracts.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSContracts.cs @@ -358,10 +358,11 @@ public VaultCreds() /// resource name /// management cert /// authenticating service namespace - public VaultCreds(string subscriptionId, string resourceName, string managementCert, AcsNamespace acsNamespace) + /// resource type backup vault or ASR vault + public VaultCreds(string subscriptionId, string resourceName, string managementCert, AcsNamespace acsNamespace, string resourceType = null) { this.SubscriptionId = subscriptionId; - this.ResourceType = Constants.ASRVaultType; + this.ResourceType = string.IsNullOrEmpty(resourceType) ? Constants.ASRVaultType : resourceType; this.ResourceName = resourceName; this.ManagementCert = managementCert; this.AcsNamespace = acsNamespace; @@ -504,6 +505,54 @@ public ASRVaultCreds( #endregion } + /// + /// Class to define backup vault credentials + /// + [DataContract] + public class BackupVaultCreds : VaultCreds + { + /// + /// Gets or sets the agent links + /// + [DataMember(Order = 0)] + public string AgentLinks { get; set; } + + #region Constructors + + /// + /// Initializes a new instance of the BackupVaultCreds class + /// + public BackupVaultCreds() { } + + /// + /// Initializes a new instance of the BackupVaultCreds class + /// + /// subscription Id + /// resource type + /// resource name + /// management cert + /// acs namespace + public BackupVaultCreds(string subscriptionId, string resourceName, string managementCert, AcsNamespace acsNamespace) + : base(subscriptionId, resourceName, managementCert, acsNamespace, Constants.BackupVaultType) { } + + /// + /// Initializes a new instance of the BackupVaultCreds class + /// + /// subscription Id + /// resource type + /// resource name + /// management cert + /// acs namespace + /// agent links + public BackupVaultCreds(string subscriptionId, string resourceName, string managementCert, AcsNamespace acsNamespace, string agentLinks) + : this(subscriptionId, resourceName, managementCert, acsNamespace) + { + AgentLinks = agentLinks; + } + + #endregion + } + /// /// Class to define ACS name space /// diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSObjects.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSObjects.cs index e89150a3072e..1095894ac51c 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSObjects.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Models/PSObjects.cs @@ -26,6 +26,11 @@ public class Constants /// public const string ASRVaultType = "HyperVRecoveryManagerVault"; + /// + /// Backup vault type + /// + public const string BackupVaultType = "Vaults"; + /// /// Vault Credential version. /// diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.Designer.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.Designer.cs index 9e9061c1ad0b..31ae6ced6777 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.Designer.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.Designer.cs @@ -69,6 +69,15 @@ internal static string AzureVMNetworkIsNotAssociatedWithTheSubscription { } } + /// + /// Looks up a localized string similar to RecoveryService - Backup Vault - Successfully serialized the file content. + /// + internal static string BackupVaultSerialized { + get { + return ResourceManager.GetString("BackupVaultSerialized", resourceCulture); + } + } + /// /// Looks up a localized string similar to Operation Failed. ///. @@ -106,6 +115,15 @@ internal static string DisableProtectionWhatIfMessage { } } + /// + /// Looks up a localized string similar to Executing cmdlet with SubscriptionId = {0}, ResourceGroupName = {1}, ResourceName = {2}, TargetLocation = {3}. + /// + internal static string ExecutingGetVaultCredCmdlet { + get { + return ResourceManager.GetString("ExecutingGetVaultCredCmdlet", resourceCulture); + } + } + /// /// Looks up a localized string similar to Calls using ID based parameter {0} will not be supported from next release. Please use its corresponding full object parameter instead.. /// @@ -368,6 +386,15 @@ internal static string ResourceNameNullOrEmpty { } } + /// + /// Looks up a localized string similar to Saving Vault Credentials to file : {0}. + /// + internal static string SavingVaultCred { + get { + return ResourceManager.GetString("SavingVaultCred", resourceCulture); + } + } + /// /// Looks up a localized string similar to Server {0} is not associated with the Vault {1}. /// @@ -441,6 +468,24 @@ internal static string SubscriptionIsNotAssociatedWithTheAccount { } } + /// + /// Looks up a localized string similar to RecoveryService - Successfully uploaded the certificate. + /// + internal static string UploadedCertToIdmgmt { + get { + return ResourceManager.GetString("UploadedCertToIdmgmt", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to RecoveryService - Going to upload the certificate. + /// + internal static string UploadingCertToIdmgmt { + get { + return ResourceManager.GetString("UploadingCertToIdmgmt", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not validate the storage account and subscription given. ///Are you sure you want to continue {0}?. @@ -469,6 +514,15 @@ internal static string VaultCreationSuccessMessage { } } + /// + /// Looks up a localized string similar to The target location provided is not a directory. Please provide a directory.. + /// + internal static string VaultCredPathException { + get { + return ResourceManager.GetString("VaultCredPathException", resourceCulture); + } + } + /// /// Looks up a localized string similar to Vault has been deleted. /// diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.resx b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.resx index feef11f65291..783bbe796ff6 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.resx +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Properties/Resources.resx @@ -277,4 +277,22 @@ Please provide a storage account with the same location as that of the vault. Site {0} is not associated with the Vault {1} + + RecoveryService - Backup Vault - Successfully serialized the file content + + + Executing cmdlet with SubscriptionId = {0}, ResourceGroupName = {1}, ResourceName = {2}, TargetLocation = {3} + + + Saving Vault Credentials to file : {0} + + + RecoveryService - Successfully uploaded the certificate + + + RecoveryService - Going to upload the certificate + + + The target location provided is not a directory. Please provide a directory. + \ No newline at end of file diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Utilities/CertUtils.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Utilities/CertUtils.cs index eb1b9e2d38bb..0845a32cfb10 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Utilities/CertUtils.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Utilities/CertUtils.cs @@ -221,5 +221,16 @@ private static CngKey Create2048RsaKey() return CngKey.Create(CngAlgorithm2.Rsa, null, keyCreationParameters); } + + /// + /// Returns serialized certificate - Base64 encoded based on the content type + /// + /// The certificate provided + /// Cert content type + /// The serialized cert value in string + public static string SerializeCert(X509Certificate2 cert, X509ContentType contentType) + { + return Convert.ToBase64String(cert.Export(contentType)); + } } } \ No newline at end of file diff --git a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Vault/GetAzureRMRecoveryServicesVaultSettingsFile.cs b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Vault/GetAzureRMRecoveryServicesVaultSettingsFile.cs index 2c952368f686..37b3b6e701a8 100644 --- a/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Vault/GetAzureRMRecoveryServicesVaultSettingsFile.cs +++ b/src/ResourceManager/RecoveryServices/Commands.RecoveryServices/Vault/GetAzureRMRecoveryServicesVaultSettingsFile.cs @@ -18,6 +18,13 @@ using System.Security.Cryptography.X509Certificates; using Microsoft.Azure.Common.Authentication.Models; using Microsoft.Azure.Portal.RecoveryServices.Models.Common; +using System.IO; +using System.Xml; +using System.Runtime.Serialization; +using System.Text; +using System.Globalization; +using Microsoft.Azure.Management.RecoveryServices.Models; +using Microsoft.Azure.Commands.RecoveryServices.Properties; namespace Microsoft.Azure.Commands.RecoveryServices { @@ -31,15 +38,19 @@ public class GetAzureRmRecoveryServicesVaultSettingsFile : RecoveryServicesCmdle /// /// Expiry in hours for generated certificate. /// - private const int VaultCertificateExpiryInHoursForHRM = 120; + private const int VaultCertificateExpiryInHoursForHRM = 120; + + /// + /// Expiry in hours for generated certificate. + /// + private const int VaultCertificateExpiryInHoursForBackup = 48; #region Parameters /// /// Gets or sets vault Object. /// - [Parameter(ParameterSetName = ASRParameterSets.ByDefault, Mandatory = true, ValueFromPipeline = true)] - [Parameter(ParameterSetName = ASRParameterSets.ForSite, Mandatory = true, ValueFromPipeline = true)] + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 1)] [ValidateNotNullOrEmpty] public ARSVault Vault { get; set; } @@ -63,10 +74,38 @@ public class GetAzureRmRecoveryServicesVaultSettingsFile : RecoveryServicesCmdle /// /// Gets or sets vault Object. /// - [Parameter(ParameterSetName = ASRParameterSets.ByDefault)] - [Parameter(ParameterSetName = ASRParameterSets.ForSite)] + [Parameter(Position = 2)] public string Path { get; set; } + /// + /// Gets or sets the path where the credential file is to be generated + /// + /// + /// Gets or sets vault Object. + /// + [Parameter(ParameterSetName = ASRParameterSets.ByDefault, Mandatory = false)] + [Parameter(ParameterSetName = ASRParameterSets.ForSite, Mandatory = false)] + public SwitchParameter SiteRecovery + { + get { return siteRecovery; } + set { siteRecovery = value; } + } + private bool siteRecovery; + + /// + /// Gets or sets the path where the credential file is to be generated + /// + /// + /// Gets or sets vault Object. + /// + [Parameter(ParameterSetName = "ForBackup", Mandatory = true)] + public SwitchParameter Backup + { + get { return backup; } + set { backup = value; } + } + private bool backup; + #endregion Parameters /// @@ -76,7 +115,15 @@ public override void ExecuteCmdlet() { try { - this.GetVaultSettingsFile(); + if (backup) + { + this.GetAzureRMRecoveryServicesVaultBackupCredentials(); + } + else + { + this.GetVaultSettingsFile(); + } + } catch (AggregateException aggregateEx) { @@ -101,7 +148,7 @@ private void GetVaultSettingsFile() if (!string.IsNullOrEmpty(this.SiteIdentifier) && !string.IsNullOrEmpty(this.SiteFriendlyName)) { - site.ID = this.SiteIdentifier; + site.ID = this.SiteIdentifier; site.Name = this.SiteFriendlyName; } @@ -144,5 +191,153 @@ private string GenerateFileName() return fileName; } + + #region Backup Vault Credentials + /// + /// Get vault credentials for backup vault type. + /// + public void GetAzureRMRecoveryServicesVaultBackupCredentials() + { + string targetLocation = string.IsNullOrEmpty(this.Path) ? Utilities.GetDefaultPath() : this.Path; + if (!Directory.Exists(targetLocation)) + { + throw new ArgumentException(Resources.VaultCredPathException); + } + + string subscriptionId = DefaultContext.Subscription.Id.ToString(); + string displayName = this.Vault.Name; + + WriteDebug(string.Format(CultureInfo.InvariantCulture, + Resources.ExecutingGetVaultCredCmdlet, + subscriptionId, this.Vault.ResouceGroupName, this.Vault.Name, targetLocation)); + + // Generate certificate + X509Certificate2 cert = CertUtils.CreateSelfSignedCertificate(VaultCertificateExpiryInHoursForBackup, subscriptionId.ToString(), this.Vault.Name); + + AcsNamespace acsNamespace = null; + string channelIntegrityKey = string.Empty; + try + { + // Upload cert into ID Mgmt + WriteDebug(string.Format(CultureInfo.InvariantCulture, Resources.UploadingCertToIdmgmt)); + acsNamespace = UploadCert(cert); + WriteDebug(string.Format(CultureInfo.InvariantCulture, Resources.UploadedCertToIdmgmt)); + } + catch (Exception exception) + { + throw exception; + } + + // generate vault credentials + string vaultCredsFileContent = GenerateVaultCreds(cert, subscriptionId, acsNamespace); + + // NOTE: One of the scenarios for this cmdlet is to generate a file which will be an input to DPM servers. + // We found a bug in the DPM UI which is looking for a particular namespace in the input file. + // The below is a hack to circumvent this issue and this would be removed once the bug can be fixed. + vaultCredsFileContent = vaultCredsFileContent.Replace("Microsoft.Azure.Commands.AzureBackup.Models", + "Microsoft.Azure.Portal.RecoveryServices.Models.Common"); + + // prepare for download + string fileName = string.Format("{0}_{1:ddd MMM dd yyyy}.VaultCredentials", displayName, DateTime.UtcNow); + string filePath = System.IO.Path.Combine(targetLocation, fileName); + WriteDebug(string.Format(Resources.SavingVaultCred, filePath)); + + File.WriteAllBytes(filePath, Encoding.UTF8.GetBytes(vaultCredsFileContent)); + + VaultSettingsFilePath output = new VaultSettingsFilePath() + { + FilePath = filePath, + }; + + // Output filename back to user + WriteObject(output); + } + + /// + /// Upload certificate + /// + /// management certificate + /// acs namespace of the uploaded cert + private AcsNamespace UploadCert(X509Certificate2 cert) + { + UploadCertificateResponse response = RecoveryServicesClient.UploadCertificate(cert, this.Vault); + + return new AcsNamespace(response); + } + + /// + /// Generates vault creds file + /// + /// management certificate + /// subscription Id + /// acs namespace + /// xml file in string format + private string GenerateVaultCreds(X509Certificate2 cert, string subscriptionId, AcsNamespace acsNamespace) + { + try + { + return GenerateVaultCredsForBackup(cert, subscriptionId, acsNamespace); + } + catch (Exception exception) + { + throw exception; + } + } + + /// + /// Generates vault creds file content for backup Vault + /// + /// management certificate + /// subscription Id + /// acs namespace + /// xml file in string format + private string GenerateVaultCredsForBackup(X509Certificate2 cert, string subscriptionId, AcsNamespace acsNamespace) + { + using (var output = new MemoryStream()) + { + using (var writer = XmlWriter.Create(output, GetXmlWriterSettings())) + { + BackupVaultCreds backupVaultCreds = new BackupVaultCreds(subscriptionId, + this.Vault.Name, + CertUtils.SerializeCert(cert, X509ContentType.Pfx), + acsNamespace, + GetAgentLinks()); + DataContractSerializer serializer = new DataContractSerializer(typeof(BackupVaultCreds)); + serializer.WriteObject(writer, backupVaultCreds); + + WriteDebug(string.Format(CultureInfo.InvariantCulture, Resources.BackupVaultSerialized)); + } + + return Encoding.UTF8.GetString(output.ToArray()); + } + } + + /// + /// Get Agent Links + /// + /// Agent links in string format + private static string GetAgentLinks() + { + return "WABUpdateKBLink,http://go.microsoft.com/fwlink/p/?LinkId=229525;" + + "StorageQuotaPurchaseLink,http://go.microsoft.com/fwlink/?LinkId=205490;" + + "WebPortalLink,http://go.microsoft.com/fwlink/?LinkId=252913;" + + "WABprivacyStatement,http://go.microsoft.com/fwlink/?LinkId=221308"; + } + + /// + /// A set of XmlWriterSettings to use for the publishing profile + /// + /// The XmlWriterSettings set + private XmlWriterSettings GetXmlWriterSettings() + { + return new XmlWriterSettings + { + Encoding = new UTF8Encoding(false), + Indent = true, + NewLineOnAttributes = true + }; + } + + #endregion } }