diff --git a/app/models/manageiq/providers/azure_stack/cloud_manager.rb b/app/models/manageiq/providers/azure_stack/cloud_manager.rb index 5f667b0..a04a479 100644 --- a/app/models/manageiq/providers/azure_stack/cloud_manager.rb +++ b/app/models/manageiq/providers/azure_stack/cloud_manager.rb @@ -7,6 +7,8 @@ class ManageIQ::Providers::AzureStack::CloudManager < ManageIQ::Providers::Cloud require_nested :RefreshWorker require_nested :Vm require_nested :OrchestrationStack + require_nested :OrchestrationTemplate + require_nested :OrchestrationServiceOptionConverter include ManageIQ::Providers::AzureStack::ManagerMixin diff --git a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_service_option_converter.rb b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_service_option_converter.rb new file mode 100644 index 0000000..c442666 --- /dev/null +++ b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_service_option_converter.rb @@ -0,0 +1,11 @@ +module ManageIQ::Providers + class AzureStack::CloudManager::OrchestrationServiceOptionConverter < ::ServiceOrchestration::OptionConverter + def stack_create_options + { + :parameters => stack_parameters, + :resource_group => @dialog_options['dialog_resource_group'].presence || @dialog_options['dialog_new_resource_group'], + :mode => @dialog_options['dialog_deploy_mode'] + } + end + end +end diff --git a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack.rb b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack.rb index 19d9d44..e1dc729 100644 --- a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack.rb +++ b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack.rb @@ -1,7 +1,85 @@ class ManageIQ::Providers::AzureStack::CloudManager::OrchestrationStack < ManageIQ::Providers::CloudManager::OrchestrationStack require_nested :Status + def self.raw_create_stack(ems, stack_name, template, options = {}) + create_or_update_stack(ems, stack_name, template, options) + rescue => err + $azure_stack_log.error("stack=[#{stack_name}], error: #{err}") + raise MiqException::MiqOrchestrationProvisionError, err.to_s, err.backtrace + end + + def raw_update_stack(template, options) + self.class.create_or_update_stack(ext_management_system, name, template, options) + rescue => err + $azure_stack_log.error("stack=[#{name}], error: #{err}") + raise MiqException::MiqOrchestrationUpdateError, err.to_s, err.backtrace + end + + def raw_delete_stack + $azure_stack_log.debug("Deleting orchestration stack (ems=#{ext_management_system.name}, stack_name=#{name})") + ext_management_system.with_provider_connection(:service => :Resources) do |client| + # TODO(miha-plesko): this only deletes the deployment leaving all resources still there. Need to remove those too. + client.deployments.delete(resource_group, name) + end + rescue => err + $azure_stack_log.error("stack=[#{name}], error: #{err}") + raise MiqException::MiqOrchestrationDeleteError, err.to_s, err.backtrace + end + + def raw_status + ext_management_system.with_provider_connection(:service => :Resources) do |client| + state = client.deployments.get(resource_group, name).properties.provisioning_state.downcase + Status.new(state, state == 'succeeded' ? 'OK' : failure_reason(client)) + end + rescue MsRestAzure::AzureOperationError => err + $azure_stack_log.error("stack=[#{name}], error: #{err}") + raise MiqException::MiqOrchestrationStackNotExistError, err.to_s, err.backtrace if err&.error_code == 'DeploymentNotFound' + + raise MiqException::MiqOrchestrationStatusError, err.to_s, err.backtrace + rescue => err + $azure_stack_log.error("stack=[#{name}], error: #{err}") + raise MiqException::MiqOrchestrationStatusError, err.to_s, err.backtrace + end + + def self.build_ems_ref(ems, resource_group, stack_name) + "/subscriptions/#{ems.subscription}"\ + "/resourcegroups/#{resource_group}"\ + "/providers/microsoft.resources/deployments/#{stack_name}".downcase + end + def self.display_name(number = 1) n_('Orchestration Stack (Microsoft AzureStack)', 'Orchestration Stacks (Microsoft AzureStack)', number) end + + def self.create_or_update_stack(ems, stack_name, template, options) + $azure_stack_log.debug("Creating/Updating orchestration stack [ems=#{ems.name}, " \ + "stack_name=#{stack_name}, template=#{template.name}, options=#{options}]") + ems.with_provider_connection(:service => :Resources) do |client| + # Ensure resource group exists because deployment assumes existing one. + client.resource_groups.create_or_update( + options[:resource_group], + client.model_classes.resource_group.new.tap { |g| g.location = ems.provider_region } + ) + # Deploy into the resource group. + deployment = client.model_classes.deployment.new + deployment.properties = client.model_classes.deployment_properties.new.tap do |props| + props.template = JSON.parse(template.content) + props.mode = options[:mode] + props.parameters = options[:parameters].transform_values! { |v| { 'value' => v } } + end + + client.deployments.create_or_update_async(options[:resource_group], stack_name, deployment) + build_ems_ref(ems, options[:resource_group], stack_name) + end + end + + private + + def failure_reason(client) + operations = client.deployment_operations.list(resource_group, name) + msg = operations.detect { |op| op.properties.provisioning_state.downcase != 'succeeded' }&.properties&.status_message + return nil unless msg && (reason = msg['error']) + + "[#{reason['code']}][#{reason['target']}] #{reason['message']}" + end end diff --git a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack/status.rb b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack/status.rb index 427f962..8afb8c0 100644 --- a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack/status.rb +++ b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack/status.rb @@ -1,4 +1,4 @@ -class ManageIQ::Providers::Azure::CloudManager::OrchestrationStack::Status < ::OrchestrationStack::Status +class ManageIQ::Providers::AzureStack::CloudManager::OrchestrationStack::Status < ::OrchestrationStack::Status def succeeded? status.downcase == "succeeded" end diff --git a/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template.rb b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template.rb new file mode 100644 index 0000000..51ccb44 --- /dev/null +++ b/app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template.rb @@ -0,0 +1,107 @@ +class ManageIQ::Providers::AzureStack::CloudManager::OrchestrationTemplate < ::OrchestrationTemplate + def format + 'json'.freeze + end + + def parameter_groups + [OrchestrationTemplate::OrchestrationParameterGroup.new( + :label => 'Parameters', + :parameters => parameters + )] + end + + def parameters + raw_parameters = JSON.parse(content)['parameters'] + (raw_parameters || {}).collect do |key, val| + parameter = OrchestrationTemplate::OrchestrationParameter.new( + :name => key, + :label => key.titleize, + :data_type => val['type'], + :default_value => val['defaultValue'], + :hidden => val['type'].casecmp('securestring').zero?, + :required => true + ) + + add_metadata(parameter, val['metadata']) + add_allowed_values(parameter, val['allowedValues']) + + parameter + end + end + + def deployment_options(_manager_class = nil) + super << resource_group_opt << new_resource_group_opt << mode_opt + end + + def self.eligible_manager_types + [ManageIQ::Providers::AzureStack::CloudManager] + end + + # return the parsing error message if not valid JSON; otherwise nil + def validate_format + JSON.parse(content) && nil if content + rescue JSON::ParserError => err + err.message + end + + def self.display_name(number = 1) + n_('AzureStack Template', 'AzureStack Templates', number) + end + + private + + def mode_opt + description = 'Select deployment mode.'\ + 'WARNING: Complete mode will delete all resources from '\ + 'the group that are not in the template.' + choices = {'Incremental' => 'Incremental', 'Complete' => 'Complete'} + OrchestrationTemplate::OrchestrationParameter.new( + :name => 'deploy_mode', + :label => 'Mode', + :data_type => 'string', + :description => description, + :default_value => 'Incremental', + :required => true, + :constraints => [OrchestrationTemplate::OrchestrationParameterAllowed.new(:allowed_values => choices)] + ) + end + + def resource_group_opt + OrchestrationTemplate::OrchestrationParameter.new( + :name => 'resource_group', + :label => 'Existing Resource Group', + :data_type => 'string', + :description => 'Select an existing resource group for deployment', + :constraints => [ + OrchestrationTemplate::OrchestrationParameterAllowedDynamic.new( + :fqname => '/Cloud/Orchestration/Operations/Methods/Available_Resource_Groups' + ) + ] + ) + end + + def new_resource_group_opt + OrchestrationTemplate::OrchestrationParameter.new( + :name => 'new_resource_group', + :label => '(or) New Resource Group', + :data_type => 'string', + :description => 'Create a new resource group upon deployment', + :constraints => [ + OrchestrationTemplate::OrchestrationParameterPattern.new(:pattern => '^[A-Za-z][A-Za-z0-9\-_]*$') + ] + ) + end + + def add_metadata(parameter, metadata) + return unless metadata + + parameter.description = metadata['description'] + end + + def add_allowed_values(parameter, vals) + return unless vals + + constraint = OrchestrationTemplate::OrchestrationParameterAllowed.new(:allowed_values => vals) + parameter.constraints << constraint + end +end diff --git a/spec/factories/orchestration_stack.rb b/spec/factories/orchestration_stack.rb new file mode 100644 index 0000000..4e2f098 --- /dev/null +++ b/spec/factories/orchestration_stack.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :orchestration_stack_azure_stack, + :parent => :orchestration_stack, + :class => "ManageIQ::Providers::AzureStack::CloudManager::OrchestrationStack" +end diff --git a/spec/factories/orchestration_template.rb b/spec/factories/orchestration_template.rb new file mode 100644 index 0000000..5f3beec --- /dev/null +++ b/spec/factories/orchestration_template.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :orchestration_template_azure_stack, + :parent => :orchestration_template, + :class => 'ManageIQ::Providers::AzureStack::CloudManager::OrchestrationTemplate' do + content { File.read(ManageIQ::Providers::AzureStack::Engine.root.join('spec', 'fixtures', 'orchestration_templates', 'deployment.json')) } + end +end diff --git a/spec/fixtures/orchestration_templates/deployment.json b/spec/fixtures/orchestration_templates/deployment.json new file mode 100644 index 0000000..8aefea1 --- /dev/null +++ b/spec/fixtures/orchestration_templates/deployment.json @@ -0,0 +1,198 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "apiProfile": "2018-03-01-hybrid", + "parameters": { + "adminUsername": { + "type": "string", + "metadata": { + "description": "Username for the Virtual Machine. Default value is localadmin" + }, + "defaultValue": "demoUsername" + }, + "adminPassword": { + "type": "securestring", + "defaultValue": "demoPassword123456", + "metadata": { + "description": "Password for the Virtual Machine. Default value is 'Subscription#'" + } + }, + "imagePublisher": { + "type": "string", + "defaultValue": "Canonical", + "metadata": { + "description": "Maps to the publisher in the Azure Stack Platform Image Repository manifest file Eg: Canonical, Suse, OpenLogic " + } + }, + "imageOffer": { + "type": "string", + "defaultValue": "UbuntuServer", + "metadata": { + "description": "Maps to the Offer in the Azure Stack Platform Image Repository manifest file Eg: UbuntuServer, SlesServer, CentOS " + } + }, + "imageSku": { + "type": "string", + "defaultValue": "16.04-LTS", + "metadata": { + "description": "Maps to the sku in the Azure Stack Platform Image Repository manifest file Eg: 12.SP1, 6.7 , 7.2" + }, + "allowedValues": [ + "16.04-LTS", + "14.04-LTS" + ] + }, + "vmSize": { + "type": "string", + "defaultValue": "Standard_A1", + "metadata": { + "description": "The size of the Virtual Machine." + } + } + }, + "variables": { + "dnsNameForPublicIP": "demo.dns.name", + "location": "[resourceGroup().location]", + "OSDiskName": "osdisk", + "nicName": "demoNic0", + "addressPrefix": "10.0.0.0/24", + "subnetName": "demoSubnet", + "subnetPrefix": "10.0.0.0/24", + "storageAccountName": "demoStorageAccount", + "storageAccountType": "Standard_LRS", + "vmStorageAccountContainerName": "vhds", + "vmName": "demoVm", + "virtualNetworkName": "demoNetwork", + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "networkSecurityGroupName": "demoSecurityGroup" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[toLower(variables('storageAccountName'))]", + "location": "[variables('location')]", + "properties": { + "accountType": "[variables('storageAccountType')]" + }, + "apiVersion": "2015-06-15" + }, + { + "type": "Microsoft.Network/networkSecurityGroups", + "name": "[variables('networkSecurityGroupName')]", + "location": "[resourceGroup().location]", + "properties": { + "securityRules": [ + { + "name": "ssh", + "properties": { + "description": "Allow RDP", + "protocol": "Tcp", + "sourcePortRange": "*", + "destinationPortRange": "22", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 200, + "direction": "Inbound" + } + } + ] + } + }, + { + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[variables('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('addressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "[variables('subnetPrefix')]" + } + } + ] + } + }, + { + "type": "Microsoft.Network/networkInterfaces", + "name": "[variables('nicName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]", + "[variables('networkSecurityGroupName')]" + ], + "properties": { + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]" + }, + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "name": "[variables('vmName')]", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "osProfile": { + "computerName": "[variables('vmName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "[parameters('imagePublisher')]", + "offer": "[parameters('imageOffer')]", + "sku": "[parameters('imageSku')]", + "version": "latest" + }, + "osDisk": { + "name": "osdisk", + "vhd": { + "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).primaryEndpoints.blob, variables('vmStorageAccountContainerName'),'/', variables('OSDiskName'), '.vhd')]" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "true", + "storageUri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).primaryEndpoints.blob)]" + } + } + } + } + ] +} + diff --git a/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack_spec.rb b/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack_spec.rb new file mode 100644 index 0000000..70f7c92 --- /dev/null +++ b/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack_spec.rb @@ -0,0 +1,165 @@ +require 'ms_rest_azure' +require 'azure_mgmt_resources' + +describe ManageIQ::Providers::AzureStack::CloudManager::OrchestrationStack do + supported_api_versions do |api_version| + let(:ems) { FactoryBot.create(:ems_azure_stack_with_authentication, :provider_region => 'region', :subscription => '123') } + let(:template) { FactoryBot.create(:orchestration_template_azure_stack, :content => '{}') } + let(:client) { Azure::Resources::Profiles.const_get(api_version)::Mgmt::Client.new } + + before do + allow_any_instance_of(MsRestAzure::Common::Configurable).to receive(:reset!) + allow(ems).to receive(:connect).with(:service => :Resources).and_return(client) + end + + subject do + FactoryBot.create(:orchestration_stack_azure_stack, + :ext_management_system => ems, + :name => 'stack-name', + :resource_group => 'resource-group') + end + + describe '.raw_create_stack' do + context 'when succeeds' do + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:resource_groups, :create_or_update) do |resource_group, props| + expect(resource_group).to eq('resource-group') + expect(props.location).to eq('region') + end + expect(client).to receive_message_chain(:deployments, :create_or_update_async) do |resource_group, stack_name, deployment| + expect(resource_group).to eq('resource-group') + expect(stack_name).to eq('stack-name') + expect(deployment.properties).to have_attributes( + :template => {}, + :mode => 'mode', + :parameters => { 'param1' => { 'value' => 'value1' } } + ) + end + + options = { + :resource_group => 'resource-group', + :mode => 'mode', + :parameters => { 'param1' => 'value1' } + } + ems_ref = described_class.raw_create_stack(ems, 'stack-name', template, options) + expect(ems_ref).to eq('/subscriptions/123/resourcegroups/resource-group/providers/microsoft.resources/deployments/stack-name') + end + end + + context 'when API fails' do + it 'MIQ error is raised' do + expect(client).to receive_message_chain(:resource_groups, :create_or_update).and_raise(ArgumentError) + expect { described_class.raw_create_stack(ems, 'stack-name', template, {}) }.to raise_error(MiqException::MiqOrchestrationProvisionError) + end + end + end + + describe '.raw_update_stack' do + context 'when succeeds' do + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:resource_groups, :create_or_update) do |resource_group, props| + expect(resource_group).to eq('resource-group') + expect(props.location).to eq('region') + end + expect(client).to receive_message_chain(:deployments, :create_or_update_async) do |resource_group, stack_name, deployment| + expect(resource_group).to eq('resource-group') + expect(stack_name).to eq('stack-name') + expect(deployment.properties).to have_attributes( + :template => {}, + :mode => 'mode', + :parameters => { 'param1' => { 'value' => 'value1' } } + ) + end + + options = { + :resource_group => 'resource-group', + :mode => 'mode', + :parameters => { 'param1' => 'value1' } + } + ems_ref = subject.raw_update_stack(template, options) + expect(ems_ref).to eq('/subscriptions/123/resourcegroups/resource-group/providers/microsoft.resources/deployments/stack-name') + end + end + + context 'when API fails' do + it 'MIQ error is raised' do + expect(client).to receive_message_chain(:resource_groups, :create_or_update).and_raise(ArgumentError) + expect { subject.raw_update_stack(template, {}) }.to raise_error(MiqException::MiqOrchestrationUpdateError) + end + end + end + + describe '.raw_delete_stack' do + context 'when succeeds' do + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:deployments, :delete) do |resource_group, name| + expect(resource_group).to eq('resource-group') + expect(name).to eq('stack-name') + end + subject.raw_delete_stack + end + end + + context 'when API fails' do + it 'MIQ error is raised' do + expect(client).to receive_message_chain(:deployments, :delete).and_raise(ArgumentError) + expect { subject.raw_delete_stack }.to raise_error(MiqException::MiqOrchestrationDeleteError) + end + end + end + + describe '.raw_status' do + let(:deployment) { double('deployment', :properties => double(:provisioning_state => status)) } + + context 'when stack not exist' do + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:deployments, :get).and_raise( + MsRestAzure::AzureOperationError.new('msg').tap { |e| e.error_code = 'DeploymentNotFound' } + ) + expect { subject.raw_status }.to raise_error(MiqException::MiqOrchestrationStackNotExistError) + end + end + + context 'when succeeds' do + let(:status) { 'Succeeded' } + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:deployments, :get).with('resource-group', 'stack-name').and_return(deployment) + status = subject.raw_status + expect(status).to have_attributes(:status => 'succeeded', :reason => 'OK') + end + end + + context 'when fails' do + let(:status) { 'Failed' } + let(:operation) do + double( + 'operation', + :properties => double( + :provisioning_state => status, + :status_message => { + 'error' => { + 'code' => 'CODE', + 'target' => 'TARGET', + 'message' => 'MESSAGE' + } + } + ) + ) + end + it 'properly constructs and invokes API' do + expect(client).to receive_message_chain(:deployments, :get).with('resource-group', 'stack-name').and_return(deployment) + expect(client).to receive_message_chain(:deployment_operations, :list).with('resource-group', 'stack-name').and_return([operation]) + status = subject.raw_status + expect(status).to have_attributes(:status => 'failed', :reason => '[CODE][TARGET] MESSAGE') + end + end + + context 'when API fails' do + it 'MIQ error is raised' do + expect(client).to receive_message_chain(:deployments, :get).and_raise(ArgumentError) + expect { subject.raw_status }.to raise_error(MiqException::MiqOrchestrationStatusError) + end + end + end + end +end diff --git a/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template_spec.rb b/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template_spec.rb new file mode 100644 index 0000000..01f29d7 --- /dev/null +++ b/spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template_spec.rb @@ -0,0 +1,104 @@ +describe ManageIQ::Providers::AzureStack::CloudManager::OrchestrationTemplate do + describe '.eligible_manager_types' do + it 'lists the classes of eligible managers' do + described_class.eligible_manager_types.each do |klass| + expect(klass <= ManageIQ::Providers::AzureStack::CloudManager).to be_truthy + end + end + end + + let(:valid_template) { FactoryBot.create(:orchestration_template_azure_stack) } + + context 'when a raw template in JSON format is given' do + it 'parses parameters from a template' do + groups = valid_template.parameter_groups + expect(groups.size).to eq(1) + expect(groups[0].label).to eq('Parameters') + + param_hash = groups[0].parameters.index_by(&:name) + expect(param_hash.size).to eq(6) + + assert_string_type(param_hash['adminUsername']) + assert_secret_type(param_hash['adminPassword']) + assert_allowed_values(param_hash['imageSku']) + end + end + + def assert_secret_type(parameter) + expect(parameter).to have_attributes( + :name => 'adminPassword', + :label => 'Admin Password', + :description => "Password for the Virtual Machine. Default value is 'Subscription#'", + :data_type => 'securestring', + :default_value => 'demoPassword123456', + :hidden => true, + :required => true, + :constraints => [] + ) + end + + def assert_string_type(parameter) + expect(parameter).to have_attributes( + :name => 'adminUsername', + :label => 'Admin Username', + :description => 'Username for the Virtual Machine. Default value is localadmin', + :data_type => 'string', + :default_value => 'demoUsername', + :hidden => false, + :required => true, + :constraints => [] + ) + end + + def assert_allowed_values(parameter) + expect(parameter).to have_attributes( + :name => 'imageSku', + :label => 'Image Sku', + :description => 'Maps to the sku in the Azure Stack Platform Image Repository manifest file Eg: 12.SP1, 6.7 , 7.2', + :data_type => 'string', + :default_value => '16.04-LTS', + :hidden => false, + :required => true + ) + constraints = parameter.constraints + expect(constraints.size).to eq(1) + expect(constraints[0]).to be_a OrchestrationTemplate::OrchestrationParameterAllowed + expect(constraints[0]).to be_kind_of OrchestrationTemplate::OrchestrationParameterConstraint + expect(constraints[0]).to have_attributes( + :description => nil, + :allowed_values => ['16.04-LTS', '14.04-LTS'] + ) + end + + describe '#validate_format' do + it 'passes validation if no content' do + template = described_class.new + expect(template.validate_format).to be_nil + end + + it 'passes validation with correct JSON content' do + expect(valid_template.validate_format).to be_nil + end + + it 'fails validations with incorrect JSON content' do + template = described_class.new(:content => 'invalid string') + expect(template.validate_format).not_to be_nil + end + end + + describe '#deployment_options' do + it do + options = subject.deployment_options + assert_deployment_option(options[0], 'stack_name', :OrchestrationParameterPattern, true) + assert_deployment_option(options[1], 'resource_group', :OrchestrationParameterAllowedDynamic, false) + assert_deployment_option(options[2], 'new_resource_group', :OrchestrationParameterPattern, false) + assert_deployment_option(options[3], 'deploy_mode', :OrchestrationParameterAllowed, true) + end + end + + def assert_deployment_option(option, name, constraint_type, required) + expect(option.name).to eq(name) + expect(option.required?).to eq(required) + expect(option.constraints[0]).to be_kind_of("OrchestrationTemplate::#{constraint_type}".constantize) + end +end