serfx 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +0 -1
- data/lib/serfx/commands.rb +31 -0
- data/lib/serfx/connection.rb +5 -1
- data/lib/serfx/utils/async_job.rb +171 -0
- data/lib/serfx/utils/handler.rb +91 -0
- data/lib/serfx/version.rb +1 -1
- data/spec/data/config.json +2 -1
- data/spec/data/handler.rb +11 -0
- data/spec/data/long_handler.rb +24 -0
- data/spec/serfx/client_spec.rb +14 -1
- data/spec/serfx/handler_spec.rb +22 -0
- data/spec/spec_helper.rb +1 -3
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fc4921d5134cf839d21cf4ad57a23cb55147359
|
4
|
+
data.tar.gz: cb1047f5ff43db315934db351ee08aa7b1518820
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4146f8211e7c20e55a1f12ca2ada7a94b1a615221ae7e83a4563c8c380ef2c54002293ecb1993df2b5064512a2ba1ba3adf403f25e5594f00525e114aebe4fd
|
7
|
+
data.tar.gz: e123d303f6af8433a93f8c65ced93d5c5241c5958edd1046fec63fbda950f4c383683241ade956ad1f3fb1eae69a98866b8e720096f966f45fabc9530e19c5d7
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/lib/serfx/commands.rb
CHANGED
@@ -167,5 +167,36 @@ module Serfx
|
|
167
167
|
def respond(id, payload)
|
168
168
|
request(:respond, 'ID' => id, 'Payload' => payload)
|
169
169
|
end
|
170
|
+
|
171
|
+
# install a new encryption key onto the cluster's keyring
|
172
|
+
#
|
173
|
+
# @param key [String] 16 bytes of base64-encoded data.
|
174
|
+
# @return [Response]
|
175
|
+
def install_key(key)
|
176
|
+
request(:install_key, 'Key' => key)
|
177
|
+
end
|
178
|
+
|
179
|
+
# change the primary key, which is used to encrypt messages
|
180
|
+
#
|
181
|
+
# @param key [String] 16 bytes of base64-encoded data.
|
182
|
+
# @return [Response]
|
183
|
+
def use_key(key)
|
184
|
+
request(:use_key, 'Key' => key)
|
185
|
+
end
|
186
|
+
|
187
|
+
# remove a key from the cluster's keyring
|
188
|
+
#
|
189
|
+
# @param key [String] 16 bytes of base64-encoded data.
|
190
|
+
# @return [Response]
|
191
|
+
def remove_key(key)
|
192
|
+
request(:remove_key, 'Key' => key)
|
193
|
+
end
|
194
|
+
|
195
|
+
# return a list of all encryption keys currently in use on the cluster
|
196
|
+
#
|
197
|
+
# @return [Response]
|
198
|
+
def list_keys
|
199
|
+
request(:list_keys)
|
200
|
+
end
|
170
201
|
end
|
171
202
|
end
|
data/lib/serfx/connection.rb
CHANGED
@@ -27,7 +27,11 @@ module Serfx
|
|
27
27
|
stop: [:header],
|
28
28
|
leave: [:header],
|
29
29
|
query: [:header],
|
30
|
-
respond: [:header]
|
30
|
+
respond: [:header],
|
31
|
+
install_key: [:header, :body],
|
32
|
+
use_key: [:header, :body],
|
33
|
+
remove_key: [:header, :body],
|
34
|
+
list_keys: [:header, :body]
|
31
35
|
}
|
32
36
|
|
33
37
|
include Serfx::Commands
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
module Serfx
|
5
|
+
module Utils
|
6
|
+
|
7
|
+
# serf event handler invocations are blocking calls. i.e. serf
|
8
|
+
# will not process any other event when a handler invocation is
|
9
|
+
# in progress. due to this limitations long running tasks can not be
|
10
|
+
# orchestrated or invoked as serf handler directly.
|
11
|
+
# [AsynchJob] address this by spawning the task as a background job,
|
12
|
+
# allowing the handler code to return immediately. It does double fork
|
13
|
+
# where the first child process is detached (attached to init as parent
|
14
|
+
# process) and spawn the second child process with the target,
|
15
|
+
# long running task. This allows the parent process to wait and reap the
|
16
|
+
# output of target task and save it in disk so that it can be exposed
|
17
|
+
# via other serf events
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# require 'serfx/utils/async_job'
|
21
|
+
# require 'serfx/utils/handler'
|
22
|
+
#
|
23
|
+
# job = Serfx::Utils::AsyncJob.new(
|
24
|
+
# name: "bash_test"
|
25
|
+
# command: "bash -c 'for i in `seq 1 3`; do echo $i; sleep 1; done'",
|
26
|
+
# state: '/opt/serf/states/long_task'
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# on :query, 'task_fire' do |event|
|
30
|
+
# puts job.run
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# on :query, 'task_check' do |event|
|
34
|
+
# puts job.state_info.inspect
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# on :query, 'task_kill' do |event|
|
38
|
+
# puts job.kill
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# on :query, 'task_reap' do |event|
|
42
|
+
# puts job.reap
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# run
|
46
|
+
class AsyncJob
|
47
|
+
|
48
|
+
attr_reader :command, :state_file, :stdout_file, :stderr_file
|
49
|
+
|
50
|
+
# @param opts [Hash] specify the job details
|
51
|
+
# @option opts [Symbol] :state file path which will be used to store
|
52
|
+
# task state locally
|
53
|
+
# @option opts [Symbol] :command actual command which will be invoked
|
54
|
+
# in the background
|
55
|
+
# @option opts [Symbol] :stdout standard output file for the task
|
56
|
+
# @option opts [Symbol] :stderr standard error file for the task
|
57
|
+
def initialize(opts = {})
|
58
|
+
@state_file = opts[:state] || fail(ArgumentError, 'Specify state file')
|
59
|
+
@command = opts[:command]
|
60
|
+
@stdout_file = opts[:stdout] || File::NULL
|
61
|
+
@stderr_file = opts[:stderr] || File::NULL
|
62
|
+
end
|
63
|
+
|
64
|
+
# kill an already running task
|
65
|
+
#
|
66
|
+
# @param sig [String] kill signal that will sent to the backgroun process
|
67
|
+
# @return [TrueClass,FalseClass] true on success, false on failure
|
68
|
+
def kill(sig = 'KILL')
|
69
|
+
if running?
|
70
|
+
begin
|
71
|
+
Process.kill(sig, state_info['pid'].to_i)
|
72
|
+
'success'
|
73
|
+
rescue Exception => e
|
74
|
+
'failed'
|
75
|
+
end
|
76
|
+
else
|
77
|
+
'failed'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# obtain current state information about the task
|
82
|
+
#
|
83
|
+
# @return [Hash]
|
84
|
+
def state_info
|
85
|
+
if exists?
|
86
|
+
JSON.parse(File.read(state_file))
|
87
|
+
else
|
88
|
+
{'status' => 'absent' }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# delete the statefile of a finished task
|
93
|
+
#
|
94
|
+
# @return [String] 'success' if the task is reaped, 'failed' otherwise
|
95
|
+
def reap
|
96
|
+
if state_info['status'] == 'finished'
|
97
|
+
File.unlink(state_file)
|
98
|
+
'success'
|
99
|
+
else
|
100
|
+
'failed'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# start a background daemon and spawn another process to run specified
|
106
|
+
# command. writes back state information in the state file
|
107
|
+
# after spawning daemon process (state=invoking), after spawning the
|
108
|
+
# child process (state=running) and after reaping the child process
|
109
|
+
# (sate=finished).
|
110
|
+
#
|
111
|
+
# @return [String] 'success' if task is started
|
112
|
+
def start
|
113
|
+
if exists? or command.nil?
|
114
|
+
return 'failed'
|
115
|
+
end
|
116
|
+
pid = fork do
|
117
|
+
Process.daemon
|
118
|
+
state = {
|
119
|
+
ppid: Process.pid,
|
120
|
+
status: 'invoking',
|
121
|
+
pid: -1,
|
122
|
+
time: Time.now.to_i
|
123
|
+
}
|
124
|
+
write_state(state)
|
125
|
+
begin
|
126
|
+
child_pid = Process.spawn(command, out: stdout_file, err: stderr_file)
|
127
|
+
state[:pid] = child_pid
|
128
|
+
state[:status] = 'running'
|
129
|
+
write_state(state)
|
130
|
+
_ , status = Process.wait2(child_pid)
|
131
|
+
state[:exitstatus] = status.exitstatus
|
132
|
+
state[:status] = 'finished'
|
133
|
+
rescue Errno::ENOENT => e
|
134
|
+
state[:error] = e.class.name
|
135
|
+
state[:status] = 'failed'
|
136
|
+
end
|
137
|
+
write_state(state)
|
138
|
+
exit 0
|
139
|
+
end
|
140
|
+
Process.detach(pid)
|
141
|
+
'success'
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# check if a task already running
|
147
|
+
#
|
148
|
+
# @return [TrueClass, FalseClass] true if the task running, else false
|
149
|
+
def running?
|
150
|
+
if exists?
|
151
|
+
File.exist?(File.join('/proc', state_info['pid'].to_s))
|
152
|
+
else
|
153
|
+
false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# check if a task already exist
|
158
|
+
#
|
159
|
+
# @return [TrueClass, FalseClass] true if the task exists, else false
|
160
|
+
def exists?
|
161
|
+
File.exists?(state_file)
|
162
|
+
end
|
163
|
+
|
164
|
+
# writes a hash as json in the state_file
|
165
|
+
# @param [Hash] state represented as a hash, to be written
|
166
|
+
def write_state(state)
|
167
|
+
File.open(state_file, 'w'){|f| f.write(JSON.generate(state))}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Serfx
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# helper module to for serf custom handlers
|
5
|
+
#
|
6
|
+
# serf agents can be configured to invoke an executable
|
7
|
+
# script when an user event is received.
|
8
|
+
#
|
9
|
+
# [Serfx::Utils::Handler] module provides a set of helper methods to ease
|
10
|
+
# writing ruby based serf event handlers
|
11
|
+
#
|
12
|
+
# For example, following script will respond to any qury event named
|
13
|
+
# 'upcase' and return the uppercase version of the original query event's
|
14
|
+
# payload
|
15
|
+
# @example
|
16
|
+
# require 'serfx/utils/handler'
|
17
|
+
# include Serfx::Utils::Handler
|
18
|
+
# on :query, 'upcase' do |event|
|
19
|
+
# unless event.payload.nil?
|
20
|
+
# STDOUT.write(event.payload.upcase)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# run
|
24
|
+
module Handler
|
25
|
+
|
26
|
+
# when serf agent invokes a handler it passes the event payload
|
27
|
+
# through STDIN. while event metadata such as event type, name etc
|
28
|
+
# is passed as a set of environment variables.
|
29
|
+
# [SerfEvent] encapsulates such event.
|
30
|
+
class SerfEvent
|
31
|
+
|
32
|
+
attr_reader :environment, :payload, :type, :name
|
33
|
+
|
34
|
+
# @param env [Hash] environment
|
35
|
+
# @param stdin [IO] stadard input stream for the event
|
36
|
+
def initialize(env = ENV, stdin = STDIN)
|
37
|
+
@environment ={}
|
38
|
+
@payload = nil
|
39
|
+
@name = nil
|
40
|
+
env.keys.select{|k|k=~/^SERF/}.each do | k|
|
41
|
+
@environment[k] = env[k].strip
|
42
|
+
end
|
43
|
+
@type = @environment['SERF_EVENT']
|
44
|
+
case @type
|
45
|
+
when 'query'
|
46
|
+
@name = @environment['SERF_QUERY_NAME']
|
47
|
+
begin
|
48
|
+
@payload = stdin.read_nonblock(4096).strip
|
49
|
+
rescue Errno::EAGAIN => e
|
50
|
+
end
|
51
|
+
when 'user'
|
52
|
+
@name = @environment['SERF_USER_EVENT']
|
53
|
+
begin
|
54
|
+
@payload = stdin.read_nonblock(4096).strip
|
55
|
+
rescue Errno::EAGAIN => e
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# register a callback against an event
|
62
|
+
#
|
63
|
+
# @param type [Symbol] event type for which this handler will be invoked
|
64
|
+
# @param name [String, Regex] match against name of the event
|
65
|
+
def on(type, name = nil, &block)
|
66
|
+
callbacks[type] << SerfCallback.new(name, block)
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# execute callbacks registerd using `on`
|
71
|
+
def run
|
72
|
+
event = SerfEvent.new
|
73
|
+
callbacks[event.type.downcase.to_sym].each do |cbk|
|
74
|
+
if cbk.name
|
75
|
+
cbk.block.call(event) if (event.name === cbk.name)
|
76
|
+
else
|
77
|
+
cbk.block.call(event)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
SerfCallback = Struct.new(:name, :block)
|
85
|
+
|
86
|
+
def callbacks
|
87
|
+
@_callbacks ||= Hash.new{|h,k| h[k] = []}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/serfx/version.rb
CHANGED
data/spec/data/config.json
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'serfx/utils/async_job'
|
2
|
+
require 'serfx/utils/handler'
|
3
|
+
|
4
|
+
include Serfx::Utils::Handler
|
5
|
+
|
6
|
+
command = "bash -c 'for i in `seq 1 300`; do echo $i; sleep 1; done'"
|
7
|
+
job = Serfx::Utils::AsyncJob.new(command: command, state: '/tmp/long_task')
|
8
|
+
|
9
|
+
on :query, 'task' do |event|
|
10
|
+
case event.payload
|
11
|
+
when 'start'
|
12
|
+
puts job.start
|
13
|
+
when 'check'
|
14
|
+
puts job.state_info.inspect
|
15
|
+
when 'reap'
|
16
|
+
puts job.reap
|
17
|
+
when 'kill'
|
18
|
+
puts job.kill
|
19
|
+
else
|
20
|
+
puts "unknown: #{event.payload}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
run
|
data/spec/serfx/client_spec.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
#
|
3
3
|
require 'spec_helper'
|
4
4
|
require 'timeout'
|
5
|
-
require 'thread'
|
6
5
|
|
7
6
|
describe Serfx do
|
8
7
|
|
@@ -129,4 +128,18 @@ describe Serfx do
|
|
129
128
|
t.kill
|
130
129
|
end
|
131
130
|
end
|
131
|
+
|
132
|
+
it '#list, install, use and remove keys' do
|
133
|
+
keys = @conn.list_keys.body['Keys'].keys
|
134
|
+
expect(keys).to include('QHOYjmYlxSCBhdfiolhtDQ==')
|
135
|
+
@conn.install_key('Ih6cZqutM33tMdoFo1iNyw==')
|
136
|
+
keys = @conn.list_keys.body['Keys'].keys
|
137
|
+
expect(keys).to include('Ih6cZqutM33tMdoFo1iNyw==')
|
138
|
+
@conn.use_key('Ih6cZqutM33tMdoFo1iNyw==')
|
139
|
+
new_keys = @conn.list_keys.body['Keys'].keys
|
140
|
+
expect(new_keys.first).to eq('Ih6cZqutM33tMdoFo1iNyw==')
|
141
|
+
@conn.remove_key('QHOYjmYlxSCBhdfiolhtDQ==')
|
142
|
+
final_keys = @conn.list_keys.body['Keys'].keys
|
143
|
+
expect(final_keys.first).to_not include('QHOYjmYlxSCBhdfiolhtDQ==')
|
144
|
+
end
|
132
145
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'serfx/utils/handler'
|
3
|
+
|
4
|
+
describe Serfx::Utils::Handler do
|
5
|
+
it 'should invoke all the call backs' do
|
6
|
+
class CustomHandler
|
7
|
+
extend Serfx::Utils::Handler
|
8
|
+
@@state = {}
|
9
|
+
def self.state
|
10
|
+
@@state
|
11
|
+
end
|
12
|
+
on :query, 'foo' do |event|
|
13
|
+
@@state[:payload] = event.payload
|
14
|
+
end
|
15
|
+
end
|
16
|
+
ENV['SERF_EVENT'] = 'query'
|
17
|
+
ENV['SERF_QUERY_NAME'] = 'foo'
|
18
|
+
STDIN.should_receive(:read_nonblock).and_return('yeah')
|
19
|
+
CustomHandler.run
|
20
|
+
expect(CustomHandler.state[:payload]).to eq('yeah')
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,8 +6,6 @@ require 'rspec'
|
|
6
6
|
require 'serfx'
|
7
7
|
require 'singleton'
|
8
8
|
require 'tmpdir'
|
9
|
-
require 'codeclimate-test-reporter'
|
10
|
-
CodeClimate::TestReporter.start if ENV['CODE_COVERAGE']
|
11
9
|
|
12
10
|
module Serfx
|
13
11
|
# adds helper method for unit testing
|
@@ -40,7 +38,7 @@ module Serfx
|
|
40
38
|
|
41
39
|
def daemonize(dir, join = false)
|
42
40
|
command = serf_command(join)
|
43
|
-
pid = spawn(command, out:
|
41
|
+
pid = spawn(command, out: "#{dir}/stdout", chdir: dir)
|
44
42
|
Process.detach(pid)
|
45
43
|
pid
|
46
44
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serfx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rnjib Dey
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|
@@ -161,10 +161,15 @@ files:
|
|
161
161
|
- lib/serfx/log.rb
|
162
162
|
- lib/serfx/response.rb
|
163
163
|
- lib/serfx/utils.rb
|
164
|
+
- lib/serfx/utils/async_job.rb
|
165
|
+
- lib/serfx/utils/handler.rb
|
164
166
|
- lib/serfx/version.rb
|
165
167
|
- serfx.gemspec
|
166
168
|
- spec/data/config.json
|
169
|
+
- spec/data/handler.rb
|
170
|
+
- spec/data/long_handler.rb
|
167
171
|
- spec/serfx/client_spec.rb
|
172
|
+
- spec/serfx/handler_spec.rb
|
168
173
|
- spec/spec_helper.rb
|
169
174
|
homepage: https://github.com/ranjib/serfx
|
170
175
|
licenses:
|
@@ -192,6 +197,9 @@ specification_version: 4
|
|
192
197
|
summary: A barebone ruby client for serf
|
193
198
|
test_files:
|
194
199
|
- spec/data/config.json
|
200
|
+
- spec/data/handler.rb
|
201
|
+
- spec/data/long_handler.rb
|
195
202
|
- spec/serfx/client_spec.rb
|
203
|
+
- spec/serfx/handler_spec.rb
|
196
204
|
- spec/spec_helper.rb
|
197
205
|
has_rdoc:
|