modules/mu/providers/aws/collection.rb
# Copyright:: Copyright (c) 2014 eGlobalTech, Inc., all rights reserved
#
# Licensed under the BSD-3 license (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License in the root of the project or at
#
# http://egt-labs.com/mu/LICENSE.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module MU
class Cloud
class AWS
# An Amazon CloudFormation stack as configured in {MU::Config::BasketofKittens::collections}
class Collection < MU::Cloud::Collection
# Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us.
# @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
def initialize(**args)
super
@mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: true)
MU.setVar("curRegion", @region) if !@region.nil?
end
# Called automatically by {MU::Deploy#createResources}
def create
flag="SUCCESS"
MU.setVar("curRegion", @region) if !@region.nil?
region = @region
server=@config["name"]
stack_name = getStackName(@config["name"])
if @config["type"] !=nil && @config["type"]=="existing" then
# XXX this isn't correct, need to go through and list its resources
return @config
end
@config["time"]=@deploy.timestamp
begin
stack_descriptor = {
:stack_name => stack_name,
:on_failure => @config["on_failure"],
:timeout_in_minutes => @config["timeout"],
:tags => [
{
:key => "Name",
:value => MU.appname.upcase + "-" + MU.environment.upcase + "-" + MU.timestamp.upcase + "-" + @config['name'].upcase
},
{
:key => "MU-ID",
:value => MU.deploy_id
}
]
}
keypairname, _ssh_private_key, _ssh_public_key = @deploy.SSHKey
parameters = Array.new
if !@config["parameters"].nil?
@config["parameters"].each { |parameter|
parameters << {
:parameter_key => parameter["parameter_key"],
:parameter_value => parameter["parameter_value"]
}
}
end
if @config["pass_deploy_key_as"] != nil
parameters << {
:parameter_key => @config["pass_deploy_key_as"],
:parameter_value => keypairname
}
end
stack_descriptor[:parameters] = parameters
if @config["template_file"] != nil then
# pass absolute path
if !@config["template_file"].nil?
if @config["template_file"].match(/^\//)
MU.log "Loading Cloudformation template from #{@config["template_file"]}"
template_body = File.read(@config["template_file"])
else
path = File.expand_path(File.dirname(MU::Config.config_path)+"/"+@config["template_file"])
MU.log "Loading Cloudformation template from #{path}"
template_body = File.read(path)
end
else
# json file and template path is same
file_dir =File.dirname(ARGV[0])
if File.exist? file_dir+"/"+@config["template_file"] then
template_body=File.read(file_dir+"/"+@config["template_file"]);
end
end
stack_descriptor[:template_body] = template_body.to_s
end
if @config["template_url"] != nil then
if @config["template_file"] == nil then
stack_descriptor[:template_url] = @config["template_url"]
end
end
MU.log "Creating CloudFormation stack '#{@config['name']}'", details: stack_descriptor
MU::Cloud::AWS.cloudformation(region: region, credentials: @credentials).create_stack(stack_descriptor);
sleep(10);
stack_response = MU::Cloud::AWS.cloudformation(region: region, credentials: @credentials).describe_stacks({:stack_name => stack_name}).stacks.first
attempts = 0
begin
if attempts % 5 == 0
MU.log "Waiting for CloudFormation stack '#{@config['name']}' to be ready...", MU::NOTICE
else
MU.log "Waiting for CloudFormation stack '#{@config['name']}' to be ready...", MU::DEBUG
end
stack_response =MU::Cloud::AWS.cloudformation(region: region, credentials: @credentials).describe_stacks({:stack_name => stack_name}).stacks.first
sleep 60
end while stack_response.stack_status == "CREATE_IN_PROGRESS"
if stack_response.stack_status == "CREATE_FAILED" then
showStackError server
flag="FAIL"
end
rescue Aws::EC2::Errors::ServiceError => e
flag="FAIL"
MU.log "#{stack_name} creation failed (#{e.inspect})", MU::ERR, details: e.backtrace
end
if flag == "FAIL" then
MU::Cloud::AWS.cloudformation(region: region, credentials: @credentials).delete_stack({:stack_name => stack_name})
exit 1
end
MU.log "CloudFormation stack '#{@config['name']}' complete"
begin
resources = MU::Cloud::AWS.cloudformation(region: region, credentials: @credentials).describe_stack_resources(:stack_name => stack_name)
resources[:stack_resources].each { |resource|
case resource.resource_type
when "AWS::EC2::Instance"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
instance_name = MU.deploy_id+"-"+@config['name']+"-"+resource.logical_resource_id
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", instance_name, credentials: @credentials)
instance = MU::Cloud.resourceClass("AWS", "Server").notifyDeploy(
@config['name']+"-"+resource.logical_resource_id,
resource.physical_resource_id
)
MU::Master.addHostToSSHConfig(
instance_name,
instance["private_ip_address"],
instance["private_dns_name"],
# XXX this is a hack-around
user: "ec2-user",
public_dns: instance["public_ip_address"],
public_ip: instance["public_dns_name"],
key_name: instance["key_name"]
)
mu_zone, _junk = MU::Cloud::DNSZone.find(name: "mu")
if !mu_zone.nil?
MU::Cloud.resourceClass("AWS", "DNSZone").genericMuDNSEntry(instance_name, instance["private_ip_address"], MU::Cloud::Server)
else
MU::Master.addInstanceToEtcHosts(instance["public_ip_address"], instance_name)
end
when "AWS::EC2::SecurityGroup"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @credentials)
MU::Cloud.resourceClass("AWS", "FirewallRule").notifyDeploy(
@config['name']+"-"+resource.logical_resource_id,
resource.physical_resource_id
)
when "AWS::EC2::Subnet"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @credentials)
data = {
"collection" => @config["name"],
"subnet_id" => resource.physical_resource_id,
}
@deploy.notify("subnets", @config['name']+"-"+resource.logical_resource_id, data)
when "AWS::EC2::VPC"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @credentials)
data = {
"collection" => @config["name"],
"vpc_id" => resource.physical_resource_id,
}
@deploy.notify("vpcs", @config['name']+"-"+resource.logical_resource_id, data)
when "AWS::EC2::InternetGateway"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @credentials)
when "AWS::EC2::RouteTable"
MU::Cloud::AWS.createStandardTags(resource.physical_resource_id)
MU::Cloud::AWS.createTag(resource.physical_resource_id, "Name", MU.deploy_id+"-"+@config['name']+'-'+resource.logical_resource_id, credentials: @credentials)
# The rest of these aren't anything we act on
when "AWS::EC2::Route"
MU.log resource.resource_type, MU::DEBUG
when "AWS::EC2::EIP"
MU.log resource.resource_type, MU::DEBUG
when "AWS::EC2::SecurityGroupIngress"
MU.log resource.resource_type, MU::DEBUG
when "AWS::EC2::SubnetRouteTableAssociation"
MU.log resource.resource_type, MU::DEBUG
when "AWS::EC2::VPCGatewayAttachment"
MU.log resource.resource_type, MU::DEBUG
when "AWS::IAM::InstanceProfile"
MU.log resource.resource_type, MU::DEBUG
when "AWS::IAM::Role"
MU.log resource.resource_type, MU::DEBUG
else
MU.log "Don't know what to do with #{resource.resource_type}, skipping it", MU::WARN
end
}
rescue Aws::CloudFormation::Errors::ValidationError => e
MU.log "Error processing created resource in CloudFormation stack #{stack_name}: #{e.inspect}", MU::ERR, details: e.backtrace
end
end
# Does this resource type exist as a global (cloud-wide) artifact, or
# is it localized to a region/zone?
# @return [Boolean]
def self.isGlobal?
false
end
# Remove all CloudFormation stacks associated with the currently loaded deployment.
# @param noop [Boolean]: If true, will only print what would be done
# @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
# @param region [String]: The cloud provider region
# @param wait [Boolean]: Block on the removal of this stack; AWS deletion will continue in the background otherwise if false.
# @return [void]
def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, region: MU.curRegion, wait: false, credentials: nil, flags: {})
MU.log "AWS::Collection.cleanup: need to support flags['known']", MU::DEBUG, details: flags
MU.log "Placeholder: AWS Collection artifacts do not support tags, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: ignoremaster
# XXX needs to check tags instead of name- possible?
resp = MU::Cloud::AWS.cloudformation(credentials: credentials, region: region).describe_stacks
resp.stacks.each { |stack|
ok = false
stack.tags.each { |tag|
ok = true if (tag.key == "MU-ID") and tag.value == deploy_id
}
if ok
MU.log "Deleting CloudFormation stack #{stack.stack_name})"
next if noop
if stack.stack_status != "DELETE_IN_PROGRESS"
MU::Cloud::AWS.cloudformation(credentials: credentials, region: region).delete_stack(stack_name: stack.stack_name)
end
if wait
max_retries = 10
retries = 0
mystack = nil
begin
mystack = nil
sleep 30
retries = retries + 1
desc = MU::Cloud::AWS.cloudformation(credentials: credentials, region: region).describe_stacks(stack_name: stack.stack_name)
if desc.size > 0
mystack = desc.first.stacks.first
if mystack.size > 0 and mystack.stack_status == "DELETE_FAILED"
MU.log "Couldn't delete CloudFormation stack #{stack.stack_name}", MU::ERR, details: mystack.stack_status_reason
return
end
MU.log "Waiting for CloudFormation stack #{stack.stack_name} to delete (#{stack.stack_status})...", MU::NOTICE
end
rescue Aws::CloudFormation::Errors::ValidationError
# this is ok, it means deletion finally succeeded
end while !desc.nil? and desc.size > 0 and retries < max_retries
if retries >= max_retries and !mystack.nil? and mystack.stack_status != "DELETED"
MU.log "Failed to delete CloudFormation stack #{stack.stack_name}", MU::ERR
end
end
end
}
return nil
end
# Canonical Amazon Resource Number for this resource
# @return [String]
def arn
cloud_desc.role_arn
end
# placeholder
def self.find(**args)
found = nil
resp = MU::Cloud::AWS.cloudformation(region: args[:region], credentials: args[:credentials]).describe_stacks(
stack_name: args[:cloud_id]
)
if resp and resp.stacks
found[args[:cloud_id]] = resp.stacks.first
end
found
end
# placeholder
# @return [Hash]
def notify
# XXX move those individual resource type notify calls into here
@deploy.notify("collections", @config["name"], @config)
end
# Cloud-specific configuration properties.
# @param _config [MU::Config]: The calling MU::Config object
# @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
def self.schema(_config)
toplevel_required = []
schema = {}
[toplevel_required, schema]
end
# Cloud-specific pre-processing of {MU::Config::BasketofKittens::collections}, bare and unvalidated.
# @param _stack [Hash]: The resource to process and validate
# @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member
# @return [Boolean]: True if validation succeeded, False otherwise
def self.validateConfig(_stack, _configurator)
true
end
private
# Generate a MU-friendly name for a CloudFormation stack
# @param stack [String]: The internal resource name of the stack
# @return [String]
def getStackName(stack)
stack_name = MU.deploy_id + "-" + stack.upcase
stack_name.gsub!(/[_\.]/, "-")
return stack_name
end
# Log the Amazon-specific errors associated with a CloudFormation stack.
# We have to query the AWS API explicitly to get this.
# @param stack [String]: The internal resource name of the stack
# @return [void]
def showStackError(stack)
region = stack['region']
stack_name = getStackName(stack)
begin
resources = MU::Cloud::AWS.cloudformation(region: region).describe_stack_resources(:stack_name => stack_name)
MU.log "CloudFormation stack #{stack_name} failed", MU::ERR
resources[:stack_resources].each { |resource|
MU.log "#{resource.resource_type} #{resource.resource_status} #{resource.resource_status_reason }", MU::ERR
}
rescue Aws::CloudFormation::Errors::ValidationError => e
MU.log e.inspect, MU::ERR, details: e.backtrace
end
end
end #class
end #class
end
end #module