elasticsearch-manager 0.1.0

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