elasticsearch-drain 0.0.2 → 0.1.1
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 +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
|