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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f9322921e73eb7ffbce7f78845f5cc43e1fe63cb
4
- data.tar.gz: 7eee908fc33ff6fdf6633902f4b6efb0095b8e52
3
+ metadata.gz: eb751f23604391d421be5540c9006217eb0d91b3
4
+ data.tar.gz: deb14051a07a1efd61162ff3f489c80c8513a54f
5
5
  SHA512:
6
- metadata.gz: 50d8dd4fa38c7015a08ed115173e0af9eb8221f8c238cd792990e5a46cb86da5cf933d5ddc9b53fad86b822bdb17105ec9bfd6d4bbff10495410dde431401288
7
- data.tar.gz: fa1af902021af37359f2f14cc4fb88817ed99242902fd7c6044aec2880aec74d998fbf6f690f9f621b51f99461f74b256fa23d43aaf7b6156f84bc4122568c70
6
+ metadata.gz: 3935ee1cabb06227d9f21375e90deabf10f7c552089c84ee014ef78a1527d8b3c5a65c17126696824f3cc21574c6fee9e178e6dc1da52326328a7ee43ac7b6c4
7
+ data.tar.gz: 263a73535b0908e021da6f4f4028efd1ac4e7fdb0c6f7be70e2ad2905c4a77ee511d09d9170f84bfb7d9ae0d1c9e5550f5026bd90641cd9986c965772edab166
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- elasticsearch-drain (0.0.2)
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.1.34)
18
- aws-sdk-resources (= 2.1.34)
19
- aws-sdk-core (2.1.34)
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.1.34)
22
- aws-sdk-core (= 2.1.34)
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.14)
28
- elasticsearch-api (= 1.0.14)
29
- elasticsearch-transport (= 1.0.14)
30
- elasticsearch-api (1.0.14)
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.14)
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.3)
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.11.2)
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
@@ -46,6 +46,8 @@ gem install bundler
46
46
  bundle install
47
47
  ```
48
48
 
49
+ To enable the tests that will hit the AWS APIs pass `ALLOW_DISABLED_VCR=true`
50
+
49
51
  Run test tests (unit and style):
50
52
  ```bash
51
53
  rake
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
@@ -38,12 +38,12 @@ module Elasticsearch
38
38
  @asg_client ||= AutoScaling.new(@asg_name, @region)
39
39
  end
40
40
 
41
- # Convience method to access {Elasticsearch::Drain::Nodes}
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
- # Convience method to access {Elasticsearch::Drain::Cluster#cluster}
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.nodes_in_asg(reload: true, instances: instances)
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
- # EC2 Client
11
- attr_reader :ec2_client
11
+ # AWS region
12
+ attr_reader :region
12
13
 
13
14
  def initialize(asg, region)
14
15
  @asg = asg
15
- @asg_client = Aws::AutoScaling::Client.new(region: region)
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
- @asg_client.describe_auto_scaling_instances.each do |page|
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
- @ec2_client.describe_instances(instance_ids: @instance_ids).each do |page|
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
- @asg_client.update_auto_scaling_group(
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
- resp = @asg_client.detach_instances(
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
- resp.activities.first.status_code == 'Successful'
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
- ensure_cluster_healthy
24
- drain_nodes
25
- remove_nodes
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 = active_nodes.map(&:ipaddress)
109
+ def instances(nodes)
110
+ instances = nodes.map(&:ipaddress)
45
111
  instances.join(' ')
46
112
  end
47
113
 
48
- def drain_nodes
49
- drainer.asg.min_size(0)
50
- nodes_to_drain = active_nodes.map(&:id).join(',')
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 remove_nodes # rubocop:disable Metrics/MethodLength
56
- while active_nodes.length > 0
57
- active_nodes.each do |instance|
58
- instance_id = drainer.asg.instance(instance.ipaddress).instance_id
59
- instance.instance_id = instance_id
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
- self.active_nodes = drainer.active_nodes_in_asg
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 2
152
+ sleep sleep_time
65
153
  else
66
- remove_node(instance)
67
- self.active_nodes = drainer.active_nodes_in_asg
68
- break if active_nodes.length < 1
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