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