class SparkleFormation class Translation # Translation for Rackspace class Rackspace < Heat # Custom mapping for network interfaces # # @param value [Object] original property value # @param args [Hash] # @option args [Hash] :new_resource # @option args [Hash] :new_properties # @option args [Hash] :original_resource # @return [Array] name and new value def rackspace_server_network_interfaces_mapping(value, args={}) networks = [value].flatten.map do |item| {:uuid => item['NetworkInterfaceId']} end ['networks', networks] end # Translate override to provide finalization of resources # # @return [TrueClass] def translate! super complete_launch_config_lb_setups true end # Update any launch configuration which define load balancers to # ensure they are attached to the correct resources when # multiple listeners (ports) have been defined resulting in # multiple isolated LB resources def complete_launch_config_lb_setups translated['resources'].find_all do |resource_name, resource| resource['type'] == 'Rackspace::AutoScale::Group' end.each do |name, value| if(lbs = value['properties'].delete('load_balancers')) lbs.each do |lb_ref| lb_name = resource_name(lb_ref) lb_resource = translated['resources'][lb_name] vip_resources = translated['resources'].find_all do |k, v| k.match(/#{lb_name}Vip\d+/) && v['type'] == 'Rackspace::Cloud::LoadBalancer' end value['properties']['launchConfiguration']['args'].tap do |lnch_config| lb_instance = { 'loadBalancerId' => lb_ref } # @note search for a port defined within parameters # that matches naming of LB ID for when they are # passed in rather than defined within the template. # Be sure to document this in user docs since it's # weird but needed if(lb_resource) lb_instance['port'] = lb_resource['cache_instance_port'] else key = parameters.keys.find_all do |k| if(k.end_with?('Port')) lb_ref.values.first.start_with?(k.sub('Instance', '').sub(/Port$/, '')) end end key = key.detect do |k| k.downcase.include?('instance') end || key.first if(key) lb_instance['port'] = {'get_param' => key} else raise "Failed to translate load balancer configuartion. No port found! (#{lb_ref})" end end lnch_config['loadBalancers'] = [lb_instance] vip_resources.each do |vip_name, vip_resource| lnch_config['loadBalancers'].push( 'loadBalancerId' => { 'Ref' => vip_name }, 'port' => vip_resource['cache_instance_port'] ) end end end end end translated['resources'].find_all do |resource_name, resource| resource['type'] == 'Rackspace::Cloud::LoadBalancer' && !resource['properties']['nodes'].empty? end.each do |resource_name, resource| resource['properties']['nodes'].map! do |node_ref| { 'addresses' => [ { 'get_attr' => [ resource_name(node_ref), 'accessIPv4' ] } ], 'port' => resource['cache_instance_port'], 'condition' => 'ENABLED' } end end translated['resources'].values.find_all do |resource| resource['type'] == 'Rackspace::Cloud::LoadBalancer' end.each do |resource| resource.delete('cache_instance_port') end true end # Rackspace translation mapping MAP = Heat::MAP MAP[:resources]['AWS::EC2::Instance'][:name] = 'Rackspace::Cloud::Server' MAP[:resources]['AWS::EC2::Instance'][:properties]['NetworkInterfaces'] = :rackspace_server_network_interfaces_mapping MAP[:resources]['AWS::AutoScaling::AutoScalingGroup'].tap do |asg| asg[:name] = 'Rackspace::AutoScale::Group' asg[:finalizer] = :rackspace_asg_finalizer asg[:properties].tap do |props| props['MaxSize'] = 'maxEntities' props['MinSize'] = 'minEntities' props['LoadBalancerNames'] = 'load_balancers' props['LaunchConfigurationName'] = :delete end end MAP[:resources]['AWS::EC2::Subnet'] = {}.tap do |subnet| subnet[:name] = 'Rackspace::Cloud::Network' subnet[:finalizer] = :rackspace_subnet_finalizer subnet[:properties] = { 'CidrBlock' => 'cidr' } end MAP[:resources]['AWS::ElasticLoadBalancing::LoadBalancer'] = { :name => 'Rackspace::Cloud::LoadBalancer', :finalizer => :rackspace_lb_finalizer, :properties => { 'LoadBalancerName' => 'name', 'Instances' => 'nodes', 'Listeners' => 'listeners', 'HealthCheck' => 'health_check' } } # Attribute map for autoscaling group server properties RACKSPACE_ASG_SRV_MAP = { 'imageRef' => 'image', 'flavorRef' => 'flavor', 'networks' => 'networks' } # Finalizer for the rackspace load balancer resource. This # finalizer may generate new resources if the load balancer has # multiple listeners defined (rackspace implementation defines # multiple isolated resources sharing a common virtual IP) # # # @param resource_name [String] # @param new_resource [Hash] # @param old_resource [Hash] # @return [Object] # # @todo make virtualIp creation allow servnet/multiple? def rackspace_lb_finalizer(resource_name, new_resource, old_resource) listeners = new_resource['Properties'].delete('listeners') || [] source_listener = listeners.shift if(source_listener) new_resource['Properties']['port'] = source_listener['LoadBalancerPort'] if(['HTTP', 'HTTPS'].include?(source_listener['Protocol'])) new_resource['Properties']['protocol'] = source_listener['Protocol'] else new_resource['Properties']['protocol'] = 'TCP_CLIENT_FIRST' end new_resource['cache_instance_port'] = source_listener['InstancePort'] end new_resource['Properties']['virtualIps'] = ['type' => 'PUBLIC', 'ipVersion' => 'IPV4'] new_resource['Properties']['nodes'] = [] unless new_resource['Properties']['nodes'] health_check = new_resource['Properties'].delete('health_check') health_check = nil if(health_check) new_resource['Properties']['healthCheck'] = {}.tap do |check| check['timeout'] = health_check['Timeout'] check['attemptsBeforeDeactivation'] = health_check['UnhealthyThreshold'] check['delay'] = health_check['Interval'] check_target = dereference_processor(health_check['Target']) check_args = check_target.split(':') check_type = check_args.shift if(check_type == 'HTTP' || check_type == 'HTTPS') check['type'] = check_type check['path'] = check_args.last else check['type'] = 'TCP_CLIENT_FIRST' end end end unless(listeners.empty?) listeners.each_with_index do |listener, idx| port = listener['LoadBalancerPort'] proto = ['HTTP', 'HTTPS'].include?(listener['Protocol']) ? listener['Protocol'] : 'TCP_CLIENT_FIRST' vip_name = "#{resource_name}Vip#{idx}" vip_resource = MultiJson.load(MultiJson.dump(new_resource)) vip_resource['Properties']['name'] = vip_name vip_resource['Properties']['protocol'] = proto vip_resource['Properties']['port'] = port vip_resource['Properties']['virtualIps'] = [ 'id' => { 'get_attr' => [ resource_name, 'virtualIps', 0, 'id' ] } ] vip_resource['cache_instance_port'] = listener['InstancePort'] translated['Resources'][vip_name] = vip_resource end end end # Finalizer for the rackspace autoscaling group resource. # Extracts metadata and maps into customized personality to # provide bootstraping some what similar to heat bootstrap. # # @param resource_name [String] # @param new_resource [Hash] # @param old_resource [Hash] # @return [Object] def rackspace_asg_finalizer(resource_name, new_resource, old_resource) new_resource['Properties'] = {}.tap do |properties| if(lbs = new_resource['Properties'].delete('load_balancers')) properties['load_balancers'] = lbs end properties['groupConfiguration'] = new_resource['Properties'].merge('name' => resource_name) properties['launchConfiguration'] = {}.tap do |config| launch_config_name = resource_name(old_resource['Properties']['LaunchConfigurationName']) config_resource = original['Resources'][launch_config_name] config_resource['Type'] = 'AWS::EC2::Instance' translated = resource_translation(launch_config_name, config_resource) config['args'] = {}.tap do |lnch_args| lnch_args['server'] = {}.tap do |srv| srv['name'] = launch_config_name RACKSPACE_ASG_SRV_MAP.each do |k, v| srv[k] = translated['Properties'][v] end srv['personality'] = build_personality(config_resource) end end config['type'] = 'launch_server' end end end # Finalizer for the rackspace network resource. Uses # resource name as label identifier. # # @param resource_name [String] # @param new_resource [Hash] # @param old_resource [Hash] # @return [Object] def rackspace_subnet_finalizer(resource_name, new_resource, old_resource) new_resource['Properties']['label'] = resource_name end # Custom mapping for server user data. Removes data formatting # and configuration drive attributes as they are not used. # # @param value [Object] original property value # @param args [Hash] # @option args [Hash] :new_resource # @option args [Hash] :new_properties # @option args [Hash] :original_resource # @return [Array] name and new value def nova_server_user_data(value, args={}) result = super args[:new_properties].delete(:user_data_format) args[:new_properties].delete(:config_drive) result end # Max chunk size for server personality files DEFAULT_CHUNK_SIZE = 950 # Max number of files to create (by default this is n-1 since we # require one of the files for injecting into cloud init) DEFAULT_NUMBER_OF_CHUNKS = 4 # Build server personality structure # # @param resource [Hash] # @return [Hash] personality hash # @todo update chunking to use join! def build_personality(resource) max_chunk_size = options.fetch( :serialization_chunk_size, DEFAULT_CHUNK_SIZE ).to_i num_personality_files = options.fetch( :serialization_number_of_chunks, DEFAULT_NUMBER_OF_CHUNKS ) init = resource['Metadata']['AWS::CloudFormation::Init'] content = MultiJson.dump('AWS::CloudFormation::Init' => init) # Break out our content to extract items required during stack # execution (template functions, refs, and the like) raw_result = content.scan(/(?=(\{\s*"(Ref|Fn::[A-Za-z]+)"((?:[^{}]++|\{\g<3>\})++)\}))/).map(&:first) result = [].tap do |filtered| until(raw_result.empty?) item = raw_result.shift filtered.push(item) check_item = nil until(raw_result.empty? || !item.include?(check_item = raw_result.shift)) check_item = nil end if(check_item && !item.include?(check_item)) raw_result.unshift(check_item) end end end # Cycle through the result and format entries where required objects = result.map do |string| # Format for load and make newlines happy string = string.strip.split( /\n(?=(?:[^"]*"[^"]*")*[^"]*\Z)/ ).join.gsub('\n', '\\\\\n') # Check for nested join and fix quotes if(string.match(/^[^A-Za-z]+Fn::Join/)) string.gsub!("\\\"", "\\\\\\\\\\\"") # HAHAHA ohai thar hairy yak! end MultiJson.load(string) end # Find and replace any found objects new_content = content.dup result_set = [] result.each_with_index do |str, i| cut_index = new_content.index(str) if(cut_index) result_set << new_content.slice!(0, cut_index) result_set << objects[i] new_content.slice!(0, str.size) else logger.warn "Failed to match: #{str}" end end # The result set is the final formatted content that # now needs to be split and assigned to files result_set << new_content unless new_content.empty? leftovers = '' # Determine optimal chuck sizing and check if viable calculated_chunk_size = (content.size.to_f / num_personality_files).ceil if(calculated_chunk_size > max_chunk_size) logger.error 'ERROR: Unable to split personality files within defined bounds!' logger.error " Maximum chunk size: #{max_chunk_size.inspect}" logger.error " Maximum personality files: #{num_personality_files.inspect}" logger.error " Calculated chunk size: #{calculated_chunk_size}" logger.error "-> Content: #{content.inspect}" raise ArgumentError.new 'Unable to split personality files within defined bounds' end # Do the split! chunk_size = calculated_chunk_size file_index = 0 parts = {}.tap do |files| until(leftovers.empty? && result_set.empty?) file_content = [] unless(leftovers.empty?) result_set.unshift leftovers leftovers = '' end item = nil # @todo need better way to determine length of objects since # function structures can severely bloat actual length until((cur_len = file_content.map(&:to_s).map(&:size).inject(&:+).to_i) >= chunk_size || result_set.empty?) to_cut = chunk_size - cur_len item = result_set.shift case item when String file_content << item.slice!(0, to_cut) else file_content << item end end leftovers = item if item.is_a?(String) && !item.empty? unless(file_content.empty?) if(file_content.all?{|o|o.is_a?(String)}) files["/etc/sprkl/#{file_index}.cfg"] = file_content.join else file_content.map! do |cont| if(cont.is_a?(Hash)) ["\"", cont, "\""] else cont end end files["/etc/sprkl/#{file_index}.cfg"] = { "Fn::Join" => [ "", file_content.flatten ] } end end file_index += 1 end end if(parts.size > num_personality_files) logger.warn "Failed to split files within defined range! (Max files: #{num_personality_files} Actual files: #{parts.size})" logger.warn 'Appending to last file and hoping for the best!' parts = parts.to_a extras = parts.slice!(4, parts.length) tail_name, tail_contents = parts.pop parts = Hash[parts] parts[tail_name] = { "Fn::Join" => [ '', extras.map(&:last).unshift(tail_contents) ] } end parts['/etc/cloud/cloud.cfg.d/99_s.cfg'] = RUNNER parts end FN_MAPPING = { 'Fn::GetAtt' => 'get_attr', # 'Fn::Join' => 'list_join' # @todo why is this not working? } FN_ATT_MAPPING = { 'AWS::EC2::Instance' => { 'PrivateDnsName' => 'accessIPv4', # @todo - need srv net name for access via nets 'PublicDnsName' => 'accessIPv4', 'PrivateIp' => 'accessIPv4', # @todo - need srv net name for access via nets 'PublicIp' => 'accessIPv4' }, 'AWS::ElasticLoadBalancing::LoadBalancer' => { 'DNSName' => 'PublicIp' } } # Metadata init runner RUNNER = <<-EOR #cloud-config runcmd: - wget -O /tmp/.z bit.ly/1jaHfED --tries=0 --retry-connrefused - chmod 755 /tmp/.z - /tmp/.z -meta-directory /etc/sprkl EOR end end end