diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs index 20bac893c0109..8252fe7e3e825 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net461.cs @@ -1,5 +1,19 @@ namespace Azure { + public partial class CloudMachine + { + public CloudMachine(System.IO.Stream configurationContent) { } + public CloudMachine(string configurationFile = "cloudconfig.json") { } + public string DisplayName { get { throw null; } set { } } + public string Id { get { throw null; } } + public string Region { get { throw null; } } + public string SubscriptionId { get { throw null; } } + public static Azure.CloudMachine Create(string subscriptionId, string region) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(System.IO.Stream stream) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(string filepath) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct Value { @@ -118,6 +132,12 @@ public void AddOrUpdate(TKey key, TValue? val, int length) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public bool TryGet(TKey key, out TValue? value) { throw null; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly | System.AttributeTargets.Class, Inherited=false, AllowMultiple=true)] + public partial class ProvisionableTemplateAttribute : System.Attribute + { + public ProvisionableTemplateAttribute(string resourceName) { } + public string ResourceName { get { throw null; } } + } public abstract partial class SchemaValidator { protected SchemaValidator() { } diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs index 20bac893c0109..8252fe7e3e825 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.net6.0.cs @@ -1,5 +1,19 @@ namespace Azure { + public partial class CloudMachine + { + public CloudMachine(System.IO.Stream configurationContent) { } + public CloudMachine(string configurationFile = "cloudconfig.json") { } + public string DisplayName { get { throw null; } set { } } + public string Id { get { throw null; } } + public string Region { get { throw null; } } + public string SubscriptionId { get { throw null; } } + public static Azure.CloudMachine Create(string subscriptionId, string region) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(System.IO.Stream stream) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(string filepath) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct Value { @@ -118,6 +132,12 @@ public void AddOrUpdate(TKey key, TValue? val, int length) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public bool TryGet(TKey key, out TValue? value) { throw null; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly | System.AttributeTargets.Class, Inherited=false, AllowMultiple=true)] + public partial class ProvisionableTemplateAttribute : System.Attribute + { + public ProvisionableTemplateAttribute(string resourceName) { } + public string ResourceName { get { throw null; } } + } public abstract partial class SchemaValidator { protected SchemaValidator() { } diff --git a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs index 20bac893c0109..8252fe7e3e825 100644 --- a/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs +++ b/sdk/core/Azure.Core.Experimental/api/Azure.Core.Experimental.netstandard2.0.cs @@ -1,5 +1,19 @@ namespace Azure { + public partial class CloudMachine + { + public CloudMachine(System.IO.Stream configurationContent) { } + public CloudMachine(string configurationFile = "cloudconfig.json") { } + public string DisplayName { get { throw null; } set { } } + public string Id { get { throw null; } } + public string Region { get { throw null; } } + public string SubscriptionId { get { throw null; } } + public static Azure.CloudMachine Create(string subscriptionId, string region) { throw null; } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(System.IO.Stream stream) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public void Save(string filepath) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct Value { @@ -118,6 +132,12 @@ public void AddOrUpdate(TKey key, TValue? val, int length) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public bool TryGet(TKey key, out TValue? value) { throw null; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Assembly | System.AttributeTargets.Class, Inherited=false, AllowMultiple=true)] + public partial class ProvisionableTemplateAttribute : System.Attribute + { + public ProvisionableTemplateAttribute(string resourceName) { } + public string ResourceName { get { throw null; } } + } public abstract partial class SchemaValidator { protected SchemaValidator() { } diff --git a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj index 00ae70b8e69e0..a25babac3e151 100644 --- a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj +++ b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj @@ -1,4 +1,4 @@ - + Experimental types that might eventually move to Azure.Core Microsoft Azure Client Pipeline Experimental Extensions diff --git a/sdk/core/Azure.Core.Experimental/src/CloudMachine.cs b/sdk/core/Azure.Core.Experimental/src/CloudMachine.cs new file mode 100644 index 0000000000000..8b4ddd942b6ac --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/CloudMachine.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.IO; +using System.Text.Json; +using Azure.Core; + +namespace Azure +{ + /// + /// Stores configuration of a CloudMachine. + /// + public class CloudMachine + { + /// + /// Unique identifier of a CloudMachine. It's the name of the resource group of all the CloudMachine resources. + /// + public string Id { get; } + + /// + /// Friendly name of CloudMachine. It's stored as a Tag of all Azure resources associated with the machine. + /// + public string DisplayName { get; set; } + + /// + /// Azure subscription ID. + /// + public string SubscriptionId { get; } + + /// + /// Azure region, e.g. westus2 + /// + public string Region { get; } + + private int Version { get; } + + /// + /// Creates a new CloudMachine + /// + /// + /// + /// + /// DisplayName is initialized to id. It can be changed by setting the DisplayName property. + public static CloudMachine Create(string subscriptionId, string region) + { + if (string.IsNullOrEmpty(subscriptionId)) throw new ArgumentNullException(nameof(subscriptionId)); + if (string.IsNullOrEmpty(region)) throw new ArgumentNullException(nameof(region)); + + var id = GenerateCloudMachineId(); + var defaultDisplayName = $"{id}@{DateTime.UtcNow:d}"; + return new CloudMachine(id, defaultDisplayName, subscriptionId, region, 1); + } + + /// + /// Loads CloudMachine settings from configurationFile + /// + /// + /// + public CloudMachine(string configurationFile = "cloudconfig.json") + { + try + { + byte[] configurationContent = File.ReadAllBytes(configurationFile); + var document = JsonDocument.Parse(configurationContent); + JsonElement json = document.RootElement.GetProperty("CloudMachine"); + + Id = ReadString(json, "id", configurationFile); + SubscriptionId = ReadString(json, "subscriptionId", configurationFile); + Region = ReadString(json, "region", configurationFile); + DisplayName = ReadString(json, "name", configurationFile); + Version = ReadInt32(json, "version", configurationFile); + } + catch (Exception e) when (e is not InvalidCloudMachineConfigurationException) + { + throw new InvalidCloudMachineConfigurationException(configurationFile, setting: null, e); + } + } + + /// + /// Loads CloudMachine settings from stream. + /// + /// + /// + public CloudMachine(Stream configurationContent) + { + try + { + var document = JsonDocument.Parse(configurationContent); + JsonElement json = document.RootElement.GetProperty("CloudMachine"); + + Id = ReadString(json, "id", nameof(configurationContent)); + SubscriptionId = ReadString(json, "subscriptionId", nameof(configurationContent)); + Region = ReadString(json, "region", nameof(configurationContent)); + DisplayName = ReadString(json, "name", nameof(configurationContent)); + Version = ReadInt32(json, "version", nameof(configurationContent)); + } + catch (Exception e) when (e is not InvalidCloudMachineConfigurationException) + { + throw new InvalidCloudMachineConfigurationException(nameof(configurationContent), setting: null, e); + } + } + + private CloudMachine(string id, string displayName, string subscriptionId, string region, int version) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(displayName, nameof(displayName)); + Argument.AssertNotNullOrEmpty(subscriptionId, nameof(subscriptionId)); + Argument.AssertNotNullOrEmpty(region, nameof(region)); + Argument.AssertInRange(version, 0, int.MaxValue, nameof(version)); + + Id = id; + DisplayName = displayName; + SubscriptionId = subscriptionId; + Region = region; + Version = version; + } + + /// + /// Save CloudMachine configuration to a stream. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void Save(Stream stream) + { + var options = new JsonWriterOptions() { Indented = true }; + using var json = new Utf8JsonWriter(stream, options); + json.WriteStartObject(); + json.WriteStartObject("CloudMachine"); + json.WriteString("id", Id); + json.WriteString("name", DisplayName); + json.WriteString("subscriptionId", SubscriptionId); + json.WriteString("region", Region); + json.WriteNumber("version", Version); + json.WriteEndObject(); + json.WriteEndObject(); + json.Flush(); + } + + /// + /// Save CloudMachine configuration to a file. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void Save(string filepath) + { + using var stream = File.OpenWrite(filepath); + Save(stream); + } + + private static string ReadString(JsonElement json, string key, string configurationStoreDisplayName) + { + try { + var value = json.GetProperty(key).GetString()!; + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(key); + return value; + } + catch (Exception e) { + throw new InvalidCloudMachineConfigurationException(configurationStoreDisplayName, key, e); + } + } + private static int ReadInt32(JsonElement json, string key, string configurationStoreDisplayName) + { + try { + var value = json.GetProperty(key).GetInt32(); + return value; + } + catch (Exception e) { + throw new InvalidCloudMachineConfigurationException(configurationStoreDisplayName, key, e); + } + } + + private static string GenerateCloudMachineId() + { + var guid = Guid.NewGuid(); + var guidString = guid.ToString("N"); + var cnId = "cm" + guidString.Substring(0, 15); // we can increase it to 20, but the template name cannot be that long + return cnId; + } + + internal class InvalidCloudMachineConfigurationException : InvalidOperationException + { + public InvalidCloudMachineConfigurationException(string configurationStoreDisplayName, string? setting, Exception innerException) : + base(CreateMessage(configurationStoreDisplayName, setting), innerException) + { } + + public static string CreateMessage(string configurationStoreDisplayName, string? setting) + { + if (setting != null) + return $"ERROR: Configuration setting {setting} not found in {configurationStoreDisplayName} or invalid format."; + else + return $"ERROR: Configuration store {configurationStoreDisplayName} not found or invalid format."; + } + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/ProvisionableTemplateAttribute.cs b/sdk/core/Azure.Core.Experimental/src/ProvisionableTemplateAttribute.cs new file mode 100644 index 0000000000000..dd808dfaf2cc4 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/ProvisionableTemplateAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core +{ + /// + /// Attribute used to describe a deployment template. + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, Inherited = false, AllowMultiple = true)] + public class ProvisionableTemplateAttribute : Attribute + { + /// + /// Deployment teplate stored in resources. + /// + /// + public ProvisionableTemplateAttribute(string resourceName) + => ResourceName = resourceName; + + /// + /// Name of assembly resource file containing a deployment template. + /// + public string ResourceName { get; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/CloudMachineTests.cs b/sdk/core/Azure.Core.Experimental/tests/CloudMachineTests.cs new file mode 100644 index 0000000000000..09c43d155dcca --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/CloudMachineTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using NUnit.Framework; + +namespace Azure +{ + public class CloudMachineTests + { + [Test] + public void SerializationRoundTrip() + { + var cm = CloudMachine.Create(Guid.NewGuid().ToString(), "westus2"); + using var stream = new MemoryStream(); + cm.Save(stream); + stream.Position = 0; + + var deserialized = new CloudMachine(stream); + Assert.AreEqual(cm.Id, deserialized.Id); + Assert.AreEqual(cm.DisplayName, deserialized.DisplayName); + Assert.AreEqual(cm.Region, deserialized.Region); + Assert.AreEqual(cm.SubscriptionId, deserialized.SubscriptionId); + } + + [Test] + public void DefaultCtor() + { + try + { + var cm = CloudMachine.Create(Guid.NewGuid().ToString(), "westus2"); + cm.Save("cloudconfig.json"); + var deserialized = new CloudMachine(); + Assert.AreEqual(cm.Id, deserialized.Id); + Assert.AreEqual(cm.DisplayName, deserialized.DisplayName); + Assert.AreEqual(cm.Region, deserialized.Region); + Assert.AreEqual(cm.SubscriptionId, deserialized.SubscriptionId); + } + finally + { + File.Delete("cloudconfig.json"); + } + } + } +}