flapjack 1.0.0rc1 → 1.0.0rc2
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 +4 -4
- data/.gitignore +8 -0
- data/CHANGELOG.md +15 -0
- data/README.md +4 -0
- data/bin/flapjack +15 -2
- data/build.sh +22 -0
- data/dist/etc/init.d/flapjack +1 -1
- data/dist/etc/init.d/flapjack-nagios-receiver +3 -3
- data/dist/etc/init.d/flapper +3 -3
- data/etc/flapjack_config.yaml.example +35 -17
- data/lib/flapjack/cli/receiver.rb +25 -1
- data/lib/flapjack/cli/simulate.rb +16 -9
- data/lib/flapjack/data/entity.rb +17 -21
- data/lib/flapjack/data/entity_check.rb +10 -0
- data/lib/flapjack/gateways/jabber.rb +36 -12
- data/lib/flapjack/gateways/jsonapi/check_methods.rb +32 -0
- data/lib/flapjack/gateways/jsonapi/report_methods.rb +1 -5
- data/lib/flapjack/gateways/web/views/check.html.erb +1 -1
- data/lib/flapjack/gateways/web/views/checks.html.erb +2 -2
- data/lib/flapjack/gateways/web/views/contact.html.erb +2 -2
- data/lib/flapjack/gateways/web/views/entity.html.erb +1 -1
- data/lib/flapjack/patches.rb +70 -0
- data/lib/flapjack/version.rb +1 -1
- data/libexec/httpbroker.go +161 -0
- data/libexec/oneoff.go +75 -0
- data/spec/lib/flapjack/data/entity_spec.rb +1 -1
- data/spec/lib/flapjack/gateways/jabber_spec.rb +15 -17
- data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +57 -0
- data/src/flapjack/event.go +25 -0
- data/src/flapjack/event_test.go +26 -0
- data/src/flapjack/transport.go +49 -0
- data/src/flapjack/transport_test.go +15 -0
- metadata +9 -2
@@ -50,9 +50,17 @@ module Flapjack
|
|
50
50
|
@redis = Flapjack::RedisPool.new(:config => @redis_config, :size => 2)
|
51
51
|
|
52
52
|
@logger = opts[:logger]
|
53
|
+
@logger.debug("Jabber Initializing")
|
53
54
|
|
54
55
|
@buffer = []
|
55
56
|
@hostname = Socket.gethostname
|
57
|
+
|
58
|
+
# FIXME: i suspect the following should be in #setup so a config reload updates @identifiers
|
59
|
+
# I moved it here so the rspec passes :-/
|
60
|
+
@alias = @config['alias'] || 'flapjack'
|
61
|
+
@identifiers = ((@config['identifiers'] || []) + [@alias]).uniq
|
62
|
+
@logger.debug("I will respond to the following identifiers: #{@identifiers.join(', ')}")
|
63
|
+
|
56
64
|
super()
|
57
65
|
end
|
58
66
|
|
@@ -65,7 +73,9 @@ module Flapjack
|
|
65
73
|
end
|
66
74
|
|
67
75
|
def setup
|
68
|
-
|
76
|
+
jid = @config['jabberid'] || 'flapjack'
|
77
|
+
jid += '/' + @hostname unless jid.include?('/')
|
78
|
+
@flapjack_jid = Blather::JID.new(jid)
|
69
79
|
|
70
80
|
super(@flapjack_jid, @config['password'], @config['server'], @config['port'].to_i)
|
71
81
|
|
@@ -80,13 +90,19 @@ module Flapjack
|
|
80
90
|
end
|
81
91
|
end
|
82
92
|
|
83
|
-
|
93
|
+
body_matchers = @identifiers.inject([]) do |memo, identifier|
|
94
|
+
@logger.debug("identifier: #{identifier}, memo: #{memo}")
|
95
|
+
memo << {:body => /^#{identifier}[:\s]/}
|
96
|
+
memo
|
97
|
+
end
|
98
|
+
@logger.debug("body_matchers: #{body_matchers}")
|
99
|
+
register_handler :message, :groupchat?, body_matchers do |stanza|
|
84
100
|
EventMachine::Synchrony.next_tick do
|
85
101
|
on_groupchat(stanza)
|
86
102
|
end
|
87
103
|
end
|
88
104
|
|
89
|
-
register_handler :message, :chat
|
105
|
+
register_handler :message, :chat?, :body do |stanza|
|
90
106
|
EventMachine::Synchrony.next_tick do
|
91
107
|
on_chat(stanza)
|
92
108
|
end
|
@@ -111,11 +127,11 @@ module Flapjack
|
|
111
127
|
@logger.info("Joining room #{room}")
|
112
128
|
presence = Blather::Stanza::Presence.new
|
113
129
|
presence.from = @flapjack_jid
|
114
|
-
presence.to = Blather::JID.new("#{room}/#{@
|
130
|
+
presence.to = Blather::JID.new("#{room}/#{@alias}")
|
115
131
|
presence << "<x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='0'></x>"
|
116
132
|
EventMachine::Synchrony.next_tick do
|
117
133
|
write presence
|
118
|
-
say(room, "flapjack jabber gateway started at #{Time.now}, hello!", :groupchat)
|
134
|
+
say(room, "flapjack jabber gateway started at #{Time.now}, hello! Try typing 'help'.", :groupchat)
|
119
135
|
end
|
120
136
|
end
|
121
137
|
end
|
@@ -160,7 +176,7 @@ module Flapjack
|
|
160
176
|
out
|
161
177
|
end
|
162
178
|
|
163
|
-
def interpreter(command_raw,from)
|
179
|
+
def interpreter(command_raw, from)
|
164
180
|
msg = nil
|
165
181
|
action = nil
|
166
182
|
entity_check = nil
|
@@ -239,7 +255,8 @@ module Flapjack
|
|
239
255
|
t = Process.times
|
240
256
|
fqdn = `/bin/hostname -f`.chomp
|
241
257
|
pid = Process.pid
|
242
|
-
msg = "Flapjack #{Flapjack::VERSION} process #{pid} on #{fqdn}
|
258
|
+
msg = "Flapjack #{Flapjack::VERSION} process #{pid} on #{fqdn}\n" +
|
259
|
+
"Identifiers: #{@identifiers.join(', ')}\n" +
|
243
260
|
"Boot time: #{@boot_time}\n" +
|
244
261
|
"User CPU Time: #{t.utime}\n" +
|
245
262
|
"System CPU Time: #{t.stime}\n" +
|
@@ -537,18 +554,23 @@ module Flapjack
|
|
537
554
|
return if @should_quit
|
538
555
|
@logger.debug("groupchat message received: #{stanza.inspect}")
|
539
556
|
|
540
|
-
|
541
|
-
|
557
|
+
the_command = nil
|
558
|
+
@identifiers.each do |identifier|
|
559
|
+
if stanza.body =~ /^#{identifier}:?\s*(.*)/m
|
560
|
+
the_command = $1
|
561
|
+
@logger.debug("matched identifier: #{identifier}, command: #{the_command.inspect}")
|
562
|
+
break
|
563
|
+
end
|
542
564
|
end
|
543
565
|
|
544
566
|
from = stanza.from
|
545
567
|
|
546
568
|
begin
|
547
|
-
results = interpreter(
|
569
|
+
results = interpreter(the_command, from.to_s)
|
548
570
|
msg = results[:msg]
|
549
571
|
action = results[:action]
|
550
572
|
rescue => e
|
551
|
-
@logger.
|
573
|
+
@logger.debug("Exception when interpreting command '#{the_command}' - #{e.class}, #{e.message}")
|
552
574
|
msg = "Oops, something went wrong processing that command (#{e.class}, #{e.message})"
|
553
575
|
end
|
554
576
|
|
@@ -571,8 +593,10 @@ module Flapjack
|
|
571
593
|
command = stanza.body
|
572
594
|
end
|
573
595
|
|
596
|
+
from = stanza.from
|
597
|
+
|
574
598
|
begin
|
575
|
-
results = interpreter(command)
|
599
|
+
results = interpreter(command, from.resource.to_s)
|
576
600
|
msg = results[:msg]
|
577
601
|
action = results[:action]
|
578
602
|
rescue => e
|
@@ -31,6 +31,38 @@ module Flapjack
|
|
31
31
|
app.helpers Flapjack::Gateways::JSONAPI::Helpers
|
32
32
|
app.helpers Flapjack::Gateways::JSONAPI::CheckMethods::Helpers
|
33
33
|
|
34
|
+
app.get %r{^/checks(?:/)?(.+)?$} do
|
35
|
+
requested_checks = if params[:captures] && params[:captures][0]
|
36
|
+
params[:captures][0].split(',').uniq
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
checks = if requested_checks
|
42
|
+
requested_checks.collect do|req_check|
|
43
|
+
Flapjack::Data::EntityCheck.for_event_id(req_check, :logger => logger, :redis => redis)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
Flapjack::Data::EntityCheck.find_all(:redis => redis)
|
47
|
+
end
|
48
|
+
checks.compact!
|
49
|
+
|
50
|
+
if requested_checks && checks.empty?
|
51
|
+
raise Flapjack::Gateways::JSONAPI::ChecksNotFound.new(requested_checks)
|
52
|
+
end
|
53
|
+
|
54
|
+
linked_entity_ids = checks.empty? ? [] : checks.inject({}) do |memo, check|
|
55
|
+
memo[check.key] = check.entity.id
|
56
|
+
memo
|
57
|
+
end
|
58
|
+
|
59
|
+
checks_json = checks.collect {|check|
|
60
|
+
check.to_jsonapi(:entity_ids => linked_entity_ids[check.key])
|
61
|
+
}.join(",")
|
62
|
+
|
63
|
+
'{"checks":[' + checks_json + ']}'
|
64
|
+
end
|
65
|
+
|
34
66
|
app.patch %r{^/checks/(.+)$} do
|
35
67
|
checks_for_check_names(params[:captures][0].split(',')).each do |check|
|
36
68
|
apply_json_patch('checks') do |op, property, linked, value|
|
@@ -27,11 +27,7 @@ module Flapjack
|
|
27
27
|
end
|
28
28
|
|
29
29
|
checks = if event_ids.nil?
|
30
|
-
Flapjack::Data::
|
31
|
-
entity.check_list.collect {|check_name|
|
32
|
-
find_entity_check(entity, check_name)
|
33
|
-
}
|
34
|
-
}.flatten(2)
|
30
|
+
Flapjack::Data::EntityCheck.find_all(:redis => redis)
|
35
31
|
elsif !event_ids.empty?
|
36
32
|
event_ids.collect {|event_id| find_entity_check_by_name(*event_id.split(':', 2)) }
|
37
33
|
else
|
@@ -4,7 +4,7 @@
|
|
4
4
|
current_time = Time.now
|
5
5
|
%>
|
6
6
|
<div class="page-header">
|
7
|
-
<% entity_link =
|
7
|
+
<% entity_link = @base_url + "entity/" << u(@entity) %>
|
8
8
|
<h2><%= h @check %> on <a href="<%= entity_link %>" title="entity summary"><%= h @entity %></a></h2>
|
9
9
|
</div>
|
10
10
|
|
@@ -21,7 +21,7 @@
|
|
21
21
|
<tbody>
|
22
22
|
<% @entities_sorted.each do |entity| %>
|
23
23
|
<% row_entity = nil %>
|
24
|
-
<% entity_link =
|
24
|
+
<% entity_link = @base_url + "entity/" << u(entity) %>
|
25
25
|
<% @states[entity].each do |check, status, summary, changed, updated, in_unscheduled_outage, in_scheduled_outage, notified| %>
|
26
26
|
<%
|
27
27
|
row_colour = case status
|
@@ -33,7 +33,7 @@
|
|
33
33
|
status
|
34
34
|
end
|
35
35
|
|
36
|
-
check_link =
|
36
|
+
check_link = @base_url + "check?entity=" << u(entity) << "&check=" << u(check)
|
37
37
|
%>
|
38
38
|
<tr class="<%= row_colour %>">
|
39
39
|
<% unless row_entity && entity == row_entity %>
|
@@ -81,7 +81,7 @@
|
|
81
81
|
<td>
|
82
82
|
<% checks.each do |entity_check| %>
|
83
83
|
<% entity, check = entity_check.split(':', 2) %>
|
84
|
-
<% check_link = "<a href=\"#{
|
84
|
+
<% check_link = "<a href=\"#{@base_url}check?entity=#{u(entity)}&check=#{u(check)}\" title=\"check status\">" +
|
85
85
|
h(check) + "</a>"%>
|
86
86
|
<a href="<%= @base_url %>entity/<%= u(entity) %>" title="entity status"><%= h entity %></a> ::
|
87
87
|
<%= check_link %> <br />
|
@@ -147,7 +147,7 @@
|
|
147
147
|
<td><a href="<%= @base_url %>entity/<%= u(entity.name) %>" title="entity status"><%= h entity.name %></a></td>
|
148
148
|
<td>
|
149
149
|
<% checks.each do |check| %>
|
150
|
-
<%= "<a href=\"#{
|
150
|
+
<%= "<a href=\"#{@base_url}check?entity=#{u(entity.name)}&check=#{u(check)}\" title=\"check status\">#{ h check }</a>" %>
|
151
151
|
<% end %>
|
152
152
|
</td>
|
153
153
|
</tr>
|
data/lib/flapjack/patches.rb
CHANGED
@@ -116,3 +116,73 @@ class Redis
|
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
119
|
+
|
120
|
+
module GLI
|
121
|
+
class Command
|
122
|
+
attr_accessor :passthrough
|
123
|
+
def _action
|
124
|
+
@action
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class GLIOptionParser
|
129
|
+
class NormalCommandOptionParser
|
130
|
+
def parse!(parsing_result)
|
131
|
+
parsed_command_options = {}
|
132
|
+
command = parsing_result.command
|
133
|
+
arguments = nil
|
134
|
+
|
135
|
+
loop do
|
136
|
+
command._action.call if command.passthrough
|
137
|
+
|
138
|
+
option_parser_factory = OptionParserFactory.for_command(command,@accepts)
|
139
|
+
option_block_parser = CommandOptionBlockParser.new(option_parser_factory, self.error_handler)
|
140
|
+
option_block_parser.command = command
|
141
|
+
arguments = parsing_result.arguments
|
142
|
+
|
143
|
+
arguments = option_block_parser.parse!(arguments)
|
144
|
+
|
145
|
+
parsed_command_options[command] = option_parser_factory.options_hash_with_defaults_set!
|
146
|
+
command_finder = CommandFinder.new(command.commands,command.get_default_command)
|
147
|
+
next_command_name = arguments.shift
|
148
|
+
|
149
|
+
verify_required_options!(command.flags,parsed_command_options[command])
|
150
|
+
|
151
|
+
begin
|
152
|
+
command = command_finder.find_command(next_command_name)
|
153
|
+
rescue AmbiguousCommand
|
154
|
+
arguments.unshift(next_command_name)
|
155
|
+
break
|
156
|
+
rescue UnknownCommand
|
157
|
+
arguments.unshift(next_command_name)
|
158
|
+
# Although command finder could certainy know if it should use
|
159
|
+
# the default command, it has no way to put the "unknown command"
|
160
|
+
# back into the argument stack. UGH.
|
161
|
+
unless command.get_default_command.nil?
|
162
|
+
command = command_finder.find_command(command.get_default_command)
|
163
|
+
end
|
164
|
+
break
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
parsed_command_options[command] ||= {}
|
169
|
+
command_options = parsed_command_options[command]
|
170
|
+
|
171
|
+
this_command = command.parent
|
172
|
+
child_command_options = command_options
|
173
|
+
|
174
|
+
while this_command.kind_of?(command.class)
|
175
|
+
this_command_options = parsed_command_options[this_command] || {}
|
176
|
+
child_command_options[GLI::Command::PARENT] = this_command_options
|
177
|
+
this_command = this_command.parent
|
178
|
+
child_command_options = this_command_options
|
179
|
+
end
|
180
|
+
|
181
|
+
parsing_result.command_options = command_options
|
182
|
+
parsing_result.command = command
|
183
|
+
parsing_result.arguments = Array(arguments.compact)
|
184
|
+
parsing_result
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/flapjack/version.rb
CHANGED
@@ -0,0 +1,161 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
import (
|
4
|
+
"fmt"
|
5
|
+
"log"
|
6
|
+
"os"
|
7
|
+
"io/ioutil"
|
8
|
+
"time"
|
9
|
+
"net/http"
|
10
|
+
"flapjack"
|
11
|
+
"encoding/json"
|
12
|
+
"gopkg.in/alecthomas/kingpin.v1"
|
13
|
+
"github.com/go-martini/martini"
|
14
|
+
)
|
15
|
+
|
16
|
+
// State is a basic representation of a Flapjack event, with some extra field.
|
17
|
+
// The extra fields handle state expiry.
|
18
|
+
// Find more at https://github.com/flapjack/flapjack/wiki/DATA_STRUCTURES
|
19
|
+
type State struct {
|
20
|
+
flapjack.Event
|
21
|
+
TTL int64 `json:"ttl"`
|
22
|
+
}
|
23
|
+
|
24
|
+
// handler caches
|
25
|
+
func CreateState(updates chan State, w http.ResponseWriter, r *http.Request) {
|
26
|
+
var state State
|
27
|
+
|
28
|
+
body, err := ioutil.ReadAll(r.Body)
|
29
|
+
if err != nil {
|
30
|
+
message := "Error: Couldn't read request body: %s\n"
|
31
|
+
log.Printf(message, err)
|
32
|
+
fmt.Fprintf(w, message, err)
|
33
|
+
return
|
34
|
+
}
|
35
|
+
|
36
|
+
err = json.Unmarshal(body, &state)
|
37
|
+
if err != nil {
|
38
|
+
message := "Error: Couldn't read request body: %s\n"
|
39
|
+
log.Println(message, err)
|
40
|
+
fmt.Fprintf(w, message, err)
|
41
|
+
return
|
42
|
+
}
|
43
|
+
|
44
|
+
// Populate a time if none has been set.
|
45
|
+
if state.Time == 0 {
|
46
|
+
state.Time = time.Now().Unix()
|
47
|
+
}
|
48
|
+
|
49
|
+
if len(state.Type) == 0 {
|
50
|
+
state.Type = "service"
|
51
|
+
}
|
52
|
+
|
53
|
+
if state.TTL == 0 {
|
54
|
+
state.TTL = 300
|
55
|
+
}
|
56
|
+
|
57
|
+
updates <- state
|
58
|
+
|
59
|
+
json, _ := json.Marshal(state)
|
60
|
+
message := "Caching state: %s\n"
|
61
|
+
log.Printf(message, json)
|
62
|
+
fmt.Fprintf(w, message, json)
|
63
|
+
}
|
64
|
+
|
65
|
+
// cacheState stores a cache of event state to be sent to Flapjack.
|
66
|
+
// The event state is queried later when submitting events periodically
|
67
|
+
// to Flapjack.
|
68
|
+
func cacheState(updates chan State, state map[string]State) {
|
69
|
+
for ns := range updates {
|
70
|
+
key := ns.Entity + ":" + ns.Check
|
71
|
+
state[key] = ns
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
// submitCachedState periodically samples the cached state, sends it to Flapjack.
|
76
|
+
func submitCachedState(states map[string]State, config Config) {
|
77
|
+
transport, err := flapjack.Dial(config.Server, config.Database)
|
78
|
+
if err != nil {
|
79
|
+
fmt.Printf("Error: %s\n", err)
|
80
|
+
os.Exit(1)
|
81
|
+
}
|
82
|
+
|
83
|
+
for {
|
84
|
+
log.Printf("Number of cached states: %d\n", len(states))
|
85
|
+
for id, state := range(states) {
|
86
|
+
now := time.Now().Unix()
|
87
|
+
event := flapjack.Event{
|
88
|
+
Entity: state.Entity,
|
89
|
+
Check: state.Check,
|
90
|
+
Type: state.Type,
|
91
|
+
State: state.State,
|
92
|
+
Summary: state.Summary,
|
93
|
+
Time: now,
|
94
|
+
}
|
95
|
+
|
96
|
+
// Stale state sends UNKNOWNs
|
97
|
+
elapsed := now - state.Time
|
98
|
+
if state.TTL >= 0 && elapsed > state.TTL {
|
99
|
+
log.Printf("State for %s is stale. Sending UNKNOWN.\n", id)
|
100
|
+
event.State = "UNKNOWN"
|
101
|
+
event.Summary = fmt.Sprintf("httpbroker: Cached state is stale (%ds old, should be < %ds)", elapsed, state.TTL)
|
102
|
+
}
|
103
|
+
if config.Debug {
|
104
|
+
log.Printf("Sending event data for %s\n", id)
|
105
|
+
}
|
106
|
+
transport.Send(event)
|
107
|
+
}
|
108
|
+
time.Sleep(config.Interval)
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
var (
|
113
|
+
port = kingpin.Flag("port", "Address to bind HTTP server (default 3090)").Default("3090").OverrideDefaultFromEnvar("PORT").String()
|
114
|
+
server = kingpin.Flag("server", "Redis server to connect to (default localhost:6380)").Default("localhost:6380").String()
|
115
|
+
database = kingpin.Flag("database", "Redis database to connect to (default 0)").Int() // .Default("13").Int()
|
116
|
+
interval = kingpin.Flag("interval", "How often to submit events (default 10s)").Default("10s").Duration()
|
117
|
+
debug = kingpin.Flag("debug", "Enable verbose output (default false)").Bool()
|
118
|
+
)
|
119
|
+
|
120
|
+
type Config struct {
|
121
|
+
Port string
|
122
|
+
Server string
|
123
|
+
Database int
|
124
|
+
Interval time.Duration
|
125
|
+
Debug bool
|
126
|
+
}
|
127
|
+
|
128
|
+
func main() {
|
129
|
+
kingpin.Version("0.0.1")
|
130
|
+
kingpin.Parse()
|
131
|
+
|
132
|
+
config := Config{
|
133
|
+
Server: *server,
|
134
|
+
Database: *database,
|
135
|
+
Interval: *interval,
|
136
|
+
Debug: *debug,
|
137
|
+
Port: ":" + *port,
|
138
|
+
}
|
139
|
+
if config.Debug {
|
140
|
+
log.Printf("Booting with config: %+v\n", config)
|
141
|
+
}
|
142
|
+
|
143
|
+
updates := make(chan State)
|
144
|
+
state := map[string]State{}
|
145
|
+
|
146
|
+
go cacheState(updates, state)
|
147
|
+
go submitCachedState(state, config)
|
148
|
+
|
149
|
+
m := martini.Classic()
|
150
|
+
m.Group("/state", func(r martini.Router) {
|
151
|
+
r.Post("", func(res http.ResponseWriter, req *http.Request) {
|
152
|
+
CreateState(updates, res, req)
|
153
|
+
})
|
154
|
+
r.Get("", func() []byte {
|
155
|
+
data, _ := json.Marshal(state)
|
156
|
+
return data
|
157
|
+
})
|
158
|
+
})
|
159
|
+
|
160
|
+
log.Fatal(http.ListenAndServe(config.Port, m))
|
161
|
+
}
|