arborist 0.2.0.pre20170519125456 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +670 -1
  5. data/History.md +67 -0
  6. data/Manifest.txt +9 -6
  7. data/README.md +1 -3
  8. data/Rakefile +39 -4
  9. data/TODO.md +22 -31
  10. data/lib/arborist.rb +9 -2
  11. data/lib/arborist/cli.rb +67 -85
  12. data/lib/arborist/client.rb +125 -59
  13. data/lib/arborist/command/ack.rb +86 -0
  14. data/lib/arborist/command/reset.rb +48 -0
  15. data/lib/arborist/command/start.rb +11 -1
  16. data/lib/arborist/command/summary.rb +173 -0
  17. data/lib/arborist/command/tree.rb +215 -0
  18. data/lib/arborist/command/watch.rb +22 -22
  19. data/lib/arborist/dependency.rb +24 -4
  20. data/lib/arborist/event.rb +18 -2
  21. data/lib/arborist/event/node.rb +6 -2
  22. data/lib/arborist/event/node_warn.rb +16 -0
  23. data/lib/arborist/manager.rb +179 -48
  24. data/lib/arborist/mixins.rb +11 -0
  25. data/lib/arborist/monitor.rb +29 -17
  26. data/lib/arborist/monitor/connection_batching.rb +293 -0
  27. data/lib/arborist/monitor/socket.rb +101 -167
  28. data/lib/arborist/monitor_runner.rb +101 -24
  29. data/lib/arborist/node.rb +297 -68
  30. data/lib/arborist/node/ack.rb +1 -1
  31. data/lib/arborist/node/host.rb +26 -5
  32. data/lib/arborist/node/resource.rb +14 -5
  33. data/lib/arborist/node/root.rb +12 -3
  34. data/lib/arborist/node/service.rb +29 -26
  35. data/lib/arborist/node_subscription.rb +65 -0
  36. data/lib/arborist/observer.rb +8 -0
  37. data/lib/arborist/observer/action.rb +6 -0
  38. data/lib/arborist/subscription.rb +22 -16
  39. data/lib/arborist/tree_api.rb +7 -2
  40. data/spec/arborist/client_spec.rb +157 -51
  41. data/spec/arborist/dependency_spec.rb +21 -0
  42. data/spec/arborist/event/node_spec.rb +5 -0
  43. data/spec/arborist/event_spec.rb +3 -3
  44. data/spec/arborist/manager_spec.rb +626 -347
  45. data/spec/arborist/mixins_spec.rb +19 -0
  46. data/spec/arborist/monitor/socket_spec.rb +1 -2
  47. data/spec/arborist/monitor_runner_spec.rb +81 -29
  48. data/spec/arborist/monitor_spec.rb +89 -14
  49. data/spec/arborist/node/host_spec.rb +68 -0
  50. data/spec/arborist/node/resource_spec.rb +2 -0
  51. data/spec/arborist/node/root_spec.rb +13 -0
  52. data/spec/arborist/node/service_spec.rb +9 -0
  53. data/spec/arborist/node_spec.rb +673 -111
  54. data/spec/arborist/node_subscription_spec.rb +54 -0
  55. data/spec/arborist/observer/action_spec.rb +6 -0
  56. data/spec/arborist/observer_runner_spec.rb +8 -1
  57. data/spec/arborist/tree_api_spec.rb +111 -8
  58. data/spec/data/monitors/pings.rb +0 -11
  59. data/spec/data/monitors/port_checks.rb +0 -9
  60. data/spec/data/nodes/sidonie.rb +1 -0
  61. data/spec/data/nodes/vhosts.rb +23 -0
  62. data/spec/data/nodes/yevaud.rb +4 -2
  63. data/spec/spec_helper.rb +71 -1
  64. metadata +91 -28
  65. metadata.gz.sig +0 -0
  66. data/Events.md +0 -35
  67. data/Monitors.md +0 -155
  68. data/Nodes.md +0 -70
  69. data/Observers.md +0 -72
  70. data/Protocol.md +0 -276
  71. data/Tutorial.md +0 -8
@@ -0,0 +1,173 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'time'
5
+ require 'tty/table'
6
+
7
+ require 'arborist/cli' unless defined?( Arborist::CLI )
8
+ require 'arborist/client'
9
+
10
+
11
+ # Command to fetch down/acked/disabled nodes for quick display.
12
+ module Arborist::CLI::Summary
13
+ extend Arborist::CLI::Subcommand
14
+ using Arborist::TimeRefinements
15
+
16
+ BANNER = [
17
+ ' _ _ _',
18
+ ' __ _ _ _| |__ ___ _ _(_)__| |',
19
+ '/ _` | \'_| \'_ \\/ _ \\ \'_| (_-< _|',
20
+ '\\__,_|_| |_.__/\\___/_| |_/__/\\__| %s, %s nodes',
21
+ ]
22
+
23
+ desc 'Summarize known problems'
24
+
25
+ command :summary do |cmd|
26
+
27
+ cmd.flag [:s, :sort],
28
+ type: String,
29
+ desc: "Sort output by this node key",
30
+ arg_name: 'sort',
31
+ default_value: 'status_changed'
32
+
33
+ cmd.action do |globals, options, args|
34
+ client = Arborist::Client.new
35
+ status = client.status
36
+ nodes = client.fetch
37
+
38
+ down = get_status( nodes, 'down' )
39
+ warning = get_status( nodes, 'warn' )
40
+ acked = get_status( nodes, 'acked' )
41
+ disabled = get_status( nodes, 'disabled' )
42
+ quieted = get_status( nodes, 'quieted' )
43
+ problems = ! ( down.size + acked.size + disabled.size + warning.size ).zero?
44
+
45
+ prompt.say "Connected to: %s" % [ highlight_string(client.tree_api_url) ]
46
+ prompt.say "Status as of: %s" % [ hl.on_blue.bright_white(Time.now.to_s) ]
47
+
48
+ (0..2).each do |i|
49
+ prompt.say "%s" % [ hl.bold.bright_green( BANNER[i] ) ]
50
+ end
51
+ prompt.say hl.bold.bright_green( BANNER.last ) % [
52
+ highlight_string(status['server_version']),
53
+ highlight_string(status['nodecount'])
54
+ ]
55
+
56
+ puts
57
+ if problems
58
+ output_problems( disabled, acked, down, quieted, warning, options[:sort] )
59
+ else
60
+ prompt.say success_string( "No problems found!" )
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+
67
+ ###############
68
+ module_function
69
+ ###############
70
+
71
+ ### Since we fetch all nodes instead of doing separate
72
+ ### API searches, quickly return nodes of a given +status+.
73
+ def get_status( nodes, status )
74
+ return nodes.select{|n| n['status'] == status}
75
+ end
76
+
77
+
78
+ ### Output all problems.
79
+ ###
80
+ def output_problems( disabled, acked, down, quieted, warning, sort )
81
+ unless disabled.size.zero?
82
+ prompt.say hl.headline( "Disabled Nodes" )
83
+ display_table( *format_acked(disabled, sort) )
84
+ puts
85
+ end
86
+ unless acked.size.zero?
87
+ prompt.say hl.headline( "Acknowledged Outages" )
88
+ display_table( *format_acked(acked, sort) )
89
+ puts
90
+ end
91
+ unless warning.size.zero?
92
+ prompt.say hl.headline( "Warnings" )
93
+ header = [
94
+ highlight_string( 'identifier' ),
95
+ highlight_string( 'type' ),
96
+ highlight_string( 'when' ),
97
+ highlight_string( 'warnings' )
98
+ ]
99
+
100
+ display_table( header, format_warn(warning, sort) )
101
+ puts
102
+ end
103
+ unless down.size.zero?
104
+ prompt.say hl.headline( "Current Outages" )
105
+ header = [
106
+ highlight_string( 'identifier' ),
107
+ highlight_string( 'type' ),
108
+ highlight_string( 'when' ),
109
+ highlight_string( 'errors' )
110
+ ]
111
+
112
+ display_table( header, format_down(down, sort) )
113
+ prompt.say "%d nodes have been %s as a result of the above problems." % [
114
+ quieted.size,
115
+ hl.quieted( 'quieted' )
116
+ ]
117
+ puts
118
+ end
119
+ end
120
+
121
+
122
+ ### Prepare an array of acked/disabled nodes.
123
+ def format_acked( nodes, sort_key )
124
+ header = [
125
+ highlight_string( 'identifier' ),
126
+ highlight_string( 'type' ),
127
+ highlight_string( 'when' ),
128
+ highlight_string( 'who' ),
129
+ highlight_string( 'message' )
130
+ ]
131
+
132
+ rows = nodes.sort_by{|n| n[sort_key] }.each_with_object([]) do |node, acc|
133
+ acc << [
134
+ hl.disabled( node['identifier'] ),
135
+ node[ 'type' ],
136
+ Time.parse( node[ 'status_changed' ] ).as_delta,
137
+ node[ 'ack' ][ 'sender' ],
138
+ node[ 'ack' ][ 'message' ]
139
+ ]
140
+ end
141
+ return header, rows
142
+ end
143
+
144
+
145
+ ### Prepare an array of down nodes.
146
+ def format_down( nodes, sort_key )
147
+ return nodes.sort_by{|n| n[sort_key] }.each_with_object([]) do |node, acc|
148
+ errors = node[ 'errors' ].map{|err| "%s: %s" % [ err.first, err.last ]}
149
+ acc << [
150
+ hl.down( node['identifier'] ),
151
+ node[ 'type' ],
152
+ Time.parse( node[ 'status_changed' ] ).as_delta,
153
+ errors.join( "\n" )
154
+ ]
155
+ end
156
+ end
157
+
158
+
159
+ ### Prepare an array of nodes in a warning state.
160
+ def format_warn( nodes, sort_key )
161
+ return nodes.sort_by{|n| n[sort_key] }.each_with_object([]) do |node, acc|
162
+ warnings = node[ 'warnings' ].map{|err| "%s: %s" % [ err.first, err.last ]}
163
+ acc << [
164
+ hl.warn( node['identifier'] ),
165
+ node[ 'type' ],
166
+ Time.parse( node[ 'status_changed' ] ).as_delta,
167
+ warnings.join( "\n" )
168
+ ]
169
+ end
170
+ end
171
+
172
+ end # module Arborist::CLI::Summary
173
+
@@ -0,0 +1,215 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'pp'
5
+ require 'msgpack'
6
+ require 'tty-tree'
7
+
8
+ require 'arborist/cli' unless defined?( Arborist::CLI )
9
+ require 'arborist/client'
10
+
11
+
12
+ # Command to dump the node tree of a running Arborist manager
13
+ module Arborist::CLI::Tree
14
+ extend Arborist::CLI::Subcommand
15
+
16
+ desc 'Dump the node tree of the running manager'
17
+
18
+ command :tree do |cmd|
19
+ cmd.switch :raw,
20
+ desc: "Dump the node tree data as raw data instead of prettifying it.",
21
+ negatable: false
22
+ cmd.switch :path,
23
+ desc: "Include the parent path back to root, when using --from.",
24
+ negatable: false
25
+
26
+ cmd.flag [:f, :from],
27
+ type: String,
28
+ desc: "Start at a node other than the root.",
29
+ arg_name: 'identifier'
30
+ cmd.flag [:e, :depth],
31
+ type: Integer,
32
+ desc: "Limit the depth of the fetched tree.",
33
+ arg_name: 'integer'
34
+
35
+ cmd.action do |globals, options, args|
36
+ client = Arborist::Client.instance
37
+
38
+ opts = { tree: true }
39
+ opts[ :from ] = options[ :from ] if options[ :from ]
40
+ opts[ :depth ] = options[ :depth ] if options[ :depth ]
41
+
42
+ nodes = client.fetch( opts )
43
+
44
+ if options[:raw]
45
+ pp nodes.first
46
+
47
+ else
48
+ status = client.status
49
+ prompt.say "Arborist Manager %s {%s} [%s nodes] (uptime: %s secs)\n\n" % [
50
+ highlight_string( status['server_version'] ),
51
+ highlight_string( client.tree_api_url ),
52
+ highlight_string( status['nodecount'] ),
53
+ highlight_string( "%d" % status['uptime'] )
54
+ ]
55
+
56
+ root = nodes.first
57
+ root_data = {}
58
+
59
+ # Recursively fetch each parent node upwards to the
60
+ # Arborist root.
61
+ #
62
+ tree_data = if options[ :path ] && options[ :from ]
63
+ path_to_root = fetch_parents( root )
64
+ { node_description( path_to_root.shift ) => build_path( path_to_root, root ) }
65
+
66
+ # Just display starting at the specified root.
67
+ #
68
+ else
69
+ { node_description(root) => root_data }
70
+ end
71
+
72
+ root[ 'children' ].each_value do |node|
73
+ root_data[ node_description(node) ] = build_tree( node )
74
+ end
75
+
76
+ tree = TTY::Tree.new( tree_data )
77
+ prompt.say tree.render( indent: 4 )
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+
84
+ ###############
85
+ module_function
86
+ ###############
87
+
88
+ #### Reorganize the node data to format used by TTY::Tree.
89
+ def build_tree( node )
90
+ return [] if node[ 'children' ].empty?
91
+
92
+ children = []
93
+ node[ 'children' ].each_value do |child|
94
+ children << { node_description(child) => build_tree(child) }
95
+ end
96
+ return children
97
+ end
98
+
99
+
100
+ ### Given a sorted array of +nodes+, reorganize it for TTY::Tree.
101
+ def build_path( nodes, root )
102
+ children = []
103
+ parent_node = nodes.shift
104
+ return children unless parent_node
105
+
106
+ if parent_node == root
107
+ children << { node_description(parent_node) => build_tree(parent_node) }
108
+ else
109
+ children << { node_description(parent_node) => build_path(nodes, root) }
110
+ end
111
+ return children
112
+ end
113
+
114
+
115
+ ### Given a starting node, walk upwards through the tree until
116
+ ### reaching the Arborist root node. Returns an array of nodes,
117
+ ### sorted root down.
118
+ def fetch_parents( start_node )
119
+ client = Arborist::Client.instance
120
+ path = [ start_node ]
121
+ parent = start_node[ 'parent' ]
122
+ while parent
123
+ parent_node = client.fetch_node( parent )
124
+ path << parent_node
125
+ parent = parent_node[ 'parent' ]
126
+ end
127
+ return path.reverse
128
+ end
129
+
130
+
131
+ ### Return a description of the specified +node+.
132
+ def node_description( node )
133
+ desc = ""
134
+
135
+ case node['type']
136
+ when 'root'
137
+ desc << "%s" % [ hl.bold.bright_blue(node['type']) ]
138
+ else
139
+ desc << highlight_string( node['identifier'] )
140
+ desc << " %s" % [ hl.dark.white(node['type']) ]
141
+ end
142
+
143
+ desc << " [%s]" % [ node['description'] ] unless
144
+ !node['description'] || node['description'].empty?
145
+ desc << " (%s)" % [ status_description(node) ]
146
+
147
+ child_count = node[ 'children' ].length
148
+ desc << " [%d child node%s" % [
149
+ child_count, child_count == 1 ? ']' : 's]'
150
+ ] unless child_count.zero?
151
+
152
+ case node['status']
153
+ when 'down'
154
+ desc << errors_description( node )
155
+ when 'warn'
156
+ desc << warnings_description( node )
157
+ when 'quieted'
158
+ desc << quieted_reasons_description( node )
159
+ when 'acked'
160
+ desc << ack_description( node )
161
+ desc << "; was: "
162
+ desc << errors_description( node )
163
+ end
164
+
165
+ return desc
166
+ end
167
+
168
+
169
+ ### Return a more colorful description of the status of the given +node+.
170
+ def status_description( node )
171
+ status = node['status'] or return '-'
172
+ return hl.decorate( status, status.to_sym ) rescue status
173
+ end
174
+
175
+
176
+ ### Return the errors from the specified +node+ in a single line.
177
+ def errors_description( node )
178
+ errors = node['errors'] or return ''
179
+ return ' ' + errors.map do |monid, error|
180
+ "%s: %s" % [ monid, error ]
181
+ end.join( '; ' )
182
+ end
183
+
184
+ ### Return the warnings from the specified +node+ in a single line.
185
+ def warnings_description( node )
186
+ warnings = node['warnings'] or return ''
187
+ return ' ' + warnings.map do |monid, error|
188
+ "%s: %s" % [ monid, error ]
189
+ end.join( '; ' )
190
+ end
191
+
192
+
193
+ ### Return the quieted reasons from the specified +node+ in a single line.
194
+ def quieted_reasons_description( node )
195
+ reasons = node['quieted_reasons'] or return ''
196
+ return ' ' + reasons.map do |depname, reason|
197
+ "%s: %s" % [ depname, reason ]
198
+ end.join( '; ' )
199
+ end
200
+
201
+
202
+ ### Return a description of the acknowledgement from the node.
203
+ def ack_description( node )
204
+ ack = node['ack'] or return '(no ack)'
205
+
206
+ return " Acked by %s at %s%s: %s" % [
207
+ ack['sender'],
208
+ ack['time'],
209
+ ack['via'] ? ' via ' + ack['via'] : '',
210
+ ack['message']
211
+ ]
212
+ end
213
+
214
+ end # module Arborist::CLI::Tree
215
+
@@ -1,6 +1,7 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
3
 
4
+ require 'tty/spinner'
4
5
  require 'msgpack'
5
6
 
6
7
  require 'arborist/cli' unless defined?( Arborist::CLI )
@@ -27,6 +28,9 @@ module Arborist::CLI::Watch
27
28
  prompt.say "Subscribing to manager heartbeat events."
28
29
  sock.subscribe( 'sys.heartbeat' )
29
30
 
31
+ spinner = TTY::Spinner.new( "[ :spinner ] waiting for events...\r",
32
+ frames: HEARTBEAT_CHARACTERS, hide_cursor: true )
33
+
30
34
  begin
31
35
  last_runid = nil
32
36
  prompt.say "Watching for events on manager at %s" % [ client.event_api_url ]
@@ -47,22 +51,22 @@ module Arborist::CLI::Watch
47
51
  self.log.debug "Manager is alive (runid: %s)" % [ this_runid ]
48
52
  end
49
53
 
50
- $stderr.print( heartbeat() )
54
+ spinner.spin
51
55
  last_runid = this_runid
52
56
  when 'sys.node_added'
53
57
  prompt.say "[%s] «Node added» %s\n" % [
54
- hl(Time.now.strftime('%Y-%m-%d %H:%M:%S %Z')).color(:dark, :white),
55
- hl(event['node']).color( :bold, :cyan )
58
+ hl.dark.white( Time.now.strftime('%Y-%m-%d %H:%M:%S %Z') ),
59
+ hl.bold.cyan( event['node'] )
56
60
  ]
57
61
  when 'sys.node_removed'
58
62
  prompt.say "[%s] »Node removed« %s\n" % [
59
- hl(Time.now.strftime('%Y-%m-%d %H:%M:%S %Z')).color(:dark, :white),
60
- hl(event['node']).color( :dark, :cyan )
63
+ hl.dark.white( Time.now.strftime('%Y-%m-%d %H:%M:%S %Z') ),
64
+ hl.dark.cyan( event['node'] )
61
65
  ]
62
66
  else
63
67
  prompt.say "[%s] %s\n" % [
64
- hl(Time.now.strftime('%Y-%m-%d %H:%M:%S %Z')).color(:dark, :white),
65
- hl(dump_event( event )).color( :dark, :white )
68
+ hl.dark.white( Time.now.strftime('%Y-%m-%d %H:%M:%S %Z') ),
69
+ hl.dark.white( dump_event( event ) )
66
70
  ]
67
71
  end
68
72
  end
@@ -97,42 +101,38 @@ module Arborist::CLI::Watch
97
101
  when 'node.update'
98
102
  type, status, errors = event['data'].values_at( *%w'type status errors' )
99
103
  return "%s updated: %s is %s%s" % [
100
- hl( id ).color( :cyan ),
104
+ hl.cyan( id ),
101
105
  type,
102
- hl( status ).color( status.to_sym ),
106
+ hl.decorate( status, status.to_sym ),
103
107
  errors ? " (#{errors})" : ''
104
108
  ]
105
109
  when 'node.delta'
106
110
  pairs = diff_pairs( event['data'] )
107
- return "%s delta, changes: %s" % [ hl( id ).color( :cyan ), pairs ]
111
+ return "%s delta, changes: %s" % [ hl.cyan( id ), pairs ]
108
112
  else
109
- return "%s event: %p" % [ hl(event_type).color(:dark, :white), event ]
113
+ return "%s event: %p" % [ hl.dark.white( event_type ), event ]
110
114
  end
111
115
  end
112
116
 
113
117
 
114
118
  ### Return a string showing the differences in a delta event's change +data+.
115
119
  def diff_pairs( data )
116
- return data.collect do |key, pairs|
120
+ diff = data.collect do |key, pairs|
117
121
  if pairs.is_a?( Hash )
118
122
  diff_pairs( pairs )
119
123
  else
120
124
  val1, val2 = *pairs
121
125
  "%s: %s -> %s" % [
122
- hl( key ).color( :dark, :white ),
123
- hl( val1 ).color( :yellow ),
124
- hl( val2 ).color( :bold, :yellow )
126
+ hl.dark.white( key ),
127
+ hl.yellow( val1 ),
128
+ hl.bold.yellow( val2 )
125
129
  ]
126
130
  end
127
- end.join( hl(", ").color(:dark, :white) )
128
- end
129
-
131
+ end
130
132
 
131
- ### Return a heartbeat string for the current time.
132
- def heartbeat
133
- idx = (Time.now.to_i % HEARTBEAT_CHARACTERS.length) - 1
134
- return " " + HEARTBEAT_CHARACTERS[ idx ] + "\x08\x08"
133
+ return hl.dark.white( diff.join(', ') )
135
134
  end
136
135
 
136
+
137
137
  end # module Arborist::CLI::Watch
138
138