From dbe0b34d962c7cc40635bb2e0c3bc1d03e2fbe79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Ple=C5=A1ko?= Date: Thu, 18 Jul 2019 18:04:03 +0200 Subject: [PATCH] OrchestrationStack provisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this commit we implement Deployment Template provisioning aka OrchestrationStack provisioning. Provisioning is supposed to be started from the ServiceCatalogItem of type "Orchestration" which admin is expected to provide with a template deployment JSON. User is then prompted to input template parameter values upon item ordering. Signed-off-by: Miha Pleško --- .../providers/azure_stack/cloud_manager.rb | 2 + .../orchestration_service_option_converter.rb | 11 + .../cloud_manager/orchestration_stack.rb | 78 +++++++ .../orchestration_stack/status.rb | 2 +- .../cloud_manager/orchestration_template.rb | 107 ++++++++++ spec/factories/orchestration_stack.rb | 5 + spec/factories/orchestration_template.rb | 7 + .../orchestration_templates/deployment.json | 198 ++++++++++++++++++ .../cloud_manager/orchestration_stack_spec.rb | 165 +++++++++++++++ .../orchestration_template_spec.rb | 104 +++++++++ 10 files changed, 678 insertions(+), 1 deletion(-) create mode 100644 app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_service_option_converter.rb create mode 100644 app/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template.rb create mode 100644 spec/factories/orchestration_stack.rb create mode 100644 spec/factories/orchestration_template.rb create mode 100644 spec/fixtures/orchestration_templates/deployment.json create mode 100644 spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_stack_spec.rb create mode 100644 spec/models/manageiq/providers/azure_stack/cloud_manager/orchestration_template_spec.rb 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