cfer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/cfer.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cfer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cfer"
8
+ spec.version = Cfer::VERSION
9
+ spec.authors = ["Sean Edwards"]
10
+ spec.email = ["stedwards87+git@gmail.com"]
11
+
12
+ spec.summary = %q{Toolkit for automating infrastructure using AWS CloudFormation}
13
+ spec.description = spec.summary
14
+ spec.homepage = "https://github.com/seanedwards/cfer"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'bin'
19
+ spec.executables = 'cfer'
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency 'docile'
23
+ spec.add_runtime_dependency 'thor'
24
+ spec.add_runtime_dependency 'activesupport'
25
+ spec.add_runtime_dependency 'aws-sdk'
26
+ spec.add_runtime_dependency 'aws-sdk-resources'
27
+ spec.add_runtime_dependency 'preconditions'
28
+ spec.add_runtime_dependency 'semantic'
29
+ spec.add_runtime_dependency 'rainbow'
30
+ spec.add_runtime_dependency 'highline'
31
+ spec.add_runtime_dependency 'rugged'
32
+ spec.add_runtime_dependency 'table_print'
33
+ spec.add_runtime_dependency "rake"
34
+
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "yard"
37
+ end
@@ -0,0 +1,84 @@
1
+ description 'Example stack template for a small EC2 instance'
2
+
3
+ # NOTE: This template depends on vpc.rb
4
+
5
+
6
+ # By not specifying a default value, a parameter becomes required.
7
+ # Specify this parameter by adding `--parameters KeyName:<ec2-keyname>` to your CLI options.
8
+ parameter :KeyName
9
+
10
+ # We define some more parameters the same way we did in the VPC template.
11
+ # Cfer will interpret the default value by looking up the stack output named `vpcid`
12
+ # on the stack named `vpc`.
13
+ #
14
+ # If you created the VPC stack with a different name, you can overwrite these default values
15
+ # by adding `VpcId:@<vpc-stack-name>.vpcid SubnetId:@<vpc-stack-name>.subnetid1`
16
+ # to your `--parameters` option
17
+ parameter :VpcId, default: '@vpc.vpcid'
18
+ parameter :SubnetId, default: '@vpc.subnetid1'
19
+
20
+ # This is the Ubuntu 14.04 LTS HVM AMI provided by Amazon.
21
+ parameter :ImageId, default: 'ami-d05e75b8'
22
+ parameter :InstanceType, default: 't2.medium'
23
+
24
+ # Define a security group to be applied to an instance.
25
+ # This one will allow SSH access from anywhere, and no other inbound traffic.
26
+ resource :instancesg, "AWS::EC2::SecurityGroup" do
27
+ group_description 'Wide-open SSH'
28
+ vpc_id Fn::ref(:VpcId)
29
+
30
+ # Parameter values can be Ruby arrays and hashes. These will be transformed to JSON.
31
+ # You could write your own functions to make stuff like this easier, too.
32
+ security_group_ingress [
33
+ {
34
+ CidrIp: '0.0.0.0/0',
35
+ IpProtocol: 'tcp',
36
+ FromPort: 22,
37
+ ToPort: 22
38
+ }
39
+ ]
40
+ end
41
+
42
+ # We can define extension objects, which extend the basic JSON-building
43
+ # functionality of Cfer. Cfer provides a few of these, but you're free
44
+ # to define your own by creating a class that matches the name of an
45
+ # CloudFormation resource type, inheriting from `Cfer::AWS::Resource`
46
+ # inside the `CferExt` module:
47
+ module CferExt::AWS::EC2
48
+ # This class adds methods to resources with the type `AWS::EC2::Instance`
49
+ # Remember, this class could go in your own gem to be shared between your templates
50
+ # in a way that works with the rest of your infrastructure.
51
+ class Instance < Cfer::Cfn::Resource
52
+ def boot_script(data)
53
+ # This function simply wraps a bash script in the little bit of extra
54
+ # sugar (hashbang + base64 encoding) that EC2 requires for userdata boot scripts.
55
+ # See the AWS docs here: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
56
+ script = <<-EOS.strip_heredoc
57
+ #!/bin/bash
58
+ #{data}
59
+ EOS
60
+
61
+ user_data Base64.encode64(script)
62
+ end
63
+ end
64
+ end
65
+
66
+ resource :instance, "AWS::EC2::Instance" do
67
+ # Using the extension defined above, we can have the instance write a simple
68
+ # file to show that it's working. When you converge this template, there
69
+ # should be a `welcome.txt` file sitting in the `ubuntu` user's home directory.
70
+ boot_script "echo 'Welcome to Cfer!' > /home/ubuntu/welcome.txt"
71
+
72
+ image_id Fn::ref(:ImageId)
73
+ instance_type Fn::ref(:InstanceType)
74
+ key_name Fn::ref(:KeyName)
75
+
76
+ network_interfaces [ {
77
+ AssociatePublicIpAddress: "true",
78
+ DeviceIndex: "0",
79
+ GroupSet: [ Fn::ref(:instancesg) ],
80
+ SubnetId: Fn::ref(:SubnetId)
81
+ } ]
82
+ end
83
+
84
+ output :instance, Fn::ref(:instance)
data/examples/vpc.rb ADDED
@@ -0,0 +1,84 @@
1
+ description 'Stack template for a simple example VPC'
2
+
3
+ # This template creates the following resources for a basic beginning AWS VPC setup:
4
+ #
5
+ # 1) A VPC
6
+ # 2) A route table to control network routing
7
+ # 3) An Internet gateway to route traffic to the public internet
8
+ # 4) 3 subnets, one in each of the account's first 3 availability zones
9
+ # 5) A default network route to the IGW
10
+ # 6) Associated plumbing resources to link it all together
11
+
12
+ # Parameters may be defined using the `parameter` function
13
+ parameter :VpcName, default: 'Example VPC'
14
+
15
+ # Resources are created using the `resource` function, accepting the following arguments:
16
+ # 1) The resource name (string or symbol)
17
+ # 2) The resource type. See the AWS CloudFormation docs for the available resource types: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
18
+ resource :vpc, 'AWS::EC2::VPC' do
19
+ # Each line within the resource block sets a single property.
20
+ # These properties are simply camelized using the ActiveSupport gem's `camelize` function.
21
+ # This means that the `cidr_block` function will set the `CidrBlock` property.
22
+ cidr_block '172.42.0.0/16'
23
+
24
+ # Following this pattern, `enable_dns_support` sets the `EnableDnsSupport` property.
25
+ enable_dns_support true
26
+ enable_dns_hostnames true
27
+ instance_tenancy 'default'
28
+
29
+ # The `tag` function is available on all resources, and adds keys to the resource's `Tags` property. It accepts the following arguments:
30
+ # 1) Tag name (symbol or string)
31
+ # 2) Tag value
32
+ tag :DefaultVpc, true
33
+
34
+ # Parameters are required at template generation time, and therefore may be referenced using the `parameters` hash anywhere in a template.
35
+ # This will render the parameter value as a string constant in the CloudFormation JSON output
36
+ tag :Name, parameters[:VpcName]
37
+ end
38
+
39
+ # If there are no properties to set on a resource, the block may be omitted entirely
40
+ resource :defaultigw, 'AWS::EC2::InternetGateway'
41
+
42
+ resource :vpcigw, 'AWS::EC2::VPCGatewayAttachment' do
43
+ # Fn::ref serves the same purpose as CloudFormation's {"Ref": ""} intrinsic function.
44
+ vpc_id Fn::ref(:vpc)
45
+ internet_gateway_id Fn::ref(:defaultigw)
46
+ end
47
+
48
+ resource :routetable, 'AWS::EC2::RouteTable' do
49
+ vpc_id Fn::ref(:vpc)
50
+ end
51
+
52
+ (1..3).each do |i|
53
+ resource "subnet#{i}", 'AWS::EC2::Subnet' do
54
+ # Other CloudFormation intrinsics, such as `Fn::Select` and `AWS::Region` are available as Ruby objects
55
+ # Inspecting these functions will reveal that they simply return a Ruby hash representing the same CloudFormation structures
56
+ availability_zone Fn::select(i, Fn::get_azs(AWS::region))
57
+ cidr_block "172.42.#{i}.0/24"
58
+ vpc_id Fn::ref(:vpc)
59
+ end
60
+
61
+ resource "srta#{i}".to_sym, 'AWS::EC2::SubnetRouteTableAssociation' do
62
+ subnet_id Fn::ref("subnet#{i}")
63
+ route_table_id Fn::ref(:routetable)
64
+ end
65
+
66
+ # Functions do not need to be called in any particular order.
67
+ # The `output` function defines a stack output, which may be referenced from another stack using the `@stack_name.output_name` format
68
+ output "subnetid#{i}", Fn::ref("subnet#{i}")
69
+ end
70
+
71
+ # The `resource` function accepts one additional parameter that was not addressed above: the options hash
72
+ # Additional options passed here will be placed inside the resource, but outside the `Properties` block.
73
+ # In this case, we've specified that the default route explicitly depends on the VPC Internet Gateway.
74
+ # (As of this writing, this is actually a required workaround for this template,
75
+ # because the gateway must be attached to the VPC before a route can be created to it.)
76
+ resource :defaultroute, 'AWS::EC2::Route', DependsOn: [:vpcigw] do
77
+ route_table_id Fn::ref(:routetable)
78
+ gateway_id Fn::ref(:defaultigw)
79
+ destination_cidr_block '0.0.0.0/0'
80
+ end
81
+
82
+ output :vpcid, Fn::ref(:vpc)
83
+
84
+
data/lib/cfer.rb ADDED
@@ -0,0 +1,236 @@
1
+ require 'active_support/all'
2
+ require 'aws-sdk'
3
+ require 'logger'
4
+ require 'json'
5
+ require 'rugged'
6
+ require 'preconditions'
7
+ require 'rainbow'
8
+
9
+
10
+ # Contains extensions that Cfer will dynamically use
11
+ module CferExt
12
+ module AWS
13
+ end
14
+ end
15
+
16
+ # Contains the core Cfer logic
17
+ module Cfer
18
+ module Cfn
19
+ end
20
+
21
+ module Core
22
+ end
23
+
24
+ # The Cfer logger
25
+ LOGGER = Logger.new(STDERR)
26
+ LOGGER.level = Logger::INFO
27
+ LOGGER.formatter = proc { |severity, datetime, progname, msg|
28
+ msg = case severity
29
+ when 'FATAL'
30
+ Rainbow(msg).red.bright
31
+ when 'ERROR'
32
+ Rainbow(msg).red
33
+ when 'WARN'
34
+ Rainbow(msg).yellow
35
+ when 'DEBUG'
36
+ Rainbow(msg).black.bright
37
+ else
38
+ msg
39
+ end
40
+
41
+ "#{msg}\n"
42
+ }
43
+
44
+ class << self
45
+
46
+ def converge!(stack_name, options = {})
47
+ config(options)
48
+ tmpl = options[:template] || "#{stack_name}.rb"
49
+ cfn = options[:aws_options] || {}
50
+
51
+ cfn_stack = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name))
52
+ stack = Cfer::stack_from_file(tmpl, options.merge(client: cfn_stack))
53
+
54
+ begin
55
+ cfn_stack.converge(stack, options)
56
+ if options[:follow]
57
+ tail! stack_name, options
58
+ end
59
+ rescue Aws::CloudFormation::Errors::ValidationError => e
60
+ Cfer::LOGGER.info "CFN validation error: #{e.message}"
61
+ end
62
+ describe! stack_name, options
63
+ end
64
+
65
+ def describe!(stack_name, options = {})
66
+ config(options)
67
+ cfn = options[:aws_options] || {}
68
+ cfn_stack = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name)).fetch_stack
69
+
70
+ Cfer::LOGGER.debug "Describe stack: #{cfn_stack}"
71
+
72
+ case options[:output_format] || 'table'
73
+ when 'json'
74
+ puts render_json(cfn_stack, options)
75
+ when 'table'
76
+ puts "Status: #{cfn_stack[:stack_status]}"
77
+ puts "Description: #{cfn_stack[:description]}" if cfn_stack[:description]
78
+ puts ""
79
+ parameters = (cfn_stack[:parameters] || []).map { |param| {:Type => "Parameter", :Key => param[:parameter_key], :Value => param[:parameter_value]} }
80
+ outputs = (cfn_stack[:outputs] || []).map { |output| {:Type => "Output", :Key => output[:output_key], :Value => output[:output_value]} }
81
+ tp parameters + outputs, :Type, :Key, {:Value => {:width => 80}}
82
+ else
83
+ raise Cfer::Util::CferError, "Invalid output format #{options[:output_format]}."
84
+ end
85
+ end
86
+
87
+ def tail!(stack_name, options = {}, &block)
88
+ config(options)
89
+ cfn = options[:aws_options] || {}
90
+ cfn_client = Cfer::Cfn::Client.new(cfn.merge(stack_name: stack_name))
91
+ if block
92
+ cfn_client.tail(options, &block)
93
+ else
94
+ cfn_client.tail(options) do |event|
95
+ Cfer::LOGGER.info "%s %-30s %-40s %-20s %s" % [event.timestamp, color_map(event.resource_status), event.resource_type, event.logical_resource_id, event.resource_status_reason]
96
+ end
97
+ end
98
+ describe! stack_name, options
99
+ end
100
+
101
+ def generate!(tmpl, options = {})
102
+ config(options)
103
+ cfn_stack = Cfer::Cfn::Client.new(options[:aws_options] || {})
104
+ stack = Cfer::stack_from_file(tmpl, options.merge(client: cfn_stack)).to_h
105
+ puts render_json(stack, options)
106
+ end
107
+
108
+ # Builds a Cfer::Core::Stack from a Ruby block
109
+ #
110
+ # @param options [Hash] The stack options
111
+ # @param block The block containing the Cfn DSL
112
+ # @option options [Hash] :parameters The CloudFormation stack parameters
113
+ # @return [Cfer::Core::Stack] The assembled stack object
114
+ def stack_from_block(options = {}, &block)
115
+ s = Cfer::Core::Stack.new(options)
116
+ templatize_errors('block') do
117
+ s.build_from_block(&block)
118
+ end
119
+ s
120
+ end
121
+
122
+ # Builds a Cfer::Core::Stack from a ruby script
123
+ #
124
+ # @param file [String] The file containing the Cfn DSL
125
+ # @param options [Hash] (see #stack_from_block)
126
+ # @return [Cfer::Core::Stack] The assembled stack object
127
+ def stack_from_file(file, options = {})
128
+ s = Cfer::Core::Stack.new(options)
129
+ templatize_errors(file) do
130
+ s.build_from_file file
131
+ end
132
+ s
133
+ end
134
+
135
+ private
136
+
137
+ def config(options)
138
+ Cfer::LOGGER.debug "Options: #{options}"
139
+ Cfer::LOGGER.level = Logger::DEBUG if options[:verbose]
140
+
141
+ Aws.config.update region: options[:region] if options[:region]
142
+ Aws.config.update credentials: Aws::SharedCredentials.new(profile_name: options[:profile]) if options[:profile]
143
+ end
144
+
145
+ def render_json(obj, options = {})
146
+ if options[:pretty_print]
147
+ puts JSON.pretty_generate(obj)
148
+ else
149
+ puts obj.to_json
150
+ end
151
+ end
152
+
153
+ def templatize_errors(base_loc)
154
+ yield
155
+ rescue SyntaxError => e
156
+ raise Cfer::Util::TemplateError.new([]), e.message
157
+ rescue StandardError => e
158
+ raise Cfer::Util::TemplateError.new(convert_backtrace(base_loc, e)), e.message
159
+ end
160
+
161
+ def convert_backtrace(base_loc, exception)
162
+ continue_search = true
163
+ exception.backtrace_locations.take_while { |loc|
164
+ continue_search = false if loc.path == base_loc
165
+ continue_search || loc.path == base_loc
166
+ }
167
+ end
168
+
169
+
170
+ COLORS_MAP = {
171
+ 'CREATE_IN_PROGRESS' => {
172
+ color: :yellow
173
+ },
174
+ 'DELETE_IN_PROGRESS' => {
175
+ color: :yellow
176
+ },
177
+ 'UPDATE_IN_PROGRESS' => {
178
+ color: :green
179
+ },
180
+
181
+ 'CREATE_FAILED' => {
182
+ color: :red,
183
+ finished: true
184
+ },
185
+ 'DELETE_FAILED' => {
186
+ color: :red,
187
+ finished: true
188
+ },
189
+ 'UPDATE_FAILED' => {
190
+ color: :red,
191
+ finished: true
192
+ },
193
+
194
+ 'CREATE_COMPLETE' => {
195
+ color: :green,
196
+ finished: true
197
+ },
198
+ 'DELETE_COMPLETE' => {
199
+ color: :green,
200
+ finished: true
201
+ },
202
+ 'UPDATE_COMPLETE' => {
203
+ color: :green,
204
+ finished: true
205
+ },
206
+
207
+ 'DELETE_SKIPPED' => {
208
+ color: :yellow
209
+ },
210
+
211
+ 'ROLLBACK_IN_PROGRESS' => {
212
+ color: :red
213
+ },
214
+ 'ROLLBACK_COMPLETE' => {
215
+ color: :red,
216
+ finished: true
217
+ }
218
+ }
219
+
220
+ def color_map(str)
221
+ if COLORS_MAP.include?(str)
222
+ Rainbow(str).send(COLORS_MAP[str][:color])
223
+ else
224
+ str
225
+ end
226
+ end
227
+
228
+ def stopped_state?(str)
229
+ COLORS_MAP[str][:finished] || false
230
+ end
231
+ end
232
+ end
233
+
234
+ Dir["#{File.dirname(__FILE__)}/cfer/**/*.rb"].each { |f| require(f) }
235
+ Dir["#{File.dirname(__FILE__)}/cferext/**/*.rb"].each { |f| require(f) }
236
+
data/lib/cfer/block.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'docile'
2
+
3
+ module Cfer
4
+ # Represents the base class of a Cfer DSL
5
+ class Block < ActiveSupport::HashWithIndifferentAccess
6
+ # Evaluates a DSL directly from a Ruby block, calling pre- and post- hooks.
7
+ # @param args [Array<Object>] Extra arguments to be passed into the block.
8
+ def build_from_block(*args, &block)
9
+ pre_block
10
+ Docile.dsl_eval(self, *args, &block) if block
11
+ post_block
12
+ self
13
+ end
14
+
15
+ # Evaluates a DSL from a Ruby script file
16
+ # @param args [Array<Object>] (see: #build_from_block)
17
+ # @param file [File] The Ruby script to evaluate
18
+ def build_from_file(*args, file)
19
+ build_from_block(*args) do
20
+ instance_eval File.read(file), file
21
+ end
22
+ self
23
+ end
24
+
25
+ # Executed just before the DSL is evaluated
26
+ def pre_block
27
+ end
28
+
29
+ # Executed just after the DSL is evaluated
30
+ def post_block
31
+ end
32
+ end
33
+ end