serfx 0.0.1 → 0.0.2
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/.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:
|