enscalator 0.4.0.pre.alpha.pre.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +9 -0
  4. data/.rubocop_todo.yml +59 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +148 -0
  10. data/Rakefile +43 -0
  11. data/bin/console +11 -0
  12. data/bin/setup +7 -0
  13. data/enscalator.gemspec +57 -0
  14. data/exe/enscalator +13 -0
  15. data/lib/enscalator/core/cf_parameters.rb +146 -0
  16. data/lib/enscalator/core/cf_resources.rb +225 -0
  17. data/lib/enscalator/core/instance_type.rb +205 -0
  18. data/lib/enscalator/core/network_config.rb +21 -0
  19. data/lib/enscalator/core.rb +10 -0
  20. data/lib/enscalator/enapp.rb +248 -0
  21. data/lib/enscalator/helpers/dns.rb +62 -0
  22. data/lib/enscalator/helpers/stack.rb +107 -0
  23. data/lib/enscalator/helpers/sub_process.rb +72 -0
  24. data/lib/enscalator/helpers/wrappers.rb +55 -0
  25. data/lib/enscalator/helpers.rb +127 -0
  26. data/lib/enscalator/plugins/amazon_linux.rb +93 -0
  27. data/lib/enscalator/plugins/auto_scale.rb +80 -0
  28. data/lib/enscalator/plugins/core_os.rb +88 -0
  29. data/lib/enscalator/plugins/couchbase.rb +98 -0
  30. data/lib/enscalator/plugins/debian.rb +71 -0
  31. data/lib/enscalator/plugins/elastic_beanstalk.rb +74 -0
  32. data/lib/enscalator/plugins/elasticache.rb +168 -0
  33. data/lib/enscalator/plugins/elasticsearch_amazon.rb +75 -0
  34. data/lib/enscalator/plugins/elasticsearch_bitnami.rb +198 -0
  35. data/lib/enscalator/plugins/elasticsearch_opsworks.rb +225 -0
  36. data/lib/enscalator/plugins/elb.rb +139 -0
  37. data/lib/enscalator/plugins/nat_gateway.rb +71 -0
  38. data/lib/enscalator/plugins/rds.rb +141 -0
  39. data/lib/enscalator/plugins/redis.rb +38 -0
  40. data/lib/enscalator/plugins/rethink_db.rb +21 -0
  41. data/lib/enscalator/plugins/route53.rb +143 -0
  42. data/lib/enscalator/plugins/ubuntu.rb +85 -0
  43. data/lib/enscalator/plugins/user-data/elasticsearch +367 -0
  44. data/lib/enscalator/plugins/vpc_peering_connection.rb +48 -0
  45. data/lib/enscalator/plugins.rb +30 -0
  46. data/lib/enscalator/rich_template_dsl.rb +209 -0
  47. data/lib/enscalator/templates/vpc_peering.rb +112 -0
  48. data/lib/enscalator/templates.rb +20 -0
  49. data/lib/enscalator/version.rb +5 -0
  50. data/lib/enscalator/vpc.rb +11 -0
  51. data/lib/enscalator/vpc_with_nat_gateway.rb +311 -0
  52. data/lib/enscalator/vpc_with_nat_instance.rb +402 -0
  53. data/lib/enscalator.rb +103 -0
  54. metadata +427 -0
@@ -0,0 +1,248 @@
1
+ require 'cloudformation-ruby-dsl/cfntemplate'
2
+ require_relative 'rich_template_dsl'
3
+
4
+ module Enscalator
5
+ # Template DSL for common enJapan application stack
6
+ class EnAppTemplateDSL < RichTemplateDSL
7
+ include Enscalator::Helpers
8
+
9
+ attr_reader :app_name
10
+
11
+ Struct.new('Subnet', :availability_zone, :suffix, :cidr_block)
12
+
13
+ # Subnet size (256 addresses)
14
+ SUBNET_CIDR_BLOCK_SIZE = 24
15
+
16
+ # Create new EnAppTemplateDSL instance
17
+ #
18
+ # @param [Hash] options command-line arguments
19
+ def initialize(options = {})
20
+ # application name taken from template name by default
21
+ @app_name = self.class.name.demodulize
22
+ super
23
+ end
24
+
25
+ # Get vpc stack
26
+ #
27
+ # @return [Aws::CloudFormation::Stack] stack instance of vpc stack
28
+ def vpc_stack
29
+ @vpc_stack ||= cfn_resource(cfn_client(region)).stack(vpc_stack_name)
30
+ end
31
+
32
+ # Get current stack
33
+ #
34
+ # @return [Aws::CloudFormation::Stack] current stack
35
+ def current_stack
36
+ @current_stack ||= (cfn_resource(cfn_client(region)).stack(stack_name) rescue nil) unless creating?
37
+ end
38
+
39
+ # Get vpc
40
+ #
41
+ # @return [Aws::EC2::Vpc] vpc instance
42
+ def vpc
43
+ @vpc ||= Aws::EC2::Vpc.new(id: get_resource(vpc_stack, 'VPC'), region: region)
44
+ end
45
+
46
+ # References to application subnets in all availability zones
47
+ def ref_application_subnets
48
+ availability_zones.map { |suffix, _| ref("ApplicationSubnet#{suffix.upcase}") }
49
+ end
50
+
51
+ # References to resource subnets in all availability zones
52
+ def ref_resource_subnets
53
+ availability_zones.map { |suffix, _| ref("ResourceSubnet#{suffix.upcase}") }
54
+ end
55
+
56
+ # Public subnets in all availability zones
57
+ def public_subnets
58
+ availability_zones.map { |suffix, _| get_resource(vpc_stack, "PublicSubnet#{suffix.upcase}") }
59
+ end
60
+
61
+ # Get VPC ID as reference to parameter
62
+ # @return [Hash]
63
+ def ref_vpc_id
64
+ ref('VpcId')
65
+ end
66
+
67
+ # Reference to private security group
68
+ # @return [Hash]
69
+ def ref_private_security_group
70
+ ref('PrivateSecurityGroup')
71
+ end
72
+
73
+ # Reference to resource security group
74
+ # @return [Hash]
75
+ def ref_resource_security_group
76
+ ref('ResourceSecurityGroup')
77
+ end
78
+
79
+ # Reference to application security group
80
+ # @return [Hash]
81
+ def ref_application_security_group
82
+ ref('ApplicationSecurityGroup')
83
+ end
84
+
85
+ # Get all CIRD blocks for current VPC
86
+ # @return [Hash]
87
+ def get_all_cidr_blocks
88
+ IPAddress(
89
+ Core::NetworkConfig.mapping_vpc_net[region.to_sym][:VPC]).subnet(SUBNET_CIDR_BLOCK_SIZE).map(&:to_string)
90
+ end
91
+
92
+ # Get currently used CIDR blocks
93
+ # @return [Array]
94
+ def get_used_cidr_blocks
95
+ vpc.subnets.collect(&:cidr_block)
96
+ end
97
+
98
+ # Get non-used CIDR blocks
99
+ # @return [Array]
100
+ def get_available_cidr_blocks
101
+ get_all_cidr_blocks - get_used_cidr_blocks
102
+ end
103
+
104
+ # Get application CIDR blocks availability zones mapping
105
+ # @return [Hash]
106
+ def get_application_to_az_mapping
107
+ cidr_blocks = get_available_cidr_blocks.dup
108
+ availability_zones.map do |suffix, az|
109
+ cidr_block = (begin
110
+ subnet_id = get_resource(current_stack, "ApplicationSubnet#{suffix.upcase}")
111
+ Aws::EC2::Subnet.new(id: subnet_id, region: region).cidr_block
112
+ end rescue nil) if current_stack
113
+
114
+ Struct::Subnet.new(az, suffix, cidr_block || cidr_blocks.shift)
115
+ end
116
+ end
117
+
118
+ # CIDR blocks allocated for application subnets
119
+ # @return [Array]
120
+ def get_application_cidr_blocks
121
+ get_application_to_az_mapping.map(&:cidr_block)
122
+ end
123
+
124
+ # Get resource CIDR blocks availability zones mapping
125
+ # @return [Array]
126
+ def get_resource_to_az_mapping
127
+ cidr_blocks = (get_available_cidr_blocks - get_application_cidr_blocks).dup
128
+ availability_zones.map do |suffix, az|
129
+ cidr_block = (begin
130
+ subnet_id = get_resource(current_stack, "ResourceSubnet#{suffix.upcase}")
131
+ Aws::EC2::Subnet.new(id: subnet_id, region: region).cidr_block
132
+ end rescue nil) if current_stack
133
+
134
+ Struct::Subnet.new(az, suffix, cidr_block || cidr_blocks.shift)
135
+ end
136
+ end
137
+
138
+ # CIDR blocks allocated for resource subnets
139
+ # @return [Array]
140
+ def get_resource_cidr_blocks
141
+ get_resource_to_az_mapping.map(&:cidr_block)
142
+ end
143
+
144
+ # Query and pre-configure VPC parameters required for the stack
145
+ def load_vpc_params
146
+ parameter 'VpcId',
147
+ Description: 'The Id of the VPC',
148
+ Default: vpc.id,
149
+ Type: 'String',
150
+ AllowedPattern: 'vpc-[a-zA-Z0-9]*',
151
+ ConstraintDescription: 'must begin with vpc- followed by numbers and alphanumeric characters.'
152
+
153
+ parameter 'PrivateSecurityGroup',
154
+ Description: 'Security group identifier of private instances',
155
+ Default: get_resource(vpc_stack, 'PrivateSecurityGroup'),
156
+ Type: 'String',
157
+ AllowedPattern: 'sg-[a-zA-Z0-9]*',
158
+ ConstraintDescription: 'must begin with sg- followed by numbers and alphanumeric characters.'
159
+
160
+ # allocate application/resource cidr blocks dynamically for all availability zones
161
+ availability_zones.zip(get_application_cidr_blocks,
162
+ get_resource_cidr_blocks).each do |pair, application_cidr_block, resource_cidr_block|
163
+ suffix, availability_zone = pair
164
+
165
+ private_route_table_name = "PrivateRouteTable#{suffix.upcase}"
166
+ parameter private_route_table_name,
167
+ Description: "Route table identifier for private instances of zone #{suffix}",
168
+ Default: get_resource(vpc_stack, private_route_table_name),
169
+ Type: 'String',
170
+ AllowedPattern: 'rtb-[a-zA-Z0-9]*',
171
+ ConstraintDescription: 'must begin with rtb- followed by numbers and alphanumeric characters.'
172
+
173
+ application_subnet_name = "ApplicationSubnet#{suffix.upcase}"
174
+ subnet application_subnet_name,
175
+ vpc.id,
176
+ application_cidr_block,
177
+ availability_zone: availability_zone,
178
+ tags: {
179
+ Network: 'Private',
180
+ Application: aws_stack_name,
181
+ immutable_metadata: join('', '{ "purpose": "', aws_stack_name, '-app" }')
182
+ }
183
+
184
+ resource_subnet_name = "ResourceSubnet#{suffix.upcase}"
185
+ subnet resource_subnet_name,
186
+ vpc.id,
187
+ resource_cidr_block,
188
+ availability_zone: availability_zone,
189
+ tags: {
190
+ Network: 'Private',
191
+ Application: aws_stack_name
192
+ }
193
+
194
+ resource "ApplicationRouteTableAssociation#{suffix.upcase}",
195
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
196
+ Properties: {
197
+ RouteTableId: ref(private_route_table_name),
198
+ SubnetId: ref(application_subnet_name)
199
+ }
200
+
201
+ resource "ResourceRouteTableAssociation#{suffix.upcase}",
202
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
203
+ Properties: {
204
+ RouteTableId: ref(private_route_table_name),
205
+ SubnetId: ref(resource_subnet_name)
206
+ }
207
+ end
208
+
209
+ security_group_vpc 'ResourceSecurityGroup',
210
+ 'Enable internal access with ssh',
211
+ vpc.id,
212
+ security_group_ingress: [
213
+ {
214
+ IpProtocol: 'tcp',
215
+ FromPort: '22',
216
+ ToPort: '22',
217
+ CidrIp: '10.0.0.0/8'
218
+ },
219
+ {
220
+ IpProtocol: 'tcp',
221
+ FromPort: '0',
222
+ ToPort: '65535',
223
+ SourceSecurityGroupId: ref_application_security_group
224
+ }
225
+ ],
226
+ tags: {
227
+ Name: join('-', aws_stack_name, 'res', 'sg'),
228
+ Application: aws_stack_name
229
+ }
230
+
231
+ security_group_vpc 'ApplicationSecurityGroup',
232
+ 'Security group of the application servers',
233
+ vpc.id,
234
+ security_group_ingress: [
235
+ {
236
+ IpProtocol: 'tcp',
237
+ FromPort: '0',
238
+ ToPort: '65535',
239
+ CidrIp: '10.0.0.0/8'
240
+ }
241
+ ],
242
+ tags: {
243
+ Name: join('-', aws_stack_name, 'app', 'sg'),
244
+ Application: aws_stack_name
245
+ }
246
+ end
247
+ end # class EnAppTemplateDSL
248
+ end # module Enscalator
@@ -0,0 +1,62 @@
1
+ module Enscalator
2
+ module Helpers
3
+ module Dns
4
+ # Get existing DNS records
5
+ #
6
+ # @param [String] zone_name name of the hosted zone
7
+ def get_dns_records(zone_name: nil)
8
+ client = route53_client(nil)
9
+ zone = client.list_hosted_zones[:hosted_zones].find { |x| x.name == zone_name }
10
+ records = client.list_resource_record_sets(hosted_zone_id: zone.id)
11
+ records.values.flatten.map do |x|
12
+ {
13
+ name: x.name,
14
+ type: x.type,
15
+ records: x.resource_records.map(&:value)
16
+ } if x.is_a?(Aws::Structure)
17
+ end.compact
18
+ end
19
+
20
+ # Create DNS record in given hosted zone
21
+ #
22
+ # @param [String] region aws valid region identifier
23
+ # @param [String] zone_name name of the hosted zone
24
+ # @param [String] record_name name of the dns record
25
+ # @param [String] type record type (NS, MX, CNAME and etc.)
26
+ # @param [Array] values list of record values
27
+ # @param [Integer] ttl time to live
28
+ # @param [String] suffix additional identifier following region
29
+ def upsert_dns_record(region: nil,
30
+ zone_name: nil,
31
+ record_name: nil,
32
+ type: 'A',
33
+ values: [],
34
+ ttl: 300,
35
+ suffix: '')
36
+ client = route53_client(region: region)
37
+ zone = client.list_hosted_zones[:hosted_zones].find { |x| x.name == zone_name }
38
+ record_tokens = [record_name.gsub(zone_name, ''), region]
39
+ record_tokens << suffix if suffix && !suffix.empty?
40
+ record_name = [record_tokens.join, zone_name].join('.')
41
+
42
+ client.change_resource_record_sets(
43
+ hosted_zone_id: zone.id,
44
+ change_batch: {
45
+ comment: "dns record for #{record_name}",
46
+ changes: [
47
+ {
48
+ action: 'UPSERT',
49
+ resource_record_set: {
50
+ name: record_name,
51
+ type: type,
52
+ resource_records: values.map { |x| { value: x } },
53
+ ttl: ttl
54
+ }
55
+ }
56
+ ]
57
+ }
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,107 @@
1
+ require 'ruby-progressbar'
2
+
3
+ module Enscalator
4
+ module Helpers
5
+ # Helpers for operations requiring stack instance or stack_name
6
+ module Stack
7
+ # Wait until stack gets created
8
+ #
9
+ # @param [Aws::CloudFormation::Resource] cfn accessor for cloudformation resource
10
+ # @param [String] stack_name name of the stack
11
+ # @return [Aws::CloudFormation::Stack]
12
+ def wait_stack(cfn, stack_name)
13
+ stack = cfn.stack(stack_name)
14
+ title = 'Waiting for stack to be created'
15
+ progress = ProgressBar.create(title: title, starting_at: 10, total: nil)
16
+ loop do
17
+ break unless stack.stack_status =~ /(CREATE|UPDATE)_IN_PROGRESS$/
18
+ progress.title = title + " [#{stack.stack_status}]"
19
+ progress.increment
20
+ sleep 5
21
+ stack = cfn.stack(stack_name)
22
+ end
23
+ stack
24
+ end
25
+
26
+ # Create stack using cloudformation interface
27
+ #
28
+ # @param [String] region AWS region identifier
29
+ # @param [String] dependent_stack_name name of the stack current stack depends on
30
+ # @param [String] template name
31
+ # @param [String] stack_name stack name
32
+ # @param [Array] keys keys
33
+ # @param [Array] extra_parameters additional parameters
34
+ # @return [Aws::CloudFormation::Resource]
35
+ # @deprecated this method is no longer used
36
+ def cfn_create_stack(region, dependent_stack_name, template, stack_name, keys: [], extra_parameters: [])
37
+ cfn = cfn_resource(cfn_client(region))
38
+ stack = wait_stack(cfn, dependent_stack_name)
39
+ extra_parameters_cleaned = extra_parameters.map do |x|
40
+ if x.key? 'ParameterKey'
41
+ {
42
+ parameter_key: x['ParameterKey'],
43
+ parameter_value: x['ParameterValue']
44
+ }
45
+ else
46
+ x
47
+ end
48
+ end
49
+ options = {
50
+ stack_name: stack_name,
51
+ template_body: template,
52
+ parameters: generate_parameters(stack, keys) + extra_parameters_cleaned
53
+ }
54
+ cfn.create_stack(options)
55
+ end
56
+
57
+ # Get resource for given key from given stack
58
+ #
59
+ # @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
60
+ # @param [String] key resource identifier (key)
61
+ # @return [String] AWS resource identifier
62
+ # @raise [ArgumentError] when stack is nil
63
+ # @raise [ArgumentError] when key is nil or empty
64
+ def get_resource(stack, key)
65
+ fail ArgumentError, 'stack must not be nil' if stack.nil?
66
+ fail ArgumentError, 'key must not be nil nor empty' if key.nil? || key.empty?
67
+ # query with physical_resource_id
68
+ resource = begin
69
+ stack.resource(key).physical_resource_id
70
+ rescue RuntimeError
71
+ nil
72
+ end
73
+ if resource.nil?
74
+ # fallback to values from stack.outputs
75
+ output = stack.outputs.select { |s| s.output_key == key }
76
+ resource = begin
77
+ output.first.output_value
78
+ rescue RuntimeError
79
+ nil
80
+ end
81
+ end
82
+ resource
83
+ end
84
+
85
+ # Get list of resources for given keys
86
+ #
87
+ # @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
88
+ # @param [Array] keys list of resource identifiers (keys)
89
+ # @return [String] list of AWS resource identifiers
90
+ # @raise [ArgumentError] when stack is nil
91
+ # @raise [ArgumentError] when keys are nil or empty list
92
+ def get_resources(stack, keys)
93
+ fail ArgumentError, 'stack must not be nil' if stack.nil?
94
+ fail ArgumentError, 'key must not be nil nor empty' if keys.nil? || keys.empty?
95
+ keys.map { |k| get_resource(stack, k) }.compact
96
+ end
97
+
98
+ # Generate parameters list
99
+ #
100
+ # @param [Aws::CloudFormation::Stack] stack cloudformation stack instance
101
+ # @param [Array] keys list of keys
102
+ def generate_parameters(stack, keys)
103
+ keys.map { |k| { parameter_key: k, parameter_value: get_resource(stack, k) } }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,72 @@
1
+ require 'open3'
2
+
3
+ module Enscalator
4
+ module Helpers
5
+ # Executed command as sub-processes with stdout and stderr streams
6
+ # taken from: https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
7
+ class SubProcess
8
+ # Create new subprocess and execute command there
9
+ #
10
+ # @param [String] cmd command to be executed
11
+ def initialize(cmd)
12
+ # standard input is not used
13
+ Open3.popen3(cmd) do |_stdin, stdout, stderr, thread|
14
+ { out: stdout, err: stderr }.each do |key, stream|
15
+ Thread.new do
16
+ until (line = stream.gets).nil?
17
+ # yield the block depending on the stream
18
+ if key == :out
19
+ yield line, nil, thread if block_given?
20
+ else
21
+ yield nil, line, thread if block_given?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ thread.join # wait for external process to finish
27
+ end
28
+ end
29
+ end
30
+
31
+ # Call script
32
+ #
33
+ # @param [String] region AWS region identifier
34
+ # @param [String] dependent_stack_name name of the stack current stack depends on
35
+ # @param [String] script_path path to script
36
+ # @param [Array] keys list of keys
37
+ # @param [String] prepend_args prepend arguments
38
+ # @param [String] append_args append arguments
39
+ # @deprecated this method is no longer used
40
+ def cfn_call_script(region,
41
+ dependent_stack_name,
42
+ script_path,
43
+ keys,
44
+ prepend_args: '',
45
+ append_args: '')
46
+ cfn = cfn_resource(cfn_client(region))
47
+ stack = wait_stack(cfn, dependent_stack_name)
48
+ args = get_resources(stack, keys).join(' ')
49
+ cmd = [script_path, prepend_args, args, append_args]
50
+ begin
51
+ run_cmd(cmd)
52
+ rescue Errno::ENOENT
53
+ puts $ERROR_INFO.to_s
54
+ STDERR.puts cmd
55
+ end
56
+ end
57
+
58
+ # Run command and print captured output to corresponding standard streams
59
+ #
60
+ # @param [Array] cmd command array to be executed
61
+ # @return [String] produced output from executed command
62
+ def run_cmd(cmd)
63
+ # use contracts to get rid of exceptions: https://github.com/egonSchiele/contracts.ruby
64
+ fail ArgumentError, "Expected Array, but actually was given #{cmd.class}" unless cmd.is_a?(Array)
65
+ fail ArgumentError, 'Argument cannot be empty' if cmd.empty?
66
+ SubProcess.new(cmd.join(' ')) do |stdout, stderr, _thread|
67
+ STDOUT.puts stdout if stdout
68
+ STDERR.puts stderr if stderr
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ module Enscalator
2
+ module Helpers
3
+ # Helpers that wrap some
4
+ module Wrappers
5
+ # Cloudformation client
6
+ #
7
+ # @param [String] region Region in Amazon AWS
8
+ # @return [Aws::CloudFormation::Client]
9
+ # @raise [ArgumentError] when region is not given
10
+ def cfn_client(region)
11
+ fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
12
+ opts = {}
13
+ opts[:region] = region unless Aws.config.key?(:region)
14
+ Aws::CloudFormation::Client.new(opts)
15
+ end
16
+
17
+ # EC2 client
18
+ #
19
+ # @param [String] region Region in Amazon AWS
20
+ # @return [Aws::EC2::Client]
21
+ # @raise [ArgumentError] when region is not given
22
+ def ec2_client(region)
23
+ fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
24
+ opts = {}
25
+ opts[:region] = region unless Aws.config.key?(:region)
26
+ # noinspection RubyArgCount
27
+ Aws::EC2::Client.new(opts)
28
+ end
29
+
30
+ # Route 53 client
31
+ #
32
+ # @param [String] region AWS region identifier
33
+ # @return [Aws::Route53::Client]
34
+ # @raise [ArgumentError] when region is not given
35
+ def route53_client(region)
36
+ fail ArgumentError, 'Unable to proceed without region' if region.blank? && !Aws.config.key?(:region)
37
+ opts = {}
38
+ opts[:region] = region unless Aws.config.key?(:region)
39
+ # noinspection RubyArgCount
40
+ Aws::Route53::Client.new(opts)
41
+ end
42
+
43
+ # Cloudformation resource
44
+ #
45
+ # @param [Aws::CloudFormation::Client] client instance of AWS Cloudformation client
46
+ # @return [Aws::CloudFormation::Resource]
47
+ # @raise [ArgumentError] when client is not provided or its not expected class type
48
+ def cfn_resource(client)
49
+ fail ArgumentError,
50
+ 'must be instance of Aws::CloudFormation::Client' unless client.instance_of?(Aws::CloudFormation::Client)
51
+ Aws::CloudFormation::Resource.new(client: client)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,127 @@
1
+ require_relative 'helpers/sub_process'
2
+ require_relative 'helpers/wrappers'
3
+ require_relative 'helpers/stack'
4
+ require_relative 'helpers/dns'
5
+
6
+ # Enscalator
7
+ module Enscalator
8
+ # Default directory to save generated assets like ssh keys, configs and etc.
9
+ ASSETS_DIR = File.join(ENV['HOME'], ".#{name.split('::').first.downcase}")
10
+
11
+ # Collection of helper classes and static methods
12
+ module Helpers
13
+ include Wrappers
14
+ include Stack
15
+ include Dns
16
+
17
+ # Initialize enscalator directory
18
+ # @return [String]
19
+ def init_assets_dir
20
+ FileUtils.mkdir_p(Enscalator::ASSETS_DIR) unless Dir.exist?(Enscalator::ASSETS_DIR)
21
+ end
22
+
23
+ # Provision Aws.config with custom settings
24
+ # @param [String] region valid aws region
25
+ # @param [String] profile_name aws credentials profile name
26
+ def init_aws_config(region, profile_name: nil)
27
+ fail ArgumentError, 'Unable to proceed without region' if region.blank?
28
+ opts = {}
29
+ opts[:region] = region
30
+ opts[:credentials] = Aws::SharedCredentials.new(profile_name: profile_name) unless profile_name.blank?
31
+ Aws.config.update(opts)
32
+ end
33
+
34
+ # Find ami images registered
35
+ #
36
+ # @param [Aws::EC2::Client] client instance of AWS EC2 client
37
+ # @return [Hash] images satisfying query conditions
38
+ # @raise [ArgumentError] when client is not provided or its not expected class type
39
+ def find_ami(client, owners: ['self'], filters: nil)
40
+ fail ArgumentError, 'must be instance of Aws::EC2::Client' unless client.instance_of?(Aws::EC2::Client)
41
+ query = {}
42
+ query[:dry_run] = false
43
+ query[:owners] = owners if owners.is_a?(Array) && owners.any?
44
+ query[:filters] = filters if filters.is_a?(Array) && filters.any?
45
+ client.describe_images(query)
46
+ end
47
+
48
+ # Generate ssh keyname from app_name, region and stack name
49
+ #
50
+ # @param [String] app_name application name
51
+ # @param [String] region aws region
52
+ # @param [String] stack_name cloudformation stack name
53
+ def gen_ssh_key_name(app_name, region, stack_name)
54
+ [app_name, region, stack_name].map(&:underscore).join('_')
55
+ end
56
+
57
+ # Create ssh public/private key pair, save private key for current user
58
+ #
59
+ # @param [String] key_name key name
60
+ # @param [String] region aws region
61
+ # @param [Boolean] force_create force to create a new ssh key
62
+ def create_ssh_key(key_name, region, force_create: false)
63
+ # Ignoring attempts to generate new ssh key when not deploying
64
+ if @options && @options[:expand]
65
+ warn '[Warning] SSH key can be generated only for create or update stack actions'
66
+ return
67
+ end
68
+
69
+ client = ec2_client(region)
70
+ aws_profile = if Aws.config.key?(:credentials)
71
+ creds = Aws.config[:credentials]
72
+ creds.profile_name if creds.respond_to?(:profile_name)
73
+ end
74
+ target_dir = File.join(Enscalator::ASSETS_DIR, aws_profile ? aws_profile : 'default')
75
+ FileUtils.mkdir_p(target_dir) unless Dir.exist? target_dir
76
+ if !client.describe_key_pairs.key_pairs.collect(&:key_name).include?(key_name) || force_create
77
+ # delete existed ssh key
78
+ client.delete_key_pair(key_name: key_name)
79
+
80
+ # create a new ssh key
81
+ key_pair = client.create_key_pair(key_name: key_name)
82
+ STDERR.puts "Created new ssh key with fingerprint: #{key_pair.key_fingerprint}"
83
+
84
+ # save private key for current user
85
+ private_key = File.join(target_dir, key_name)
86
+ File.open(private_key, 'w') do |wfile|
87
+ wfile.write(key_pair.key_material)
88
+ end
89
+ STDERR.puts "Saved created key to: #{private_key}"
90
+ File.chmod(0600, private_key)
91
+ else
92
+ key_fingerprint =
93
+ begin
94
+ Aws::EC2::KeyPair.new(key_name, client: client).key_fingerprint
95
+ rescue NotImplementedError
96
+ # TODO: after upgrade of aws-sdk use only Aws::EC2::KeyPairInfo
97
+ Aws::EC2::KeyPairInfo.new(key_name, client: client).key_fingerprint
98
+ end
99
+ STDERR.puts "Found existing ssh key with fingerprint: #{key_fingerprint}"
100
+ end
101
+ end
102
+
103
+ # Read user data from file
104
+ #
105
+ # @param [String] app_name application name
106
+ def read_user_data(app_name)
107
+ user_data_path = File.join(File.expand_path('..', __FILE__), 'plugins', 'user-data', app_name)
108
+ fail("User data path #{user_data_path} not exists") unless File.exist?(user_data_path)
109
+ File.read(user_data_path)
110
+ end
111
+
112
+ # Convert hash with nested values to flat hash
113
+ #
114
+ # @param [Hash] input that should be flatten
115
+ def flatten_hash(input)
116
+ input.each_with_object({}) do |(k, v), h|
117
+ if v.is_a?(Hash)
118
+ flatten_hash(v).map do |h_k, h_v|
119
+ h["#{k}.#{h_k}".to_sym] = h_v
120
+ end
121
+ else
122
+ h[k] = v
123
+ end
124
+ end
125
+ end
126
+ end # module Helpers
127
+ end # module Enscalator