Issue pushing custom script extension via Fluent SDK
See original GitHub issueHi,
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:
- Created 6 years ago
- Reactions:2
- Comments:10 (5 by maintainers)
Top GitHub Comments
@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:
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
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.