aws-cfn-dsl 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+