require 'set' require 'ostruct' require 'yaml' require 'pathname' require 'aws/with-stacco-patches' require 'stacco/base' require 'stacco/template/old' class Stacco::Stack def initialize(stack_bucket) @bucket = stack_bucket @bucket.cache_dir = Pathname.new(ENV['HOME']) + '.config' + 'stacco' + 'stack' + @bucket.name @config_object = @bucket.objects['stack.yml'] aws_creds = self.aws_credentials @services = { ec2: AWS::EC2.new(aws_creds), s3: AWS::S3.new(aws_creds), cloudformation: AWS::CloudFormation.new(aws_creds), cloudfront: AWS::CloudFront.new(aws_creds), iam: AWS::IAM.new(aws_creds) } @aws_stack = @services[:cloudformation].stacks[self.name] @aws_stack.service_registry = @services end def connections connections = {} @aws_stack.instances.each{ |i| connections[i.tags["aws:cloudformation:logical-id"]] = i } connections end def resource_summaries @aws_stack.resource_summaries end def must_be_up! unless self.up? $stderr.puts "stack #{self.name} is down" Kernel.exit 1 end end def aws_status @aws_stack.status end def status self.up? ? self.aws_status : "DOWN" end def config YAML.load(@config_object.read) end def config=(new_config) @config_object.write(new_config.to_yaml) end def update_config # TODO end def aws_credentials Hash[ *(self.config['aws'].map{ |k, v| [k.intern, v] }.flatten) ] end def description self.config['description'] end def name self.config['name'] end def name=(new_name) update_config{ |config| config.merge("name" => new_name) } end def up? @aws_stack.exists? end def up! if @aws_stack.exists? @aws_stack.update(template: self.cloudformation_template) else @services[:cloudformation].stacks.create(self.name, self.cloudformation_template) end end def up_since @aws_stack.creation_time if @aws_stack.exists? end def initialize_distributions! @services[:cloudfront].distributions.each do |dist| dist.update do dist.price_class = :"100" dist.certificate = @aws_stack.server_certificates(domain: dist.aliases).first.id end end end def down! return false unless self.up? @aws_stack.buckets.each{ |bucket| bucket.delete! } @aws_stack.delete true end def cloudformation_template #Kernel.eval(Stacco::Resources::Templates[:cloudformation]) tpl = Stacco::Template.const_get(self.config['template']).new tpl.to_json(stack: self) end def cloudformation_template_body Stacco::Resources::Templates[:cloudformation] end def validate baked_template = self.cloudformation_template test_template = baked_template.gsub(/"[cm][123]\.(\dx)?(small|medium|large)"/, '"m1.small"') begin @services[:cloudformation].estimate_template_cost test_template [true] rescue AWS::CloudFormation::Errors::ValidationError => e msg = e.message match = msg.scan(/^Template format error: JSON not well-formed. \(line (\d+), column (\d+)\)$/) if match.length.nonzero? line, column = match.to_a.flatten.map{ |el| el.to_i } [false, msg, [baked_template.split("\n")[line.to_i], column]] else [false, msg] end end end def iam_private_key @config_object.bucket.objects.with_prefix("sshkey/#{self.name}-").to_a.sort_by{ |obj| obj.key.split('/').last.split('-').last.to_i }.last end def iam_keypair_name "stacco-" + self.iam_private_key.key.split('/').last end def stream_events Enumerator.new do |out| known_events = Set.new ticks_without_add = 0 while self.up? added = 0 @aws_stack.events.sort_by{ |ev| ev.timestamp }.each do |event| next if known_events.include? event.event_id out.yield event known_events.add event.event_id added += 1 ticks_without_add = 0 end ticks_without_add += 1 if added == 0 if ticks_without_add >= 8 and (Math.log2(ticks_without_add) % 1) == 0.0 jobs = @aws_stack.resource_summaries active_jobs = jobs.find_all{ |job| job[:resource_status] =~ /IN_PROGRESS$/ }.map{ |job| job[:logical_resource_id] }.sort unless active_jobs.empty? out.yield OpenStruct.new( logical_resource_id: "Scheduler", status: "WAIT", operation: "WAIT", timestamp: Time.now, error: "waiting on #{active_jobs.join(', ')}" ) end end Kernel.sleep 2 end end end end