enscalator 0.4.0.pre.alpha.pre.16

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.
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