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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +59 -0
- data/.travis.yml +22 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +43 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/enscalator.gemspec +57 -0
- data/exe/enscalator +13 -0
- data/lib/enscalator/core/cf_parameters.rb +146 -0
- data/lib/enscalator/core/cf_resources.rb +225 -0
- data/lib/enscalator/core/instance_type.rb +205 -0
- data/lib/enscalator/core/network_config.rb +21 -0
- data/lib/enscalator/core.rb +10 -0
- data/lib/enscalator/enapp.rb +248 -0
- data/lib/enscalator/helpers/dns.rb +62 -0
- data/lib/enscalator/helpers/stack.rb +107 -0
- data/lib/enscalator/helpers/sub_process.rb +72 -0
- data/lib/enscalator/helpers/wrappers.rb +55 -0
- data/lib/enscalator/helpers.rb +127 -0
- data/lib/enscalator/plugins/amazon_linux.rb +93 -0
- data/lib/enscalator/plugins/auto_scale.rb +80 -0
- data/lib/enscalator/plugins/core_os.rb +88 -0
- data/lib/enscalator/plugins/couchbase.rb +98 -0
- data/lib/enscalator/plugins/debian.rb +71 -0
- data/lib/enscalator/plugins/elastic_beanstalk.rb +74 -0
- data/lib/enscalator/plugins/elasticache.rb +168 -0
- data/lib/enscalator/plugins/elasticsearch_amazon.rb +75 -0
- data/lib/enscalator/plugins/elasticsearch_bitnami.rb +198 -0
- data/lib/enscalator/plugins/elasticsearch_opsworks.rb +225 -0
- data/lib/enscalator/plugins/elb.rb +139 -0
- data/lib/enscalator/plugins/nat_gateway.rb +71 -0
- data/lib/enscalator/plugins/rds.rb +141 -0
- data/lib/enscalator/plugins/redis.rb +38 -0
- data/lib/enscalator/plugins/rethink_db.rb +21 -0
- data/lib/enscalator/plugins/route53.rb +143 -0
- data/lib/enscalator/plugins/ubuntu.rb +85 -0
- data/lib/enscalator/plugins/user-data/elasticsearch +367 -0
- data/lib/enscalator/plugins/vpc_peering_connection.rb +48 -0
- data/lib/enscalator/plugins.rb +30 -0
- data/lib/enscalator/rich_template_dsl.rb +209 -0
- data/lib/enscalator/templates/vpc_peering.rb +112 -0
- data/lib/enscalator/templates.rb +20 -0
- data/lib/enscalator/version.rb +5 -0
- data/lib/enscalator/vpc.rb +11 -0
- data/lib/enscalator/vpc_with_nat_gateway.rb +311 -0
- data/lib/enscalator/vpc_with_nat_instance.rb +402 -0
- data/lib/enscalator.rb +103 -0
- 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
|