elasticsearch-manager 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +81 -0
  7. data/LICENSE +201 -0
  8. data/README.md +55 -0
  9. data/REALEASES.md +0 -0
  10. data/bin/elasticsearch-manager +56 -0
  11. data/elasticsearch-manager.gemspec +33 -0
  12. data/lib/elasticsearch/client.rb +8 -0
  13. data/lib/elasticsearch/client/base.rb +71 -0
  14. data/lib/elasticsearch/client/elasticsearch.rb +43 -0
  15. data/lib/elasticsearch/manager.rb +8 -0
  16. data/lib/elasticsearch/manager/cmd.rb +62 -0
  17. data/lib/elasticsearch/manager/errors.rb +12 -0
  18. data/lib/elasticsearch/manager/manager.rb +60 -0
  19. data/lib/elasticsearch/manager/rollingrestart.rb +79 -0
  20. data/lib/elasticsearch/manager/version.rb +5 -0
  21. data/lib/elasticsearch/model.rb +5 -0
  22. data/lib/elasticsearch/model/health.rb +26 -0
  23. data/lib/elasticsearch/model/node.rb +40 -0
  24. data/lib/elasticsearch/model/routing_nodes.rb +28 -0
  25. data/lib/elasticsearch/model/shard.rb +22 -0
  26. data/lib/elasticsearch/model/state.rb +23 -0
  27. data/spec/client_spec.rb +33 -0
  28. data/spec/cmd_spec.rb +123 -0
  29. data/spec/esclient_spec.rb +64 -0
  30. data/spec/fixtures/health.json +12 -0
  31. data/spec/fixtures/health_initializing.json +12 -0
  32. data/spec/fixtures/health_realocating.json +12 -0
  33. data/spec/fixtures/health_red.json +12 -0
  34. data/spec/fixtures/health_unassigned.json +12 -0
  35. data/spec/fixtures/health_yellow.json +13 -0
  36. data/spec/fixtures/nodes_.json +837 -0
  37. data/spec/fixtures/state-node-initializing.json +193010 -0
  38. data/spec/fixtures/state.json +193018 -0
  39. data/spec/manager_spec.rb +234 -0
  40. data/spec/spec_helper.rb +226 -0
  41. metadata +266 -0
@@ -0,0 +1,8 @@
1
+ require_relative 'client/base'
2
+ require_relative 'client/elasticsearch'
3
+
4
+
5
+ module Elasticsearch
6
+ module Client
7
+ end
8
+ end
@@ -0,0 +1,71 @@
1
+ require 'logger'
2
+ require 'rest-client'
3
+
4
+ module Elasticsearch
5
+ module Client
6
+ class Base
7
+ def initialize(host = 'localhost', port = 9200, logger = Logger.new(STDOUT))
8
+ @host = host
9
+ @port = port
10
+ @logger = logger
11
+ end
12
+
13
+ def get(uri, params = nil)
14
+ raw = _get(uri, params)
15
+ if !raw.headers[:content_type].nil? && raw.headers[:content_type][/json/]
16
+ JSON.parse(raw)
17
+ else
18
+ raw
19
+ end
20
+ end
21
+
22
+ def put(uri, body, params = nil)
23
+ raw = _put(uri, body, params)
24
+ if !raw.headers[:content_type].nil? && raw.headers[:content_type][/json/]
25
+ JSON.parse(raw)
26
+ else
27
+ raw
28
+ end
29
+ end
30
+
31
+ def _get(uri, params = nil)
32
+ url = _build_url(uri)
33
+ opts = _prep_opts(params)
34
+
35
+ begin
36
+ return RestClient.get url, opts
37
+ rescue Exception => e
38
+ @logger.error(e.message)
39
+ raise e
40
+ raise IOError.new "Unable to complete get request: #{e}"
41
+ end
42
+ end
43
+
44
+ def _put(uri, body, params = nil)
45
+ url = _build_url(uri)
46
+ opts = _prep_opts(params)
47
+
48
+ begin
49
+ return RestClient.put url, body, opts
50
+ rescue Exception => e
51
+ @logger.error(e.message)
52
+ raise IOError.new "Unable to complete put request: #{e}"
53
+ end
54
+ end
55
+
56
+ def _prep_opts(params)
57
+ unless params.nil?
58
+ {:params => params}
59
+ else
60
+ {}
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ def _build_url(uri)
67
+ "http://#{@host}:#{@port}#{uri}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,43 @@
1
+ require 'json'
2
+
3
+
4
+ module Elasticsearch
5
+ module Client
6
+ class ESClient < Base
7
+ def status
8
+ data = get('/_cluster/health')
9
+ data['status']
10
+ end
11
+
12
+ def green?
13
+ status == 'green'
14
+ end
15
+
16
+ def health
17
+ get('/_cluster/health')
18
+ end
19
+
20
+ def nodes
21
+ get('/_nodes')
22
+ end
23
+
24
+ def state
25
+ get('/_cluster/state')
26
+ end
27
+
28
+ def settings
29
+ get('/_cluster/settings')
30
+ end
31
+
32
+ def routing(disable = true)
33
+ data = {
34
+ 'transient' => {
35
+ 'cluster.routing.allocation.enable' => disable ? 'none' : 'all'
36
+ }
37
+ }.to_json
38
+
39
+ put('/_cluster/settings', data)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'manager/manager'
2
+ require_relative 'manager/rollingrestart'
3
+ require_relative 'manager/version'
4
+
5
+ module Elasticsearch
6
+ module Manager
7
+ end
8
+ end
@@ -0,0 +1,62 @@
1
+ require 'colorize'
2
+ require 'net/ssh'
3
+
4
+ require 'elasticsearch/manager/manager'
5
+ require 'elasticsearch/manager/rollingrestart'
6
+
7
+
8
+ module Elasticsearch
9
+ module Manager
10
+ module CMD
11
+ include Elasticsearch::Manager
12
+
13
+ def self.rolling_restart(opts)
14
+ manager = _manager(opts)
15
+ # Check that the cluster is stable?
16
+ unless manager.cluster_stable?
17
+ print_cluster_stable(manager)
18
+ return 2
19
+ end
20
+ puts "Discovering cluster members...\n"
21
+ manager.cluster_members!
22
+ timeout = opts[:timeout] || 600
23
+ sleep_interval = opts[:sleep_interval] || 30
24
+ begin
25
+ manager.rolling_restart(timeout, sleep_interval)
26
+ rescue Exception => e
27
+ puts e
28
+ return 2
29
+ end
30
+ puts 'Rolling restart complete.'
31
+ return 0
32
+ end
33
+
34
+ def self.status(opts)
35
+ manager = _manager(opts)
36
+ status = manager.cluster_status
37
+ puts "The Elasticsearch cluster is currently: #{status.colorize(status.to_sym)}"
38
+ return 0
39
+ end
40
+
41
+ protected
42
+ def self._manager(opts)
43
+ ESManager.new(opts[:hostname], opts[:port])
44
+ end
45
+
46
+ def self.print_cluster_stable(manager)
47
+ health = manager.cluster_health
48
+ puts 'The cluster is currently unstable! Not proceeding with rolling-restart'
49
+ puts "\tCluster status: #{health.status.colorize(health.status.to_sym)}"
50
+
51
+ relocating = health.relocating_shards == 0 ? :green : :red
52
+ puts "\tRelocating shards: #{health.relocating_shards.to_s.colorize(relocating)}"
53
+
54
+ initializing = health.initializing_shards == 0 ? :green : :red
55
+ puts "\tInitializing shards: #{health.relocating_shards.to_s.colorize(relocating)}"
56
+
57
+ unassigned = health.unassigned_shards == 0 ? :green : :red
58
+ puts "\tUnassigned shards: #{health.relocating_shards.to_s.colorize(relocating)}"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,12 @@
1
+ module Elasticsearch
2
+ module Manager
3
+ class StabalizationTimeout < StandardError
4
+ end
5
+
6
+ class NodeAvailableTimeout < StandardError
7
+ end
8
+
9
+ class UserRequestedStop < StandardError
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ require 'elasticsearch/client'
2
+ require 'elasticsearch/model'
3
+
4
+ module Elasticsearch
5
+ module Manager
6
+ class ESManager
7
+ include Elasticsearch::Model
8
+
9
+ attr_accessor :leader, :members, :nodes
10
+
11
+ def initialize(cluster_host = 'localhost', port = 9200)
12
+ @client = Elasticsearch::Client::ESClient.new(cluster_host, port)
13
+ @leader = nil
14
+ @nodes = nil
15
+ @members = nil
16
+ end
17
+
18
+ def cluster_green?
19
+ @client.green?
20
+ end
21
+
22
+ def cluster_status
23
+ @client.status
24
+ end
25
+
26
+ def cluster_stable?
27
+ health = cluster_health
28
+ moving = [health.relocating_shards, health.initializing_shards, health.unassigned_shards]
29
+ cluster_green? && moving.all? { |x| x == 0 }
30
+ end
31
+
32
+ def cluster_members!
33
+ state = cluster_state
34
+ @nodes = state.nodes
35
+ @leader = @nodes.select { |n| n.master }[0].ip
36
+ @members = @nodes.map { |n| n.ip }
37
+ end
38
+
39
+ def cluster_health
40
+ health = @client.health
41
+ Health.new.extend(Health::Representer).from_hash(health)
42
+ end
43
+
44
+ def cluster_state
45
+ state = @client.state
46
+ ClusterState.new.extend(ClusterState::Representer).from_hash(state)
47
+ end
48
+
49
+ def disable_routing
50
+ ret = @client.routing(true)
51
+ ret['transient']['cluster']['routing']['allocation']['enable'] == 'none'
52
+ end
53
+
54
+ def enable_routing
55
+ ret = @client.routing(false)
56
+ ret['transient']['cluster']['routing']['allocation']['enable'] == 'all'
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,79 @@
1
+ require 'colorize'
2
+ require 'net/ssh'
3
+ require 'timeout'
4
+ require 'highline'
5
+
6
+ require 'elasticsearch/manager/errors'
7
+ require 'elasticsearch/manager/manager'
8
+
9
+
10
+ module Elasticsearch
11
+ module Manager
12
+
13
+ class ESManager
14
+ def rolling_restart(timeout = 600, sleep_interval = 30)
15
+ highline = HighLine.new
16
+ @members.each do |m|
17
+ restart_node(m, timeout, sleep_interval) unless m == @leader
18
+ if m != @members[-1]
19
+ unless highline.agree('Continue with rolling restart of cluster? (y/n) ')
20
+ raise UserRequestedStop, "Stopping rolling restart at user request!".colorize(:red)
21
+ end
22
+ end
23
+ end
24
+ unless highline.agree("\nRestarting current cluster master, continue? (y/n) ")
25
+ raise UserRequestedStop, "Stopping rolling restart at user request before restarting master node!".colorize(:red)
26
+ end
27
+ restart_node(@leader, timeout, sleep_interval)
28
+ end
29
+
30
+ def restart_node(node_ip, timeout, sleep_interval)
31
+ puts "\nRestarting Elasticsearch on node: #{node_ip}"
32
+ raise "Could not disable shard routing prior to restarting node: #{node_ip}".colorize(:red) unless disable_routing
33
+
34
+ Net::SSH.start(node_ip, ENV['USER']) do |ssh|
35
+ ssh.exec 'sudo service elasticsearch restart'
36
+ end
37
+ puts "Elasticsearch restarted on node: #{node_ip}"
38
+
39
+ begin
40
+ wait_for_node_available(node_ip, timeout, sleep_interval)
41
+ puts "Node back up!".colorize(:green)
42
+ rescue Timeout::Error
43
+ raise NodeAvailableTimeout, "Node did not become available after waiting #{timeout} seconds...".colorize(:red)
44
+ end
45
+
46
+ raise "Could not re-enable shard routing following restart of node: #{node_ip}".colorize(:red) unless enable_routing
47
+
48
+ begin
49
+ wait_for_stable(timeout, sleep_interval)
50
+ puts "Cluster stabilized!".colorize(:green)
51
+ rescue Timeout::Error
52
+ raise StabalizationTimeout, "Cluster not re-stabilize after waiting #{timeout} seconds...".colorize(:red)
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def wait_for_stable(timeout = 600, sleep_interval = 30)
59
+ Timeout.timeout(timeout) do
60
+ while !cluster_stable?
61
+ puts "Waiting for cluster to stabilize...".colorize(:yellow)
62
+ sleep(sleep_interval)
63
+ end
64
+ end
65
+ end
66
+
67
+ def wait_for_node_available(node_ip, timeout = 600, sleep_interval = 30)
68
+ Timeout.timeout(timeout) do
69
+ state = cluster_state
70
+ while !state.nodes.map { |n| n.ip }.include?(node_ip)
71
+ puts "Waiting for node to become available...".colorize(:yellow)
72
+ sleep(sleep_interval)
73
+ state = cluster_state
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ module Elasticsearch
2
+ module Manager
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'model/health'
2
+ require_relative 'model/node'
3
+ require_relative 'model/routing_nodes'
4
+ require_relative 'model/state'
5
+ require_relative 'model/shard'
@@ -0,0 +1,26 @@
1
+ require 'ostruct'
2
+ require 'representable/json'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class Health < OpenStruct
7
+ module Representer
8
+ include Representable::JSON
9
+ include Representable::Hash
10
+ include Representable::Hash::AllowSymbols
11
+
12
+ property :cluster_name
13
+ property :status
14
+ property :timed_out
15
+ property :number_of_nodes
16
+ property :number_of_data_nodes
17
+ property :active_primary_shards
18
+ property :active_shards
19
+ property :relocating_shards
20
+ property :initializing_shards
21
+ property :unassigned_shards
22
+ end
23
+ extend Representer
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'ostruct'
2
+ require 'representable/json'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class Node < OpenStruct
7
+ module Representer
8
+ include Representable::JSON
9
+ include Representable::Hash
10
+ include Representable::Hash::AllowSymbols
11
+
12
+ property :name
13
+ property :transport_address
14
+ property :host
15
+ property :ip
16
+ property :version
17
+ property :build
18
+ property :http_address
19
+
20
+ nested :settings do
21
+ nested :gateway do
22
+ property :gateway_expected_nodes, as: :expected_nodes
23
+ end
24
+
25
+ nested :http do
26
+ property :http_port, as: :port
27
+ end
28
+
29
+ nested :path do
30
+ property :data_path, as: :data
31
+ property :home_path, as: :home
32
+ property :logs_path, as: :logs
33
+ property :conf_path, as: :conf
34
+ end
35
+ end
36
+ end
37
+ extend Representer
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ require 'ostruct'
2
+ require 'representable/json'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class RoutingNodes < OpenStruct
7
+ module Representer
8
+ include Representable::JSON
9
+ include Representable::Hash
10
+ include Representable::Hash::AllowSymbols
11
+
12
+ property :cluster_name
13
+ property :master_node
14
+ nested :routing_nodes do
15
+ property :unassigned, setter: (lambda do |v,args|
16
+ self.unassigned = v.map {|s| Elasticsearch::Manager::Model::Shard.new.extend(Elastiman::Model::Shard::Representer).from_hash(s) }
17
+ end)
18
+ property :nodes, setter: (lambda do |v,args|
19
+ self.nodes = v.map do |id, shards|
20
+ shards.each { |shard| Elasticsearch::Manager::Model::Shard.new.extend(Elastiman::Model::Shard::Representer).from_hash(shard) }
21
+ end.flatten
22
+ end)
23
+ end
24
+ end
25
+ extend Representer
26
+ end
27
+ end
28
+ end