elasticsearch-drain 0.0.2 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +16 -13
- data/README.md +2 -0
- data/Rakefile +2 -1
- data/lib/elasticsearch/drain.rb +14 -4
- data/lib/elasticsearch/drain/autoscaling.rb +56 -11
- data/lib/elasticsearch/drain/cli.rb +115 -21
- data/lib/elasticsearch/drain/cluster.rb +5 -0
- data/lib/elasticsearch/drain/node.rb +1 -1
- data/lib/elasticsearch/drain/nodes.rb +3 -3
- data/lib/elasticsearch/drain/util.rb +25 -0
- data/lib/elasticsearch/drain/version.rb +1 -1
- data/test/cassettes/autoscaling.yml +1377 -886
- data/test/elasticsearch/drain/test_autoscaling.rb +39 -1
- data/test/test_helper.rb +13 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb751f23604391d421be5540c9006217eb0d91b3
|
4
|
+
data.tar.gz: deb14051a07a1efd61162ff3f489c80c8513a54f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3935ee1cabb06227d9f21375e90deabf10f7c552089c84ee014ef78a1527d8b3c5a65c17126696824f3cc21574c6fee9e178e6dc1da52326328a7ee43ac7b6c4
|
7
|
+
data.tar.gz: 263a73535b0908e021da6f4f4028efd1ac4e7fdb0c6f7be70e2ad2905c4a77ee511d09d9170f84bfb7d9ae0d1c9e5550f5026bd90641cd9986c965772edab166
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
elasticsearch-drain (0.0.
|
4
|
+
elasticsearch-drain (0.0.1)
|
5
5
|
aws-sdk (~> 2)
|
6
6
|
elasticsearch (~> 1.0)
|
7
7
|
thor (~> 0.19)
|
@@ -14,25 +14,25 @@ GEM
|
|
14
14
|
ast (2.1.0)
|
15
15
|
astrolabe (1.3.1)
|
16
16
|
parser (~> 2.2)
|
17
|
-
aws-sdk (2.
|
18
|
-
aws-sdk-resources (= 2.
|
19
|
-
aws-sdk-core (2.
|
17
|
+
aws-sdk (2.6.14)
|
18
|
+
aws-sdk-resources (= 2.6.14)
|
19
|
+
aws-sdk-core (2.6.14)
|
20
20
|
jmespath (~> 1.0)
|
21
|
-
aws-sdk-resources (2.
|
22
|
-
aws-sdk-core (= 2.
|
21
|
+
aws-sdk-resources (2.6.14)
|
22
|
+
aws-sdk-core (= 2.6.14)
|
23
23
|
coderay (1.1.0)
|
24
24
|
crack (0.4.2)
|
25
25
|
safe_yaml (~> 1.0.0)
|
26
26
|
docile (1.1.5)
|
27
|
-
elasticsearch (1.0
|
28
|
-
elasticsearch-api (= 1.0
|
29
|
-
elasticsearch-transport (= 1.0
|
30
|
-
elasticsearch-api (1.0
|
27
|
+
elasticsearch (1.1.0)
|
28
|
+
elasticsearch-api (= 1.1.0)
|
29
|
+
elasticsearch-transport (= 1.1.0)
|
30
|
+
elasticsearch-api (1.1.0)
|
31
31
|
multi_json
|
32
32
|
elasticsearch-extensions (0.0.18)
|
33
33
|
ansi
|
34
34
|
ruby-prof
|
35
|
-
elasticsearch-transport (1.0
|
35
|
+
elasticsearch-transport (1.1.0)
|
36
36
|
faraday
|
37
37
|
multi_json
|
38
38
|
faraday (0.9.2)
|
@@ -53,7 +53,7 @@ GEM
|
|
53
53
|
guard-compat (~> 1.2)
|
54
54
|
minitest (>= 3.0)
|
55
55
|
hashdiff (0.2.2)
|
56
|
-
jmespath (1.1
|
56
|
+
jmespath (1.3.1)
|
57
57
|
json (1.8.3)
|
58
58
|
listen (3.0.3)
|
59
59
|
rb-fsevent (>= 0.9.3)
|
@@ -62,7 +62,7 @@ GEM
|
|
62
62
|
method_source (0.8.2)
|
63
63
|
minitest (5.8.1)
|
64
64
|
mixlib-shellout (2.2.3)
|
65
|
-
multi_json (1.
|
65
|
+
multi_json (1.12.1)
|
66
66
|
multipart-post (2.0.0)
|
67
67
|
nenv (0.2.0)
|
68
68
|
notiffany (0.0.8)
|
@@ -127,3 +127,6 @@ DEPENDENCIES
|
|
127
127
|
vcr (~> 2.9)
|
128
128
|
webmock (~> 1.21)
|
129
129
|
yard (~> 0.8)
|
130
|
+
|
131
|
+
BUNDLED WITH
|
132
|
+
1.13.2
|
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -14,6 +14,8 @@ Rake::TestTask.new do |t|
|
|
14
14
|
t.pattern = 'test/**/test_*.rb'
|
15
15
|
end
|
16
16
|
|
17
|
+
ENV['ES_VERSION'] ||= '1.7.2'
|
18
|
+
|
17
19
|
def elasticsearch_command
|
18
20
|
path = "tmp/elasticsearch-#{ENV['ES_VERSION']}/bin/elasticsearch"
|
19
21
|
path = ::File.expand_path(path, __dir__)
|
@@ -22,7 +24,6 @@ end
|
|
22
24
|
|
23
25
|
ENV['TEST_CLUSTER_NODES'] = '1'
|
24
26
|
ENV['TEST_CLUSTER_COMMAND'] = elasticsearch_command
|
25
|
-
ENV['ES_VERSION'] ||= '1.7.2'
|
26
27
|
|
27
28
|
namespace :elasticsearch do
|
28
29
|
task :clean do
|
data/lib/elasticsearch/drain.rb
CHANGED
@@ -38,12 +38,12 @@ module Elasticsearch
|
|
38
38
|
@asg_client ||= AutoScaling.new(@asg_name, @region)
|
39
39
|
end
|
40
40
|
|
41
|
-
#
|
41
|
+
# Convenience method to access {Elasticsearch::Drain::Nodes}
|
42
42
|
def nodes
|
43
|
-
Nodes.new(client, asg)
|
43
|
+
@nodes ||= Nodes.new(client, asg)
|
44
44
|
end
|
45
45
|
|
46
|
-
#
|
46
|
+
# Convenience method to access {Elasticsearch::Drain::Cluster#cluster}
|
47
47
|
#
|
48
48
|
# @return [Elasticsearch::API::Cluster] Elasticsearch cluster client
|
49
49
|
def cluster
|
@@ -52,11 +52,21 @@ module Elasticsearch
|
|
52
52
|
|
53
53
|
def active_nodes_in_asg
|
54
54
|
instances = asg.instances
|
55
|
-
nodes.
|
55
|
+
nodes.filter_nodes(instances, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
module Errors
|
59
|
+
class WaiterExpired < RuntimeError
|
60
|
+
def new(_msg = nil)
|
61
|
+
'Waiter Expired' + $ERROR_INFO
|
62
|
+
end
|
63
|
+
end
|
64
|
+
class NodeNotFound < RuntimeError; end
|
56
65
|
end
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
69
|
+
require_relative 'drain/util'
|
60
70
|
require_relative 'drain/autoscaling'
|
61
71
|
require_relative 'drain/version'
|
62
72
|
require_relative 'drain/base'
|
@@ -1,26 +1,34 @@
|
|
1
1
|
module Elasticsearch
|
2
2
|
class Drain
|
3
3
|
class AutoScaling
|
4
|
+
include Drain::Util
|
4
5
|
|
5
6
|
# @attribute [r]
|
6
7
|
# EC2 AutoScaling Group name
|
7
8
|
attr_reader :asg
|
8
9
|
|
9
10
|
# @attribute [r]
|
10
|
-
#
|
11
|
-
attr_reader :
|
11
|
+
# AWS region
|
12
|
+
attr_reader :region
|
12
13
|
|
13
14
|
def initialize(asg, region)
|
14
15
|
@asg = asg
|
15
|
-
@
|
16
|
-
@ec2_client = Aws::EC2::Client.new(region: region)
|
16
|
+
@region = region
|
17
17
|
@instances = nil
|
18
18
|
@instance_ids = nil
|
19
19
|
end
|
20
20
|
|
21
|
+
def asg_client
|
22
|
+
Aws::AutoScaling::Client.new(region: region)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ec2_client
|
26
|
+
Aws::EC2::Client.new(region: region)
|
27
|
+
end
|
28
|
+
|
21
29
|
def find_instances_in_asg
|
22
30
|
instances = []
|
23
|
-
|
31
|
+
asg_client.describe_auto_scaling_instances.each do |page|
|
24
32
|
instances << page.auto_scaling_instances.map do |i|
|
25
33
|
i.instance_id if i.auto_scaling_group_name == asg
|
26
34
|
end
|
@@ -30,17 +38,30 @@ module Elasticsearch
|
|
30
38
|
@instance_ids = instances
|
31
39
|
end
|
32
40
|
|
41
|
+
# Get instances in an AutoScaling Group
|
42
|
+
#
|
43
|
+
# @return [Array<Aws::EC2::Types::Instance>] EC2 Instance objects
|
33
44
|
def describe_instances
|
34
45
|
instances = []
|
35
46
|
find_instances_in_asg if @instance_ids.nil?
|
36
47
|
return [] if @instance_ids.empty?
|
37
|
-
|
48
|
+
ec2_client.describe_instances(instance_ids: @instance_ids).each do |page|
|
38
49
|
instances << page.reservations.map(&:instances)
|
39
50
|
end
|
40
51
|
instances.flatten!
|
41
52
|
@instances = instances
|
42
53
|
end
|
43
54
|
|
55
|
+
# Describe an AutoScaling Group
|
56
|
+
#
|
57
|
+
# @return [Struct] AutoScaling Group
|
58
|
+
def describe_autoscaling_group
|
59
|
+
groups = asg_client.describe_auto_scaling_groups(
|
60
|
+
auto_scaling_group_names: [asg]
|
61
|
+
)
|
62
|
+
groups.auto_scaling_groups.first
|
63
|
+
end
|
64
|
+
|
44
65
|
def find_private_ips
|
45
66
|
instances = describe_instances.clone
|
46
67
|
return [] if instances.nil?
|
@@ -57,27 +78,51 @@ module Elasticsearch
|
|
57
78
|
def instance(ipaddress)
|
58
79
|
describe_instances if @instances.nil?
|
59
80
|
instances = @instances.clone
|
60
|
-
instances.find { |i| i.private_ip_address == ipaddress }
|
81
|
+
instance = instances.find { |i| i.private_ip_address == ipaddress }
|
82
|
+
fail Errors::NodeNotFound if instance.nil?
|
83
|
+
instance
|
61
84
|
end
|
62
85
|
|
63
86
|
# Sets the MinSize of an AutoScalingGroup
|
64
87
|
#
|
65
88
|
# @option [FixNum] count (0) The new MinSize of the AutoScalingGroup
|
66
89
|
# @return [Struct] Empty response from the sdk
|
67
|
-
def min_size(count = 0)
|
68
|
-
|
90
|
+
def min_size=(count = 0)
|
91
|
+
asg_client.update_auto_scaling_group(
|
69
92
|
auto_scaling_group_name: asg,
|
70
93
|
min_size: count
|
71
94
|
)
|
95
|
+
wait_until(count) do
|
96
|
+
min_size
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Gets the MinSize of an AutoScalingGroup
|
101
|
+
#
|
102
|
+
# @return [Integer] Value of MinSize of an AutoScalingGroup
|
103
|
+
def min_size
|
104
|
+
group = describe_autoscaling_group
|
105
|
+
group.min_size
|
106
|
+
end
|
107
|
+
|
108
|
+
# Gets the DesiredCapacity of an AutoScalingGroup
|
109
|
+
#
|
110
|
+
# @return [Integer] Value of DesiredCapacity of an AutoScalingGroup
|
111
|
+
def desired_capacity
|
112
|
+
group = describe_autoscaling_group
|
113
|
+
group.desired_capacity
|
72
114
|
end
|
73
115
|
|
74
116
|
def detach_instance(instance_id)
|
75
|
-
|
117
|
+
current_desired_capacity = desired_capacity
|
118
|
+
asg_client.detach_instances(
|
76
119
|
instance_ids: [instance_id],
|
77
120
|
auto_scaling_group_name: asg,
|
78
121
|
should_decrement_desired_capacity: true
|
79
122
|
)
|
80
|
-
|
123
|
+
wait_until(current_desired_capacity - 1) do
|
124
|
+
desired_capacity
|
125
|
+
end
|
81
126
|
end
|
82
127
|
end
|
83
128
|
end
|
@@ -2,27 +2,93 @@ require 'thor'
|
|
2
2
|
|
3
3
|
module Elasticsearch
|
4
4
|
class Drain
|
5
|
-
class CLI < ::Thor
|
5
|
+
class CLI < ::Thor # rubocop:disable Metrics/ClassLength
|
6
6
|
package_name :elasticsearch
|
7
7
|
|
8
8
|
attr_reader :drainer
|
9
9
|
attr_accessor :active_nodes
|
10
10
|
|
11
|
+
# rubocop:disable Metrics/LineLength
|
11
12
|
desc 'asg', 'Drain all documents from all nodes in an EC2 AutoScaling Group'
|
12
13
|
option :host, default: 'localhost:9200'
|
13
14
|
option :asg, required: true
|
14
15
|
option :region, required: true
|
16
|
+
option :nodes, type: :array, desc: 'A comma separated list of node IDs to drain. If specified, the --number option has no effect'
|
17
|
+
option :number, type: :numeric, desc: 'The number of nodes to drain'
|
18
|
+
option :continue, type: :boolean, default: true, desc: 'Whether to continue draining nodes once the first iteration of --number is complete'
|
19
|
+
# rubocop:enable Metrics/LineLength
|
15
20
|
def asg # rubocop:disable Metrics/MethodLength
|
16
21
|
@drainer = Elasticsearch::Drain.new(options[:host],
|
17
22
|
options[:asg],
|
18
23
|
options[:region])
|
24
|
+
|
19
25
|
ensure_cluster_healthy
|
20
26
|
@active_nodes = drainer.active_nodes_in_asg
|
27
|
+
|
28
|
+
# If :nodes are specified, :number has no effect
|
29
|
+
if options[:nodes]
|
30
|
+
say "Nodes #{options[:nodes].join(', ')} have been specified, the --number option has no effect"
|
31
|
+
number_to_drain = nil
|
32
|
+
currently_draining_nodes = nil
|
33
|
+
else
|
34
|
+
number_to_drain = options[:number]
|
35
|
+
currently_draining_nodes = drainer.cluster.currently_draining('_id')
|
36
|
+
end
|
37
|
+
|
38
|
+
# If a node or nodes are specified, only drain the requested node(s)
|
39
|
+
@active_nodes = active_nodes.find_all do |n|
|
40
|
+
instance_id = drainer.asg.instance(n.ipaddress).instance_id
|
41
|
+
options[:nodes].include?(instance_id)
|
42
|
+
end if options[:nodes]
|
43
|
+
|
21
44
|
do_exit { say_status 'Complete', 'Nothing to do', :green } if active_nodes.empty?
|
22
|
-
say_status 'Found Nodes', "AutoScalingGroup: #{instances}", :magenta
|
23
|
-
|
24
|
-
|
25
|
-
|
45
|
+
say_status 'Found Nodes', "AutoScalingGroup: #{instances(active_nodes)}", :magenta
|
46
|
+
|
47
|
+
until active_nodes.empty?
|
48
|
+
ensure_cluster_healthy
|
49
|
+
|
50
|
+
nodes = active_nodes
|
51
|
+
|
52
|
+
# If there are nodes in cluster settings "transient.cluster.routing.allocation.exclude"
|
53
|
+
# test if those nodes are still in the ASG. If so, work on them first unless nodes are
|
54
|
+
# specified.
|
55
|
+
if currently_draining_nodes
|
56
|
+
nodes_to_drain = active_nodes.find_all { |n| currently_draining_nodes.split(',').include?(n.id) }
|
57
|
+
|
58
|
+
# If the list of nodes_to_drain isn't empty, we want to set nodes to the list of nodes
|
59
|
+
# we've already been working on.
|
60
|
+
unless nodes_to_drain.empty?
|
61
|
+
nodes = nodes_to_drain
|
62
|
+
|
63
|
+
say_status 'Active Nodes', "Resuming drain process on #{instances(nodes)}", :magenta
|
64
|
+
end
|
65
|
+
|
66
|
+
# We should only process currently_draining_nodes once
|
67
|
+
currently_draining_nodes = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# If we specify a number but DON'T specify nodes, sample the active_nodes.
|
71
|
+
if number_to_drain
|
72
|
+
nodes = nodes.sample(number_to_drain.to_i)
|
73
|
+
say_status 'Active Nodes', "Sampled #{number_to_drain} nodes and got #{instances(nodes)}", :magenta
|
74
|
+
end
|
75
|
+
|
76
|
+
@active_nodes = nodes unless options[:continue]
|
77
|
+
|
78
|
+
drain_nodes(nodes)
|
79
|
+
remove_nodes(nodes)
|
80
|
+
|
81
|
+
# Remove the drained nodes from the list of active_nodes
|
82
|
+
@active_nodes -= nodes
|
83
|
+
|
84
|
+
unless active_nodes.empty?
|
85
|
+
say_status 'Drain Nodes', "#{active_nodes.length} nodes remaining", :green
|
86
|
+
|
87
|
+
sleep_time = wait_sleep_time
|
88
|
+
say_status 'Waiting', "Sleeping for #{sleep_time} seconds before the next iteration", :green
|
89
|
+
sleep sleep_time
|
90
|
+
end
|
91
|
+
end
|
26
92
|
say_status 'Complete', 'Draining nodes complete!', :green
|
27
93
|
end
|
28
94
|
|
@@ -40,32 +106,54 @@ module Elasticsearch
|
|
40
106
|
exit code
|
41
107
|
end
|
42
108
|
|
43
|
-
def instances
|
44
|
-
instances =
|
109
|
+
def instances(nodes)
|
110
|
+
instances = nodes.map(&:ipaddress)
|
45
111
|
instances.join(' ')
|
46
112
|
end
|
47
113
|
|
48
|
-
def
|
49
|
-
drainer.asg.min_size
|
50
|
-
|
114
|
+
def adjusted_min_size(nodes)
|
115
|
+
min_size = drainer.asg.min_size
|
116
|
+
desired_capacity = drainer.asg.desired_capacity
|
117
|
+
desired_min_size = if (desired_capacity - nodes.length) >= min_size # Removing the nodes won't violate the min_size
|
118
|
+
# Reduce the asg min_size proportionally
|
119
|
+
(min_size - nodes.length) <= 0 ? 0 : (min_size - nodes.length)
|
120
|
+
else
|
121
|
+
# Removing the nodes will result in the min_size being violated
|
122
|
+
desired_capacity - nodes.length
|
123
|
+
end
|
124
|
+
say_status 'Debug', 'min_size = #{min_size}, desired_capacity = #{desired_capacity}, desired_min_size = #{desired_min_size}', :magenta
|
125
|
+
desired_min_size
|
126
|
+
end
|
127
|
+
|
128
|
+
def drain_nodes(nodes)
|
129
|
+
drainer.asg.min_size = adjusted_min_size(nodes)
|
130
|
+
nodes_to_drain = nodes.map(&:id).join(',')
|
51
131
|
say_status 'Drain Nodes', "Draining nodes: #{nodes_to_drain}", :magenta
|
52
132
|
drainer.cluster.drain_nodes(nodes_to_drain, '_id')
|
53
133
|
end
|
54
134
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
135
|
+
def wait_sleep_time
|
136
|
+
ips = active_nodes.map(&:ipaddress)
|
137
|
+
bytes = drainer.nodes.filter_nodes(ips).map(&:bytes_stored)
|
138
|
+
sleep_time = 10
|
139
|
+
sleep_time = 30 if bytes.any? { |b| b >= 100_000 }
|
140
|
+
sleep_time = 60 if bytes.any? { |b| b >= 1_000_000 }
|
141
|
+
sleep_time = 120 if bytes.any? { |b| b >= 10_000_000_000 }
|
142
|
+
sleep_time
|
143
|
+
end
|
60
144
|
|
61
|
-
|
145
|
+
def remove_nodes(nodes) # rubocop:disable Metrics/MethodLength
|
146
|
+
while nodes.length > 0
|
147
|
+
sleep_time = wait_sleep_time
|
148
|
+
nodes.each do |instance|
|
149
|
+
instance = drainer.nodes.filter_nodes([instance.ipaddress], true).first
|
62
150
|
if instance.bytes_stored > 0
|
63
151
|
say_status 'Drain Status', "Node #{instance.ipaddress} has #{instance.bytes_stored} bytes to move", :blue
|
64
|
-
sleep
|
152
|
+
sleep sleep_time
|
65
153
|
else
|
66
|
-
remove_node(instance)
|
67
|
-
|
68
|
-
break if
|
154
|
+
next unless remove_node(instance)
|
155
|
+
nodes.delete_if { |n| n.ipaddress == instance.ipaddress }
|
156
|
+
break if nodes.length < 1
|
69
157
|
say_status 'Waiting', 'Sleeping for 1 minute before removing the next node', :green
|
70
158
|
sleep 60
|
71
159
|
end
|
@@ -74,18 +162,24 @@ module Elasticsearch
|
|
74
162
|
end
|
75
163
|
|
76
164
|
def remove_node(instance) # rubocop:disable Metrics/MethodLength
|
165
|
+
instance_id = drainer.asg.instance(instance.ipaddress).instance_id
|
166
|
+
instance.instance_id = instance_id
|
77
167
|
say_status(
|
78
168
|
'Removing Node',
|
79
169
|
"Removing #{instance.ipaddress} from Elasticsearch cluster and #{drainer.asg.asg} AutoScalingGroup",
|
80
|
-
:magenta
|
170
|
+
:magenta
|
171
|
+
)
|
81
172
|
sleep 5 unless instance.in_recovery?
|
82
173
|
node = "#{instance.instance_id}(#{instance.ipaddress})"
|
83
174
|
ensure_cluster_healthy
|
84
175
|
say_status 'ASG Remove Node', "Removing node: #{node} from AutoScalingGroup: #{drainer.asg.asg}", :magenta
|
85
176
|
drainer.asg.detach_instance(instance.instance_id)
|
177
|
+
sleep 2
|
86
178
|
ensure_cluster_healthy
|
87
179
|
say_status 'Terminate Instance', "Terminating instance: #{node}", :magenta
|
88
180
|
instance.terminate
|
181
|
+
rescue Errors::NodeNotFound
|
182
|
+
false
|
89
183
|
end
|
90
184
|
end
|
91
185
|
end
|