aws-cfn-dsl 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ require "aws/cfn/decompiler"
2
+
3
+ module Aws
4
+ module Cfn
5
+ module Dsl
6
+ class FnCall
7
+ attr_reader :name, :arguments, :multiline
8
+
9
+ def initialize(name, arguments, multiline = false)
10
+ @name = name
11
+ @arguments = arguments
12
+ @multiline = multiline
13
+ end
14
+
15
+ def to_s()
16
+ @name + "(" + @arguments.join(', ') + ")"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ require 'slop'
2
+ require "aws/cfn/dsl/base"
3
+
4
+ module Aws
5
+ module Cfn
6
+ module Dsl
7
+ class Main < Base
8
+
9
+ def run
10
+
11
+ @opts = Slop.parse(help: true) do
12
+ on :j, :template=, 'The template to convert', as: String
13
+ on :o, :output=, 'The directory to output the DSL to.', as: String
14
+ end
15
+
16
+ unless @opts[:template]
17
+ puts @opts
18
+ exit
19
+ end
20
+
21
+ load @opts[:template]
22
+
23
+ pprint(simplify(@items))
24
+
25
+
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,242 @@
1
+ require 'bundler/setup'
2
+ require 'cloudformation-ruby-dsl/cfntemplate'
3
+ require 'cloudformation-ruby-dsl/table'
4
+ #require 'cloudformation-ruby-dsl/spotprice'
5
+ require 'slop'
6
+
7
+ module Aws
8
+ module Cfn
9
+ module Dsl
10
+ class Template < ::TemplateDSL
11
+
12
+ def initialize(&block)
13
+ @path = File.dirname(caller[2].split(%r'\s+').shift.split(':')[0])
14
+ super
15
+ end
16
+
17
+ def file(b)
18
+ block = File.read File.join(@path,b)
19
+ eval block
20
+ end
21
+
22
+ def mapping(name, options=nil)
23
+ if options.nil?
24
+ file "Mappings/#{name}.rb"
25
+ else
26
+ super(name,options)
27
+ end
28
+ end
29
+
30
+ # def mapping_file(p)
31
+ # file "Mappings/#{p}.rb"
32
+ # end
33
+
34
+ def parameter(name, options=nil)
35
+ if options.nil?
36
+ file "Parameters/#{name}.rb"
37
+ else
38
+ super(name,options)
39
+ end
40
+ end
41
+
42
+ # def parameter_file(p)
43
+ # file "Parameters/#{p}.rb"
44
+ # end
45
+
46
+ def resource(name, options=nil)
47
+ if options.nil?
48
+ file "Resources/#{name}.rb"
49
+ else
50
+ super(name,options)
51
+ end
52
+ end
53
+
54
+ # def resource_file(p)
55
+ # file "Resources/#{p}.rb"
56
+ # end
57
+
58
+ def output(name, options=nil)
59
+ if options.nil?
60
+ file "Outputs/#{name}.rb"
61
+ else
62
+ super(name,options)
63
+ end
64
+ end
65
+
66
+ # def output_file(p)
67
+ # file "Outputs/#{p}.rb"
68
+ # end
69
+
70
+ def exec!(argv=ARGV)
71
+ @opts = Slop.parse(help: true) do
72
+ banner "usage: #{$PROGRAM_NAME} <expand|diff|validate|create|update|delete>"
73
+ on :o, :output=, 'The template file to save this DSL expansion to', as: String
74
+ end
75
+
76
+ action = argv[0] || 'expand'
77
+ unless %w(expand diff validate create update delete).include? action
78
+ $stderr.puts "usage: #{$PROGRAM_NAME} <expand|diff|validate|create|update|delete>"
79
+ exit(2)
80
+ end
81
+ unless (argv & %w(--template-file --template-url)).empty?
82
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: The --template-file and --template-url command-line options are not allowed. (You are running the template itself right now ... !)"
83
+ exit(2)
84
+ end
85
+
86
+ # Find parameters where extension attribute :Immutable is true then remove it from the
87
+ # cfn template since we can't pass it to CloudFormation.
88
+ immutable_parameters = excise_parameter_attribute!(:Immutable)
89
+
90
+ # Tag CloudFormation stacks based on :Tags defined in the template
91
+ cfn_tags = excise_tags!
92
+ # The command line string looks like: --tag "Key=key; Value=value" --tag "Key2=key2; Value2=value"
93
+ cfn_tags_options = cfn_tags.sort.map { |tag| ["--tag", "Key=%s; Value=%s" % tag.split('=')] }.flatten
94
+
95
+ # example: <template.rb> cfn-create-stack my-stack-name --parameters "Env=prod" --region eu-west-1
96
+ # Execute the AWS CLI cfn-cmd command to validate/create/update a CloudFormation stack.
97
+ if action == 'diff' or (action == 'expand' and not nopretty)
98
+ template_string = JSON.pretty_generate(self)
99
+ else
100
+ template_string = JSON.generate(self)
101
+ end
102
+
103
+ if action == 'expand'
104
+ # Write the pretty-printed JSON template to stdout and exit. [--nopretty] option writes output with minimal whitespace
105
+ # example: <template.rb> expand --parameters "Env=prod" --region eu-west-1 --nopretty
106
+ if @opts[:output]
107
+ dest = @opts[:output]
108
+ if File.directory? dest
109
+ file = File.basename $PROGRAM_NAME
110
+ file.gsub!(%r'\.rb', '.json')
111
+ dest = File.join dest, file
112
+ end
113
+ IO.write(dest, template_string)
114
+ else
115
+ puts template_string
116
+ end
117
+ exit(true)
118
+ end
119
+
120
+ temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
121
+ File.write(temp_file, template_string)
122
+
123
+ cmdline = ['cfn-cmd'] + argv + ['--template-file', temp_file] + cfn_tags_options
124
+
125
+ case action
126
+ when 'diff'
127
+ # example: <template.rb> diff my-stack-name --parameters "Env=prod" --region eu-west-1
128
+ # Diff the current template for an existing stack with the expansion of this template.
129
+
130
+ # The --parameters and --tag options were used to expand the template but we don't need them anymore. Discard.
131
+ _, cfn_options = extract_options(argv[1..-1], %w(), %w(--parameters --tag))
132
+
133
+ # Separate the remaining command-line options into options for 'cfn-cmd' and options for 'diff'.
134
+ cfn_options, diff_options = extract_options(cfn_options, %w(),
135
+ %w(--stack-name --region --parameters --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))
136
+
137
+ # If the first argument is a stack name then shift it from diff_options over to cfn_options.
138
+ if diff_options[0] && !(/^-/ =~ diff_options[0])
139
+ cfn_options.unshift(diff_options.shift)
140
+ end
141
+
142
+ # Run CloudFormation commands to describe the existing stack
143
+ cfn_options_string = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
144
+ old_template_raw = exec_capture_stdout("cfn-cmd cfn-get-template #{cfn_options_string}")
145
+ # ec2 template output is not valid json: TEMPLATE "<json>\n"\n
146
+ old_template_object = JSON.parse(old_template_raw[11..-3])
147
+ old_template_string = JSON.pretty_generate(old_template_object)
148
+ old_stack_attributes = exec_describe_stack(cfn_options_string)
149
+ old_tags_string = old_stack_attributes["TAGS"]
150
+ old_parameters_string = old_stack_attributes["PARAMETERS"]
151
+
152
+ # Sort the tag strings alphabetically to make them easily comparable
153
+ old_tags_string = (old_tags_string || '').split(';').sort.map { |tag| %Q(TAG "#{tag}"\n) }.join
154
+ tags_string = cfn_tags.sort.map { |tag| "TAG \"#{tag}\"\n" }.join
155
+
156
+ # Sort the parameter strings alphabetically to make them easily comparable
157
+ old_parameters_string = (old_parameters_string || '').split(';').sort.map { |param| %Q(PARAMETER "#{param}"\n) }.join
158
+ parameters_string = parameters.sort.map { |key, value| "PARAMETER \"#{key}=#{value}\"\n" }.join
159
+
160
+ # Diff the expanded template with the template from CloudFormation.
161
+ old_temp_file = File.absolute_path("#{$PROGRAM_NAME}.current.json")
162
+ new_temp_file = File.absolute_path("#{$PROGRAM_NAME}.expanded.json")
163
+ File.write(old_temp_file, old_tags_string + old_parameters_string + old_template_string)
164
+ File.write(new_temp_file, tags_string + parameters_string + template_string)
165
+
166
+ # Compare templates
167
+ system(*["diff"] + diff_options + [old_temp_file, new_temp_file])
168
+
169
+ File.delete(old_temp_file)
170
+ File.delete(new_temp_file)
171
+
172
+ exit(true)
173
+
174
+ when 'cfn-validate-template'
175
+ # The cfn-validate-template command doesn't support --parameters so remove it if it was provided for template expansion.
176
+ _, cmdline = extract_options(cmdline, %w(), %w(--parameters --tag))
177
+
178
+ when 'cfn-update-stack'
179
+ # Pick out the subset of cfn-update-stack options that apply to cfn-describe-stacks.
180
+ cfn_options, other_options = extract_options(argv[1..-1], %w(),
181
+ %w(--stack-name --region --connection-timeout -I --access-key-id -S --secret-key -K --ec2-private-key-file-path -U --url))
182
+
183
+ # If the first argument is a stack name then shift it over to cfn_options.
184
+ if other_options[0] && !(/^-/ =~ other_options[0])
185
+ cfn_options.unshift(other_options.shift)
186
+ end
187
+
188
+ # Run CloudFormation command to describe the existing stack
189
+ cfn_options_string = cfn_options.map { |arg| "'#{arg}'" }.join(' ')
190
+ old_stack_attributes = exec_describe_stack(cfn_options_string)
191
+
192
+ # If updating a stack and some parameters are marked as immutable, fail if the new parameters don't match the old ones.
193
+ if not immutable_parameters.empty?
194
+ old_parameters_string = old_stack_attributes["PARAMETERS"]
195
+ old_parameters = Hash[(old_parameters_string || '').split(';').map { |pair| pair.split('=', 2) }]
196
+ new_parameters = parameters
197
+
198
+ immutable_parameters.sort.each do |param|
199
+ if old_parameters[param].to_s != new_parameters[param].to_s
200
+ $stderr.puts "Error: cfn-update-stack may not update immutable parameter " +
201
+ "'#{param}=#{old_parameters[param]}' to '#{param}=#{new_parameters[param]}'."
202
+ exit(false)
203
+ end
204
+ end
205
+ end
206
+
207
+ # Tags are immutable in CloudFormation. The cfn-update-stack command doesn't support --tag options, so remove
208
+ # the argument (if it exists) and validate against the existing stack to ensure tags haven't changed.
209
+ # Compare the sorted arrays for an exact match
210
+ old_cfn_tags = old_stack_attributes['TAGS'].split(';').sort rescue [] # Use empty Array if .split fails
211
+ if cfn_tags != old_cfn_tags
212
+ $stderr.puts "CloudFormation stack tags do not match and cannot be updated. You must either use the same tags or create a new stack." +
213
+ "\n" + (old_cfn_tags - cfn_tags).map {|tag| "< #{tag}" }.join("\n") +
214
+ "\n" + "---" +
215
+ "\n" + (cfn_tags - old_cfn_tags).map {|tag| "> #{tag}"}.join("\n")
216
+ exit(false)
217
+ end
218
+ _, cmdline = extract_options(cmdline, %w(), %w(--tag))
219
+ end
220
+
221
+ # Execute command cmdline
222
+ unless system(*cmdline)
223
+ $stderr.puts "\nExecution of 'cfn-cmd' failed. To facilitate debugging, the generated JSON template " +
224
+ "file was not deleted. You may delete the file manually if it isn't needed: #{temp_file}"
225
+ exit(false)
226
+ end
227
+
228
+ File.delete(temp_file)
229
+
230
+ exit(true)
231
+ end
232
+
233
+
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ # Main entry point
240
+ def template(&block)
241
+ Aws::Cfn::Dsl::Template.new(&block)
242
+ end
@@ -1,7 +1,7 @@
1
1
  module Aws
2
2
  module Cfn
3
3
  module Dsl
4
- VERSION = "0.0.4"
4
+ VERSION = "0.1.0"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,10 @@
1
+ mapping 'AWSRegion2AMI',
2
+ :'us-east-1' => { :id => 'ami-83dee0ea' },
3
+ :'us-west-1' => { :id => 'ami-c45f6281' },
4
+ :'us-west-2' => { :id => 'ami-d0d8b8e0' },
5
+ :'eu-west-1' => { :id => 'ami-aa56a1dd' },
6
+ :'sa-east-1' => { :id => 'ami-d55bfbc8' },
7
+ :'ap-southeast-1' => { :id => 'ami-bc7325ee' },
8
+ :'ap-southeast-2' => { :id => 'ami-e577e9df' },
9
+ :'ap-northeast-1' => { :id => 'ami-f72e45f6' }
10
+
@@ -0,0 +1,4 @@
1
+ output 'ChefSecurityGroup',
2
+ :Description => 'EC2 Security Group with access to Opscode chef server',
3
+ :Value => { :Ref => 'ChefClientSecurityGroup' }
4
+
@@ -0,0 +1,13 @@
1
+ output 'ServerURL',
2
+ :Description => 'URL of newly created Opscode chef server',
3
+ :Value => {
4
+ :'Fn::Join' => [
5
+ '',
6
+ [
7
+ 'https://',
8
+ { :'Fn::GetAtt' => [ 'ChefServer', 'PublicDnsName' ] },
9
+ ':443',
10
+ ],
11
+ ],
12
+ }
13
+
@@ -0,0 +1,4 @@
1
+ output 'ValidationKeyBucket',
2
+ :Description => 'Location of validation key',
3
+ :Value => { :Ref => 'PrivateKeyBucket' }
4
+
@@ -0,0 +1,5 @@
1
+ parameter 'CookbookLocation',
2
+ :Type => 'String',
3
+ :Default => 'https://github.com/opscode-cookbooks/aws/tarball/master',
4
+ :Description => 'Location of chef cookbooks to upload to server'
5
+
@@ -0,0 +1,7 @@
1
+ parameter 'InstanceType',
2
+ :Description => 'WebServer EC2 instance type',
3
+ :Type => 'String',
4
+ :Default => 'm1.small',
5
+ :AllowedValues => %w(t1.micro m1.small m1.medium m1.large m1.xlarge m2.xlarge m2.2xlarge m2.4xlarge m3.xlarge m3.2xlarge c1.medium c1.xlarge cc1.4xlarge cc2.8xlarge cg1.4xlarge),
6
+ :ConstraintDescription => 'must be a valid EC2 instance type.'
7
+
@@ -0,0 +1,8 @@
1
+ parameter 'KeyName',
2
+ :Description => 'Name of an existing EC2 KeyPair to enable SSH access to the web server',
3
+ :Type => 'String',
4
+ :MinLength => '1',
5
+ :MaxLength => '255',
6
+ :AllowedPattern => '[\\x20-\\x7E]*',
7
+ :ConstraintDescription => 'can contain only ASCII characters.'
8
+
@@ -0,0 +1,5 @@
1
+ parameter 'RoleLocation',
2
+ :Type => 'String',
3
+ :Default => 'https://s3.amazonaws.com/cloudformation-examples/example_chef_roles.tar.gz',
4
+ :Description => 'Location of client roles to upload to server'
5
+
@@ -0,0 +1,9 @@
1
+ parameter 'SSHLocation',
2
+ :Description => 'The IP address range that can be used to SSH to the EC2 instances',
3
+ :Type => 'String',
4
+ :MinLength => '9',
5
+ :MaxLength => '18',
6
+ :Default => '0.0.0.0/0',
7
+ :AllowedPattern => '(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})',
8
+ :ConstraintDescription => 'must be a valid IP CIDR range of the form x.x.x.x/x.'
9
+
@@ -0,0 +1,28 @@
1
+ resource 'BucketPolicy', :Type => 'AWS::S3::BucketPolicy', :Properties => {
2
+ :PolicyDocument => {
3
+ :Version => '2008-10-17',
4
+ :Id => 'WritePolicy',
5
+ :Statement => [
6
+ {
7
+ :Sid => 'WriteAccess',
8
+ :Action => [ 's3:PutObject' ],
9
+ :Effect => 'Allow',
10
+ :Resource => {
11
+ :'Fn::Join' => [
12
+ '',
13
+ [
14
+ 'arn:aws:s3:::',
15
+ { :Ref => 'PrivateKeyBucket' },
16
+ '/*',
17
+ ],
18
+ ],
19
+ },
20
+ :Principal => {
21
+ :AWS => { :'Fn::GetAtt' => [ 'ChefServerUser', 'Arn' ] },
22
+ },
23
+ },
24
+ ],
25
+ },
26
+ :Bucket => { :Ref => 'PrivateKeyBucket' },
27
+ }
28
+
@@ -0,0 +1,2 @@
1
+ resource 'ChefClientSecurityGroup', :Type => 'AWS::EC2::SecurityGroup', :Properties => { :GroupDescription => 'Group with access to Chef Server' }
2
+
@@ -0,0 +1,57 @@
1
+ resource 'ChefServer', :Type => 'AWS::EC2::Instance', :Metadata => { :'AWS::CloudFormation::Init' => { :config => { :packages => { :apt => { :s3cmd => [] } }, :sources => { :'/home/ubuntu/chef-repo' => 'https://github.com/opscode/chef-repo/tarball/master', :'/home/ubuntu/chef-repo/cookbooks' => { :Ref => 'CookbookLocation' }, :'/home/ubuntu/chef-repo/roles' => { :Ref => 'RoleLocation' } }, :files => { :'/home/ubuntu/setup_environment' => { :source => 'https://s3.amazonaws.com/cloudformation-examples/setup-chef-server-with-knife', :mode => '000755', :owner => 'ubuntu', :group => 'ubuntu' }, :'/home/ubuntu/.s3cfg' => { :content => { :'Fn::Join' => [ '', [ "[default]\n", 'access_key = ', { :Ref => 'HostKeys' }, "\n", 'secret_key = ', { :'Fn::GetAtt' => [ 'HostKeys', 'SecretAccessKey' ] }, "\n", "use_https = True\n" ] ] }, :mode => '000644', :owner => 'ubuntu', :group => 'ubuntu' }, :'/home/ubuntu/chef_11.10.0-1.ubuntu.12.04_amd64.deb' => { :source => 'https://s3.amazonaws.com/cloudformation-examples/chef_11.10.0-1.ubuntu.12.04_amd64.deb', :mode => '000664', :owner => 'ubuntu', :group => 'ubuntu' }, :'/home/ubuntu/chef-server_11.0.10-1.ubuntu.12.04_amd64.deb' => { :source => 'https://s3.amazonaws.com/cloudformation-examples/chef-server_11.0.10-1.ubuntu.12.04_amd64.deb', :mode => '000664', :owner => 'ubuntu', :group => 'ubuntu' } } } } }, :Properties => {
2
+ :SecurityGroups => [
3
+ { :Ref => 'ChefServerSecurityGroup' },
4
+ ],
5
+ :ImageId => {
6
+ :'Fn::FindInMap' => [
7
+ 'AWSRegion2AMI',
8
+ { :Ref => 'AWS::Region' },
9
+ 'id',
10
+ ],
11
+ },
12
+ :UserData => {
13
+ :'Fn::Base64' => {
14
+ :'Fn::Join' => [
15
+ '',
16
+ [
17
+ "#!/bin/bash\n",
18
+ "function error_exit\n",
19
+ "{\n",
20
+ ' cfn-signal -e 1 -r "$1" \'',
21
+ { :Ref => 'ChefServerWaitHandle' },
22
+ "'\n",
23
+ " exit 1\n",
24
+ "}\n",
25
+ "apt-get -y install python-setuptools\n",
26
+ "easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
27
+ 'cfn-init --region ',
28
+ { :Ref => 'AWS::Region' },
29
+ ' -s ',
30
+ { :Ref => 'AWS::StackId' },
31
+ ' -r ChefServer ',
32
+ "|| error_exit 'Failed to run cfn-init'\n",
33
+ "# Bootstrap chef\n",
34
+ "dpkg -i /home/ubuntu/chef_11.10.0-1.ubuntu.12.04_amd64.deb > /tmp/chef_install.log 2>&1 || error_exit 'Failed to install chef client tools'\n",
35
+ "dpkg -i /home/ubuntu/chef-server_11.0.10-1.ubuntu.12.04_amd64.deb >> /tmp/chef_install.log 2>&1 || error_exit 'Failed to install chef server'\n",
36
+ "sleep 5\n",
37
+ "sudo /usr/bin/chef-server-ctl reconfigure > /tmp/chef_configure.log 2>&1 || error_exit 'Failed to configure chef server'\n",
38
+ "sleep 5\n",
39
+ "sudo chef-server-ctl start\n",
40
+ "# Setup development environment in ubuntu user\n",
41
+ "sudo -u ubuntu /home/ubuntu/setup_environment > /tmp/setup_environment.log 2>&1 || error_exit 'Failed to bootstrap chef server'\n",
42
+ "# copy validation key to S3 bucket\n",
43
+ 's3cmd -c /home/ubuntu/.s3cfg put /etc/chef-server/validation.pem s3://',
44
+ { :Ref => 'PrivateKeyBucket' },
45
+ "/validation.pem > /tmp/put_validation_key.log 2>&1 || error_exit 'Failed to put Chef Server validation key'\n",
46
+ "# If all went well, signal success\n",
47
+ 'cfn-signal -e $? -r \'Chef Server configuration\' \'',
48
+ { :Ref => 'ChefServerWaitHandle' },
49
+ "'\n",
50
+ ],
51
+ ],
52
+ },
53
+ },
54
+ :KeyName => { :Ref => 'KeyName' },
55
+ :InstanceType => { :Ref => 'InstanceType' },
56
+ }
57
+