question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Issue pushing custom script extension via Fluent SDK

See original GitHub issue

Hi,

We’ve been experiencing an issue with the Fluent SDK when trying to push a custom script extension to a VM. It seems that the command to execute we are pushing is not sent to the management API (or not being processed.) We keep seeing the error ‘Invalid Configuration - CommandToExecute is not specified in the configuration; it must be specified in either the protected or public configuration section’. We have tried putting CommandToExecute in both the public and protected settings.

After investigating it further, we determined that the generic object types which denote the public and private settings are not being serialized. Originally we were calling

public async Task PushCustomScript(string rgName, string vmName, CloudBlobContainer deploymentContainer, CancellationToken tkn)
{
	var scriptRef = deploymentContainer.GetBlobReference(Config.Configure.TrainingFile);
	var account = deploymentContainer.ServiceClient.Credentials;

	var extensionProp = new VirtualMachineExtensionInner
	{
		Location = Config.Configure.Location,
		VirtualMachineExtensionType = "CustomScriptExtension",
		Publisher = "Microsoft.Compute",
		TypeHandlerVersion = "1.9",
		AutoUpgradeMinorVersion = true,
		Tags = new Dictionary<string, string>
		{
			{"DisplayName","Initiate-Training-Procedure"}
		},
		Settings = new
		{
			FileUris = new List<string>
			{
				scriptRef.Uri.AbsoluteUri
			}
		},
		ProtectedSettings = new
		{
			CommandToExecute = $"powershell -ExecutionPolicy Unrestricted -File ./{Config.Configure.TrainingFile} -accountName {account.AccountName} -key {Config.Configure.VmStorageKey} -endpoint {Config.Configure.VmStorageEndpoint}",
			StorageAccountName = account.AccountName,
			StorageAccountKey = account.ExportBase64EncodedKey()
		}
	};
	var result = await ComputeClient.VirtualMachineExtensions.CreateOrUpdateAsync(rgName, vmName, "Initiate-Training-Procedure", extensionProp, tkn);
	Config.Log.Info($"Triggered startup powershell script on vm {vmName}");
}

however after stepping into the source it appears that the Microsoft.Rest.Serialization.SafeJsonConvert serializer skips non-strongly defined objects.

As a work-around, we wrote a strongly-typed object for the CustomVmExtension however we don’t believe it will work for other extensions. FYI the following is our work-around (highly copied from the Fluent source code). Settings

public class Settings
{
	[JsonProperty(PropertyName = "fileUris")]
	public List<string> FileUris { get; set; }
}

ProtectedSettings Class

public class ProtectedSettings
{
	[JsonProperty(PropertyName = "commandToExecute")]
	public string CommandToExecute { get; set; }
	[JsonProperty(PropertyName = "storageAccountName")]
	public string StorageAccountName { get; set; }
	[JsonProperty(PropertyName = "storageAccountKey")]
	public string StorageAccountKey { get; set; }
}

Replacement AddVirtualMachineExtensionArgs Class

[Microsoft.Rest.Serialization.JsonTransformation]
public class AddVirtualMachineExtensionArgs : Resource
{
	[JsonProperty(PropertyName = "properties.forceUpdateTag")]
	public string ForceUpdateTag { get; set; }
	[JsonProperty(PropertyName = "properties.publisher")]
	public string Publisher { get; set; }
	[JsonProperty(PropertyName = "properties.type")]
	public string VirtualMachineExtensionType { get; set; }
	[JsonProperty(PropertyName = "properties.typeHandlerVersion")]
	public string TypeHandlerVersion { get; set; }
	[JsonProperty(PropertyName = "properties.autoUpgradeMinorVersion")]
	public bool? AutoUpgradeMinorVersion { get; set; }
	[JsonProperty(PropertyName = "properties.settings")]
	public Settings Settings { get; set; }
	[JsonProperty(PropertyName = "properties.protectedSettings")]
	public ProtectedSettings ProtectedSettings { get; set; }
	[JsonProperty(PropertyName = "properties.provisioningState")]
	public string ProvisioningState { get; }
	[JsonProperty(PropertyName = "properties.instanceView")]
	public VirtualMachineExtensionInstanceView InstanceView { get; set; }

}

Manual Extension Push Method

public async Task PushCustomScriptManually(string resourceGroupName, string vmName, CloudBlobContainer deploymentContainer, CancellationToken cancellationToken)
{
	var scriptRef = deploymentContainer.GetBlobReference(Config.Configure.TrainingFile);
	var account = deploymentContainer.ServiceClient.Credentials;
	var vmExtensionName = "Initiate-Training-Procedure";
	var _client = ComputeClient;
	Dictionary<string, List<string>> customHeaders = null;
	var extensionParameters = new AddVirtualMachineExtensionArgs
	{
		Location = Config.Configure.Location,
		VirtualMachineExtensionType = "CustomScriptExtension",
		Publisher = "Microsoft.Compute",
		TypeHandlerVersion = "1.9",
		AutoUpgradeMinorVersion = true,
		Tags = new Dictionary<string, string>
		{
			{"DisplayName",vmExtensionName}
		},
		Settings = new Settings
		{
			FileUris = new List<string>
			{
				scriptRef.Uri.AbsoluteUri
			}
		},
		ProtectedSettings = new ProtectedSettings
		{
			CommandToExecute = $"powershell -ExecutionPolicy Unrestricted -File ./{Config.Configure.TrainingFile} -accountName {account.AccountName} -key {Config.Configure.VmStorageKey} -endpoint {Config.Configure.VmStorageEndpoint}",
			StorageAccountName = account.AccountName,
			StorageAccountKey = account.ExportBase64EncodedKey()
		}
	};
	string apiVersion = "2016-04-30-preview";
	// Tracing
	bool _shouldTrace = ServiceClientTracing.IsEnabled;
	string _invocationId = null;
	if (_shouldTrace)
	{
		_invocationId = ServiceClientTracing.NextInvocationId.ToString();
		Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
		tracingParameters.Add("resourceGroupName", resourceGroupName);
		tracingParameters.Add("vmName", vmName);
		tracingParameters.Add("vmExtensionName", vmExtensionName);
		tracingParameters.Add("extensionParameters", extensionParameters);
		tracingParameters.Add("apiVersion", apiVersion);
		tracingParameters.Add("cancellationToken", cancellationToken);
		ServiceClientTracing.Enter(_invocationId, this, "BeginCreateOrUpdate", tracingParameters);
	}
	// Construct URL
	var _baseUrl = _client.BaseUri.AbsoluteUri;
	var _url = new System.Uri(new System.Uri(_baseUrl + (_baseUrl.EndsWith("/") ? "" : "/")), "subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}/extensions/{vmExtensionName}").ToString();
	_url = _url.Replace("{resourceGroupName}", System.Uri.EscapeDataString(resourceGroupName));
	_url = _url.Replace("{vmName}", System.Uri.EscapeDataString(vmName));
	_url = _url.Replace("{vmExtensionName}", System.Uri.EscapeDataString(vmExtensionName));
	_url = _url.Replace("{subscriptionId}", System.Uri.EscapeDataString(_client.SubscriptionId));
	List<string> _queryParameters = new List<string>();
	if (apiVersion != null)
	{
		_queryParameters.Add(string.Format("api-version={0}", System.Uri.EscapeDataString(apiVersion)));
	}
	if (_queryParameters.Count > 0)
	{
		_url += (_url.Contains("?") ? "&" : "?") + string.Join("&", _queryParameters);
	}
	// Create HTTP transport objects
	var _httpRequest = new System.Net.Http.HttpRequestMessage();
	System.Net.Http.HttpResponseMessage _httpResponse = null;
	_httpRequest.Method = new System.Net.Http.HttpMethod("PUT");
	_httpRequest.RequestUri = new System.Uri(_url);
	// Set Headers
	if (_client.GenerateClientRequestId != null && _client.GenerateClientRequestId.Value)
	{
		_httpRequest.Headers.TryAddWithoutValidation("x-ms-client-request-id", System.Guid.NewGuid().ToString());
	}
	if (_client.AcceptLanguage != null)
	{
		if (_httpRequest.Headers.Contains("accept-language"))
		{
			_httpRequest.Headers.Remove("accept-language");
		}
		_httpRequest.Headers.TryAddWithoutValidation("accept-language", _client.AcceptLanguage);
	}
	if (customHeaders != null)
	{
		foreach (var _header in customHeaders)
		{
			if (_httpRequest.Headers.Contains(_header.Key))
			{
				_httpRequest.Headers.Remove(_header.Key);
			}
			_httpRequest.Headers.TryAddWithoutValidation(_header.Key, _header.Value);
		}
	}
	// Serialize Request
	string _requestContent = null;
	if (extensionParameters != null)
	{
		_requestContent = Microsoft.Rest.Serialization.SafeJsonConvert.SerializeObject(extensionParameters, _client.SerializationSettings);
		_httpRequest.Content = new System.Net.Http.StringContent(_requestContent, System.Text.Encoding.UTF8);
		_httpRequest.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
	}
	// Set Credentials
	if (_client.Credentials != null)
	{
		cancellationToken.ThrowIfCancellationRequested();
		await _client.Credentials.ProcessHttpRequestAsync(_httpRequest, cancellationToken).ConfigureAwait(false);
	}
	// Send Request
	if (_shouldTrace)
	{
		ServiceClientTracing.SendRequest(_invocationId, _httpRequest);
	}
	cancellationToken.ThrowIfCancellationRequested();
	_httpResponse = await _client.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false);
	if (_shouldTrace)
	{
		ServiceClientTracing.ReceiveResponse(_invocationId, _httpResponse);
	}
	HttpStatusCode _statusCode = _httpResponse.StatusCode;
	cancellationToken.ThrowIfCancellationRequested();
	string _responseContent = null;
	if ((int)_statusCode != 200 && (int)_statusCode != 201)
	{
		var ex = new CloudException(string.Format("Operation returned an invalid status code '{0}'", _statusCode));
		try
		{
			_responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
			CloudError _errorBody = Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject<CloudError>(_responseContent, _client.DeserializationSettings);
			if (_errorBody != null)
			{
				ex = new CloudException(_errorBody.Message);
				ex.Body = _errorBody;
			}
		}
		catch (Newtonsoft.Json.JsonException)
		{
			// Ignore the exception
		}
		ex.Request = new HttpRequestMessageWrapper(_httpRequest, _requestContent);
		ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent);
		if (_httpResponse.Headers.Contains("x-ms-request-id"))
		{
			ex.RequestId = _httpResponse.Headers.GetValues("x-ms-request-id").FirstOrDefault();
		}
		if (_shouldTrace)
		{
			ServiceClientTracing.Error(_invocationId, ex);
		}
		_httpRequest.Dispose();
		if (_httpResponse != null)
		{
			_httpResponse.Dispose();
		}
		throw ex;
	}
	// Create Result
	var _result = new AzureOperationResponse<VirtualMachineExtensionInner>();
	_result.Request = _httpRequest;
	_result.Response = _httpResponse;
	if (_httpResponse.Headers.Contains("x-ms-request-id"))
	{
		_result.RequestId = _httpResponse.Headers.GetValues("x-ms-request-id").FirstOrDefault();
	}
	// Deserialize Response
	if ((int)_statusCode == 200)
	{
		_responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
		try
		{
			_result.Body = Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject<VirtualMachineExtensionInner>(_responseContent, _client.DeserializationSettings);
		}
		catch (Newtonsoft.Json.JsonException ex)
		{
			_httpRequest.Dispose();
			if (_httpResponse != null)
			{
				_httpResponse.Dispose();
			}
			throw new SerializationException("Unable to deserialize the response.", _responseContent, ex);
		}
	}
	// Deserialize Response
	if ((int)_statusCode == 201)
	{
		_responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
		try
		{
			_result.Body = Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject<VirtualMachineExtensionInner>(_responseContent, _client.DeserializationSettings);
		}
		catch (Newtonsoft.Json.JsonException ex)
		{
			_httpRequest.Dispose();
			if (_httpResponse != null)
			{
				_httpResponse.Dispose();
			}
			throw new SerializationException("Unable to deserialize the response.", _responseContent, ex);
		}
	}
	if (_shouldTrace)
	{
		ServiceClientTracing.Exit(_invocationId, _result);
	}
	var fullResponse = await ComputeClient.GetPutOrPatchOperationResultAsync(_result, customHeaders, cancellationToken).ConfigureAwait(false);
	Config.Log.Info($"Triggered startup powershell script on vm {vmName}");
}

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:2
  • Comments:10 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
hovsepmcommented, Jan 19, 2018

@slawlor

As you are using Fluent SDK - could you provide us some info on why did you end up using Inner types? And also how did you create the ComputeClient in your sample?

We do have the Fluent flow for defining custom script extension like this:

IVirtualMachine windowsVM = azure.VirtualMachines.Define(windowsVmName)
        .WithRegion(Region.USEast)
        .WithExistingResourceGroup(rgName)
        .WithNewPrimaryNetwork("10.0.0.0/28")
        .WithPrimaryPrivateIPAddressDynamic()
        .WithNewPrimaryPublicIPAddress(pipDnsLabelWindowsVM)
        .WithPopularWindowsImage(KnownWindowsVirtualMachineImage.WindowsServer2012R2Datacenter)
        .WithAdminUsername(firstWindowsUserName)
        .WithAdminPassword(firstWindowsUserPassword)
        .WithSize(VirtualMachineSizeTypes.StandardD3V2)
        .DefineNewExtension(windowsCustomScriptExtensionName)
            .WithPublisher(windowsCustomScriptExtensionPublisherName)
            .WithType(windowsCustomScriptExtensionTypeName)
            .WithVersion(windowsCustomScriptExtensionVersionName)
            .WithMinorVersionAutoUpgrade()
            .WithPublicSetting("fileUris", mySQLWindowsInstallScriptFileUris)
            .WithPublicSetting("commandToExecute", mySqlScriptWindowsInstallCommand)
        .Attach()
        .Create();

you can browse the full code here - https://github.com/Azure-Samples/compute-dotnet-manage-virtual-machine-using-vm-extensions/blob/master/Program.cs#L208 or clone working project from here - https://github.com/Azure-Samples/compute-dotnet-manage-virtual-machine-using-vm-extensions

0reactions
hovsepmcommented, Jan 22, 2018

FYI we have a lot of sample that may be useful for your scenarios. Please check https://github.com/Azure/azure-libraries-for-net/blob/master/README.md the front page of the repo. It has a lot of sample links.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Custom Script Extension for Windows - Azure
The Custom Script Extension downloads and runs scripts on Azure virtual machines (VMs). Use this extension for post-deployment configuration ...
Read more >
Run powershell script on Azure VM with Azure Fluent API
According to my test, we can use the following code to install MySQL with custom script extension. var credentials = SdkContext.
Read more >
Automation - #4 - Custom Script Extension - YouTube
Learn how to work with Custom Script Extensions today at The Azure Academy Patreon - https://www.patreon.com/AzureAcademy Twitter ...
Read more >
Elasticsearch output plugin | Logstash Reference [8.9]
If the fields are missing, routing defaults to logs-generic-logstash . Example: Customize data stream name. output { elasticsearch { hosts => ...
Read more >
Azure custom script extension timeout when deploying with ...
I tried creating a replica for azure vm extension resource by using below terraform code and it deployed successfully. timeout block:
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found