Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance orchestration template parameter type support #105

Merged
merged 3 commits into from
Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class ManageIQ::Providers::Openstack::CloudManager::OrchestrationStack < ManageI

def self.raw_create_stack(orchestration_manager, stack_name, template, options = {})
create_options = {:stack_name => stack_name, :template => template.content}.merge(options).except(:tenant_name)
transform_parameters(template, create_options[:parameters]) if create_options[:parameters]
connection_options = {:service => "Orchestration"}.merge(options.slice(:tenant_name))
orchestration_manager.with_provider_connection(connection_options) do |service|
service.stacks.new.save(create_options)["id"]
Expand All @@ -14,6 +15,7 @@ def self.raw_create_stack(orchestration_manager, stack_name, template, options =

def raw_update_stack(template, options)
update_options = {:template => template.content}.merge(options.except(:disable_rollback, :timeout_mins))
self.class.transform_parameters(template, update_options[:parameters]) if update_options[:parameters]
connection_options = {:service => "Orchestration"}
connection_options[:tenant_name] = cloud_tenant.name if cloud_tenant
ext_management_system.with_provider_connection(connection_options) do |service|
Expand Down Expand Up @@ -51,4 +53,18 @@ def raw_status
_log.error "stack=[#{name}], error: #{err}"
raise MiqException::MiqOrchestrationStatusError, err.to_s, err.backtrace
end

def self.transform_parameters(template, deploy_parameters)
list_re = /^(comma_delimited_list)|(CommaDelimitedList)|(List<.+>)$/
# convert multiline text to comma delimited string
template.parameter_groups.each do |group|
group.parameters.each do |para_def|
next unless para_def.data_type =~ list_re
parameter = deploy_parameters[para_def.name]
next if parameter.nil? || !parameter.kind_of?(String)
parameter.chomp!('')
parameter.tr!("\n", ",")
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ def parameter_groups
end
end

# Parsing parameters for both Hot and Cloudformation formats
# Keys in Hot are lower case snake style,
# label, json, boolean, constraints are Hot only
# Keys in Cloudformation are upper case camel style
# List<> is Cloudformation only
def parameters(content_hash = nil)
content_hash ||= parse
(content_hash["parameters"] || content_hash["Parameters"] || {}).collect do |key, val|
OrchestrationTemplate::OrchestrationParameter.new(
:name => key,
:label => val.key?('label') ? val['label'] : key.titleize,
:data_type => val['type'],
:default_value => val['default'],
:description => val['description'],
:hidden => val['hidden'] == true,
:constraints => val.key?('constraints') ? parse_constraints(val['constraints']) : nil,
:data_type => val['type'] || val['Type'],
:default_value => parse_default_value(val),
:description => val['description'] || val['Description'],
:hidden => parse_hidden(val),
:constraints => ([constraint_from_type(val)] + parse_constraints(val)).compact,
:required => true
)
end
Expand Down Expand Up @@ -97,58 +102,156 @@ def validate_format_json
err.message
end

def parse_constraints(raw_constraints)
raw_constraints.collect do |raw_constraint|
def parse_default_value(parameter)
raw_default = parameter['default'] || parameter['Default']
case parameter['type'] || parameter['Type']
when 'json'
JSON.pretty_generate(raw_default || {'sample(please delete)' => 'JSON format'})
when 'comma_delimited_list', 'CommaDelimitedList', /^List<.+>$/
multiline_value_for_list(raw_default)
when 'boolean'
([true, 1] + %w(t T y Y yes Yes YES true True TRUE 1)).include?(raw_default)
else
raw_default
end
end

def multiline_value_for_list(val)
return val.join("\n") if val.kind_of?(Array)
return val.tr!(",", "\n") if val.kind_of?(String)
"sample1(please delete)\nsample2(please delete)"
end

def parse_hidden(parameter)
val = parameter.key?('hidden') ? parameter['hidden'] : parameter['NoEcho']
return true if val == true || val == 'true'
false
end

def constraint_from_type(parameter)
case parameter['type'] || parameter['Type']
when 'json'
OrchestrationTemplate::OrchestrationParameterMultiline.new(
:description => 'Parameter in JSON format'
)
when 'comma_delimited_list', 'CommaDelimitedList', /^List<.*>$/
OrchestrationTemplate::OrchestrationParameterMultiline.new(
:description => 'Parameter in list format'
)
when 'boolean'
OrchestrationTemplate::OrchestrationParameterBoolean.new
when 'number', 'Number'
OrchestrationTemplate::OrchestrationParameterPattern.new(
:pattern => '^[+-]?([1-9]\d*|0)(\.\d+)?$',
:description => 'Numeric parameter'
)
end
end

def parse_constraints(parameter)
return parse_constraints_hot(parameter['constraints']) if parameter.key?('constraints')
parse_constraints_cfn(parameter)
end

def parse_constraints_hot(raw_constraints)
(raw_constraints || []).collect do |raw_constraint|
if raw_constraint.key?('allowed_values')
parse_allowed_values(raw_constraint)
parse_allowed_values_hot(raw_constraint)
elsif raw_constraint.key?('allowed_pattern')
parse_pattern(raw_constraint)
parse_pattern_hot(raw_constraint)
elsif raw_constraint.key?('length')
parse_length_constraint(raw_constraint)
parse_length_constraint_hot(raw_constraint)
elsif raw_constraint.key?('range')
parse_value_constraint(raw_constraint)
parse_value_constraint_hot(raw_constraint)
elsif raw_constraint.key?('custom_constraint')
parse_custom_constraint(raw_constraint)
parse_custom_constraint_hot(raw_constraint)
else
raise MiqException::MiqParsingError, _("Unknown constraint %{constraint}") % {:constraint => raw_constraint}
end
end
end

def parse_allowed_values(hash)
def parse_allowed_values_hot(hash)
OrchestrationTemplate::OrchestrationParameterAllowed.new(
:allowed_values => hash['allowed_values'],
:description => hash['description']
)
end

def parse_pattern(hash)
def parse_pattern_hot(hash)
OrchestrationTemplate::OrchestrationParameterPattern.new(
:pattern => hash['allowed_pattern'],
:description => hash['description']
)
end

def parse_length_constraint(hash)
def parse_length_constraint_hot(hash)
OrchestrationTemplate::OrchestrationParameterLength.new(
:min_length => hash['length']['min'],
:max_length => hash['length']['max'],
:description => hash['description']
)
end

def parse_value_constraint(hash)
def parse_value_constraint_hot(hash)
OrchestrationTemplate::OrchestrationParameterRange.new(
:min_value => hash['range']['min'],
:max_value => hash['range']['max'],
:description => hash['description']
)
end

def parse_custom_constraint(hash)
def parse_custom_constraint_hot(hash)
OrchestrationTemplate::OrchestrationParameterCustom.new(
:custom_constraint => hash['custom_constraint'],
:description => hash['description']
)
end

def parse_constraints_cfn(raw_constraints)
constraints = []
if raw_constraints.key?('AllowedValues')
constraints << parse_allowed_values_cfn(raw_constraints)
end
if raw_constraints.key?('AllowedPattern')
constraints << parse_pattern_cfn(raw_constraints)
end
if raw_constraints.key?('MinLength') || raw_constraints.key?('MaxLength')
constraints << parse_length_constraint_cfn(raw_constraints)
end
if raw_constraints.key?('MinValue') || raw_constraints.key?('MaxValue')
constraints << parse_value_constraint_cfn(raw_constraints)
end
constraints
end

def parse_allowed_values_cfn(hash)
OrchestrationTemplate::OrchestrationParameterAllowed.new(
:allowed_values => hash['AllowedValues'],
:description => hash['ConstraintDescription']
)
end

def parse_pattern_cfn(hash)
OrchestrationTemplate::OrchestrationParameterPattern.new(
:pattern => hash['AllowedPattern'],
:description => hash['ConstraintDescription']
)
end

def parse_length_constraint_cfn(hash)
OrchestrationTemplate::OrchestrationParameterLength.new(
:min_length => hash['MinLength'].to_i,
:max_length => hash['MaxLength'].to_i,
:description => hash['ConstraintDescription']
)
end

def parse_value_constraint_cfn(hash)
OrchestrationTemplate::OrchestrationParameterRange.new(
:min_value => hash['MinValue'].to_r,
:max_value => hash['MaxValue'].to_r,
:description => hash['ConstraintDescription']
)
end
end
89 changes: 88 additions & 1 deletion spec/fixtures/orchestration_templates/heat_parameters.json
Original file line number Diff line number Diff line change
@@ -1 +1,88 @@
{"heat_template_version":"2013-05-23","description":"Incomplete sample template for testing parsing of various parameters","parameter_groups":[{"label":"General parameters","description":"General parameters","parameters":["flavor","image_id","cartridges"]},{"parameters":["admin_pass","db_port","metadata"]}],"parameters":{"admin_pass":{"type":"string","description":"Admin password","hidden":true,"constraints":[{"length":{"min":6,"max":8},"description":"Admin password must be between 6 and 8 characters long.\n"},{"allowed_pattern":"[a-zA-Z0-9]+","description":"Password must consist of characters and numbers only"},{"allowed_pattern":"[A-Z]+[a-zA-Z0-9]*","description":"Password must start with an uppercase character"}]},"flavor":{"type":"string","description":"Flavor for the instances to be created","default":"m1.small","constraints":[{"custom_constraint":"nova.flavor","description":"Must be a flavor known to Nova"}]},"cartridges":{"description":"Cartridges to install. \"all\" for all cartridges; \"standard\" for all cartridges except for JBossEWS or JBossEAP\n","type":"string","default":"cron,diy,haproxy,mysql,nodejs,perl,php,postgresql,python,ruby"},"db_port":{"type":"number","label":"Port Number","description":"Database port number","default":50000,"constraints":[{"range":{"min":40000,"max":60000},"description":"Port number must be between 40000 and 60000"}]},"image_id":{"type":"string","description":"ID of the image to use for the instance to be created.","default":"F18-x86_64-cfntools","constraints":[{"allowed_values":["F18-i386-cfntools","F18-x86_64-cfntools"],"description":"Image ID must be either F18-i386-cfntools or F18-x86_64-cfntools."}]},"metadata":{"type":"json"}}}
{
"heat_template_version": "2013-05-23",
"description": "Incomplete sample template for testing parsing of various parameters",
"parameter_groups": [
{
"label": "General parameters",
"description": "General parameters",
"parameters": [
"flavor",
"image_id",
"cartridges"
]
},
{
"parameters": [
"admin_pass",
"db_port",
"metadata",
"skip_failed",
"subnets",
"my_key_pair"
]
}
],
"parameters": {
"admin_pass": {
"Type": "String",
"Description": "Admin password",
"NoEcho": true,
"MinLength": "6",
"MaxLength": "8",
"AllowedPattern": "[a-zA-Z0-9]+",
"ConstraintDescription": "Admin password must be between 6 and 8 characters long. Password must consist of characters and numbers only."
},
"flavor": {
"type": "string",
"description": "Flavor for the instances to be created",
"default": "m1.small",
"constraints": [
{
"custom_constraint": "nova.flavor",
"description": "Must be a flavor known to Nova"
}
]
},
"cartridges": {
"Description": "Cartridges to install. \"all\" for all cartridges; \"standard\" for all cartridges except for JBossEWS or JBossEAP\n",
"Type": "CommaDelimitedList",
"Default": "cron,diy,haproxy,mysql,nodejs,perl,php,postgresql,python,ruby"
},
"db_port": {
"Type": "Number",
"label": "Port Number",
"Description": "Database port number",
"Default": "50000",
"MinValue": "40000",
"MaxValue": "60000",
"ConstraintDescription": "Port number must be between 40000 and 60000"
},
"image_id": {
"Type": "string",
"Description": "ID of the image to use for the instance to be created.",
"Default": "F18-x86_64-cfntools",
"AllowedValues": ["F18-i386-cfntools", "F18-x86_64-cfntools"],
"ConstraintDescription": "Image ID must be either F18-i386-cfntools or F18-x86_64-cfntools."
},
"metadata": {
"type": "json",
"default": {
"ver": "test"
}
},
"skip_failed": {
"type": "boolean",
"default": "t"
},
"subnets": {
"Description": "Subnet IDs",
"Type": "List<AWS::EC2::Subnet::Id>",
"Default": ["subnet-123a351e", "subnet-123a351f"]
},
"my_key_pair": {
"Description": "Amazon EC2 key pair",
"Type": "AWS::EC2::KeyPair::KeyName",
"Default": "my-key"
}
}
}
Loading