serfx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3bb17715c2ea3ee4ecb6de764132ba68b69c1c8
4
+ data.tar.gz: 53c8a46a3907a5218a2a8fe9d7eb66807ae39bb2
5
+ SHA512:
6
+ metadata.gz: 5dcf2af95c0d178c401604c93954d10772ae769e068f64d1647721c6bda12714de7c39b2398a777396f1e30635563525f22ad27878349a1dcfd34fe0cc412f1b
7
+ data.tar.gz: 61466ce03edfecc3697c8650841695bd58d7c32f36d48f33884d7fe98a0d2326c78d6ab6b0a78f011f32bc86fc4c61cb6501dfe53b34b0d96a94d3848a4ac39f
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ serf_binaries/*
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ MethodLength:
2
+ Max: 15
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ before_install:
2
+ - bash download_serf
3
+ - bundle install --path .bundle
4
+ rvm:
5
+ - 1.9.2
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - 2.1.0
9
+ branches:
10
+ only:
11
+ - master
12
+ script: "CODE_COVERAGE=1 bundle exec rake spec"
13
+ addons:
14
+ code_climate:
15
+ repo_token: 894a7a896e5ac4a4e7e497051fbc68152f348989a5b930cc3a037c713cf63df0
data/.yardops ADDED
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ --title=""
3
+ --markup=markdown
4
+ --template-path=./yard
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+ group :development do
5
+ gem 'irbtools'
6
+ gem 'pry'
7
+ gem 'codeclimate-test-reporter', group: :test, require: nil
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ranjib Dey
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Serfx
2
+
3
+ A bare minimal ruby client for [Serf][serf-homepage].
4
+
5
+ ## Introduction
6
+
7
+ Serfx uses Serf's [RPC protocol][rpc-protocol] under the hood. Serf RPC protocol involves communication with a local or remote serf agent over tcp using [msgpack][msgpack] for serialization.
8
+ Serf's RPC protocol mandates `handshake` (followed by `auth` if applicable) to be the first command(s) in an RPC session. Serfx exposes all the low level RPC commands via the `Connection` class, as well a convenient `connect` method which creates a connection object and does the handshake, auth and connection closing for you.
9
+ If you are creating the `Serfx::Connection` directly, they you have to do the handshake (auth if applicable) and connection closing explicitly.
10
+ For example, the command `members` can be invoked as:
11
+
12
+ ```ruby
13
+ Serfx.connect do |conn|
14
+ conn.members
15
+ end
16
+ ```
17
+ Which is equivalent to
18
+
19
+ ```ruby
20
+ conn = Serfx::Connection.new
21
+ conn.handshake
22
+ conn.members
23
+ conn.close
24
+ ```
25
+
26
+ [serf-homepage]: http://www.serfdom.io
27
+ [rpc-protocol]: http://www.serfdom.io/docs/agent/rpc.html
28
+ [msgpack]: http://msgpack.org
29
+
30
+ ## Sending custom events
31
+ Serf allows creating user events. An user event must have a name and can have an optional payload.
32
+ Following will create an user event named `play` with payload `PinkFloyd`
33
+
34
+ ```ruby
35
+ conn.event('play', 'PinkFloyd')
36
+ ```
37
+
38
+ Serf agents can be configured to invoke arbtrary script as [event handlers][event-handlers]. When serf agents receive the `play` event thay'll invoke the corresponding handler(s) and pass the payload 'PinkFloyd' via standard input.
39
+ [event-handlers]: http://www.serfdom.io/docs/agent/event-handlers.html
40
+
41
+ ## Query and response
42
+
43
+ Serf queries are special events that can be fired against a set of node(s) and the target node(s) can respond to aginst the queried event. Since Serf's event pipeline is asyn, every query event is timeboxed (by default 15s) and only those responses that are received within the timeout period is yield-ed. Following is an example will fire a query event name 'foo' with 'bar' payload and print the incoming responses from any node within the default timeout(15s).
44
+
45
+ ```ruby
46
+ conn.query('foo', 'bar') do |response|
47
+ p response
48
+ end
49
+ ```
50
+
51
+ ## Specifying connection details
52
+ By default Serfx will try to connect to localhost at port 7373 (serf agent's default RPC port). Both `Serfx::Connection#new` as well as `Serfx.connect` accepts a hash specifying connection options i.e host, port, encryption, which can be used to specify non-default values.
53
+
54
+ ```ruby
55
+ Serfx.client(host: 'serf1.example.com', port: 7373, authkey: 'secret')
56
+ ```
57
+
58
+ ## API documentation
59
+
60
+ [Detailed api documentation][api-doc] is accessible via rubydoc.
61
+ [api-doc]: http://rubydoc.info/gems/serfx
62
+
63
+ ## Supported ruby versions
64
+
65
+ Serfx aims to support and is [tested against][serfx-travis] the following Ruby implementations:
66
+
67
+ * *Ruby 1.9.2*
68
+ * *Ruby 1.9.3*
69
+ * *Ruby 2.0.0*
70
+ * *Ruby 2.1.0*
71
+
72
+ [serfx-travis]: https://travis-ci.org/ranjib/serfx
73
+
74
+ ## License
75
+ Licensed under the Apache License, Version 2.0 (the "License");
76
+ you may not use this file except in compliance with the License.
77
+ You may obtain a copy of the License at
78
+
79
+ http://www.apache.org/licenses/LICENSE-2.0
80
+
81
+ Unless required by applicable law or agreed to in writing, software
82
+ distributed under the License is distributed on an "AS IS" BASIS,
83
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
84
+ See the License for the specific language governing permissions and
85
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+
6
+ RSpec::Core::RakeTask.new("spec")
7
+
8
+ Rubocop::RakeTask.new(:rubocop) do |task|
9
+ task.patterns = ['lib/**/*.rb']
10
+ end
11
+
12
+ YARD::Rake::YardocTask.new do |task|
13
+ task.files = ['README.md', 'lib/**/*.rb']
14
+ task.options = [
15
+ '--output-dir', 'doc/yard',
16
+ '--markup', 'markdown'
17
+ ]
18
+ end
data/download_serf ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ mkdir serf_binaries
3
+ cd serf_binaries
4
+ wget -c https://dl.bintray.com/mitchellh/serf/0.6.0_linux_amd64.zip
5
+ unzip 0.6.0_linux_amd64.zip
@@ -0,0 +1,171 @@
1
+ # encoding: UTF-8
2
+ #
3
+ require 'thread'
4
+
5
+ Thread.abort_on_exception = true
6
+
7
+ module Serfx
8
+ # Implements all of Serf's rpc commands using
9
+ # Serfx::Connection#request method
10
+ module Commands
11
+
12
+ # performs initial hanshake of an RPC session. Handshake has to be the
13
+ # first command to be invoked during an RPC session.
14
+ #
15
+ # @return [Response]
16
+ def handshake
17
+ tcp_send(:handshake, 'Version' => 1)
18
+ read_response(:handshake)
19
+ end
20
+
21
+ # authenticate against the serf agent. if RPC credentials are setup, then
22
+ # `auth` has to be second command, immediately after `handshake`.
23
+ #
24
+ # @return [Response]
25
+ def auth
26
+ tcp_send(:auth, 'AuthKey' => @authkey)
27
+ read_response(:auth)
28
+ end
29
+ # fires an user event
30
+ #
31
+ # @param name [String] a string representing name of the event
32
+ # @param payload [String] payload, default is nil
33
+ # @param coalesce [Boolena] whether serf should coalesce events within
34
+ # same name during similar time frame
35
+ # @return [Response]
36
+ def event(name, payload = nil, coalesce = true)
37
+ event = {
38
+ 'Name' => name,
39
+ 'Coalesce' => coalesce
40
+ }
41
+ event['Payload'] = payload unless payload.nil?
42
+ request(:event, event)
43
+ end
44
+
45
+ # force a failed node to leave the cluster
46
+ #
47
+ # @param node [String] name of the failed node
48
+ # @return [Response]
49
+ def force_leave(node)
50
+ request(:force_leave, 'Node' => node)
51
+ end
52
+
53
+ # join an existing cluster.
54
+ #
55
+ # @param existing [Array] an array of existing serf agents
56
+ # @param replay [Boolean] Whether events should be replayed upon joining
57
+ # @return [Response]
58
+ def join(existing, replay = false)
59
+ request(:join, 'Existing' => existing, 'Replay' => replay)
60
+ end
61
+
62
+ # obtain the list of existing members
63
+ #
64
+ # @return [Response]
65
+ def members
66
+ request(:members)
67
+ end
68
+
69
+ # obatin the list of cluster members, filtered by tags.
70
+ #
71
+ # @param tags [Array] an array of tags for filter
72
+ # @param status [Boolean] filter members based on their satatus
73
+ # @param name [String] filter based on exact name or pattern.
74
+ # @return [Response]
75
+ def members_filtered(tags, status = 'alive', name = nil)
76
+ filter = {
77
+ 'Tags' => tags,
78
+ 'Status' => status
79
+ }
80
+ filter['Name'] = name unless name.nil?
81
+ request(:members_filtered, filter)
82
+ end
83
+
84
+ # alter the tags on a Serf agent while it is running. A member-update
85
+ # event will be triggered immediately to notify the other agents in the
86
+ # cluster of the change. The tags command can add new tags, modify
87
+ # existing tags, or delete tags
88
+ #
89
+ # @param tags [Hash] a hash representing tags as key-value pairs
90
+ # @param delete_tags [Array] an array of tags to be deleted
91
+ # @return [Response]
92
+ def tags(tags, delete_tags = [])
93
+ request(:tags, 'Tags' => tags, 'DeleteTags' => delete_tags)
94
+ end
95
+
96
+ # subscribe to a stream of all events matching a given type filter.
97
+ # Events will continue to be sent until the stream is stopped
98
+ #
99
+ # @param types [String] comma separated list of events
100
+ # @return [Thread, Response]
101
+ def stream(types, &block)
102
+ res = request(:stream, 'Type' => types)
103
+ t = Thread.new do
104
+ loop do
105
+ header = read_data
106
+ check_rpc_error!(header)
107
+ if header['Seq'] == res.header.seq
108
+ ev = read_data
109
+ block.call(ev) if block
110
+ else
111
+ break
112
+ end
113
+ end
114
+ end
115
+ [res, t]
116
+ end
117
+
118
+ # monitor is similar to the stream command, but instead of events it
119
+ # subscribes the channel to log messages from the agent
120
+ #
121
+ # @param loglevel [String]
122
+ # @return [Response]
123
+ def monitor(loglevel = 'debug')
124
+ request(:monitor, 'LogLevel' => loglevel.upcase)
125
+ end
126
+
127
+ # stop is used to stop either a stream or monitor
128
+ def stop(sequence_number)
129
+ tcp_send(:stop, 'Stop' => sequence_number)
130
+ end
131
+
132
+ # leave is used trigger a graceful leave and shutdown of the current agent
133
+ #
134
+ # @return [Response]
135
+ def leave
136
+ request(:leave)
137
+ end
138
+
139
+ # query is used to issue a new query
140
+ #
141
+ # @param name [String] name of the query
142
+ # @param payload [String] payload for this query event
143
+ # @param opts [Hash] additional query options
144
+ # @return [Response]
145
+ def query(name, payload, opts = {}, &block)
146
+ params = { 'Name' => name, 'Payload' => payload }
147
+ params.merge!(opts)
148
+ res = request(:query, params)
149
+ loop do
150
+ header = read_data
151
+ check_rpc_error!(header)
152
+ ev = read_data
153
+ if ev['Type'] == 'done'
154
+ break
155
+ else
156
+ block.call(ev) if block
157
+ end
158
+ end
159
+ res
160
+ end
161
+
162
+ # respond is used with `stream` to subscribe to queries and then respond.
163
+ #
164
+ # @param id [Integer] an opaque value that is assigned by the IPC layer
165
+ # @param payload [String] payload for the response event
166
+ # @return [Response]
167
+ def respond(id, payload)
168
+ request(:respond, 'ID' => id, 'Payload' => payload)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: UTF-8
2
+ #
3
+ require 'msgpack'
4
+ require 'timeout'
5
+ require 'serfx/log'
6
+ require 'serfx/response'
7
+ require 'serfx/commands'
8
+ require 'serfx/exceptions'
9
+ require 'socket'
10
+
11
+ module Serfx
12
+ # This class wraps the low level msgpack data transformation and tcp
13
+ # communication for the RPC session. methods in this module are used to
14
+ # implement the actual RPC commands available via [Commands]
15
+ class Connection
16
+ COMMANDS = {
17
+ handshake: [:header],
18
+ auth: [:header],
19
+ event: [:header],
20
+ force_leave: [:header],
21
+ join: [:header, :body],
22
+ members: [:header, :body],
23
+ members_filtered: [:header, :body],
24
+ tags: [:header],
25
+ stream: [:header],
26
+ monitor: [:header],
27
+ stop: [:header],
28
+ leave: [:header],
29
+ query: [:header],
30
+ respond: [:header]
31
+ }
32
+
33
+ include Serfx::Commands
34
+ extend Forwardable
35
+
36
+ attr_reader :host, :port, :seq
37
+ def_delegators :@socket, :close, :closed?
38
+
39
+ # @param opts [Hash] Specify the RPC connection details
40
+ # @option opts [Symbol] :host ipaddreess of the target serf agent
41
+ # @option opts [Symbol] :port port of target serf agents RPC
42
+ # @option opts [Symbol] :authkey encryption key for RPC communication
43
+ def initialize(opts = {})
44
+ @host = opts[:host] || '127.0.0.1'
45
+ @port = opts[:port] || 7373
46
+ @seq = 0
47
+ @authkey = opts[:authkey]
48
+ @requests = {}
49
+ @responses = {}
50
+ end
51
+
52
+ # creates a tcp socket if does not exist already, against RPC host/port
53
+ #
54
+ # @return [TCPSocket]
55
+ def socket
56
+ @socket ||= TCPSocket.new(host, port)
57
+ end
58
+
59
+ # creates a MsgPack un-packer object from the tcp socket unless its
60
+ # already present
61
+ #
62
+ # @return [MessagePack::Unpacker]
63
+ def unpacker
64
+ @unpacker ||= MessagePack::Unpacker.new(socket)
65
+ end
66
+
67
+ # read data from tcp socket and pipe it through msgpack unpacker for
68
+ # deserialization
69
+ #
70
+ # @return [Hash]
71
+ def read_data
72
+ unpacker.read
73
+ end
74
+
75
+ # takes raw RPC command name and an optional request body
76
+ # and convert them to msgpack encoded data and then send
77
+ # over tcp
78
+ #
79
+ # @param command [String] RPC command name
80
+ # @param body [Hash] request body of the RPC command
81
+ #
82
+ # @return [Integer]
83
+ def tcp_send(command, body = nil)
84
+ @seq += 1
85
+ header = {
86
+ 'Command' => command.to_s.gsub('_', '-'),
87
+ 'Seq' => seq
88
+ }
89
+ Log.info("#{__method__}|Header: #{header.inspect}")
90
+ buff = MessagePack::Buffer.new
91
+ buff << header.to_msgpack
92
+ buff << body.to_msgpack unless body.nil?
93
+ res = socket.send(buff.to_str, 0)
94
+ Log.info("#{__method__}|Res: #{res.inspect}")
95
+ @requests[seq] = { header: header, ack?: false }
96
+ seq
97
+ end
98
+
99
+ # checks if the RPC response header has `error` field popular or not
100
+ # raises [RPCError] exception if error string is not empty
101
+ #
102
+ # @param header [Hash] RPC response header as hash
103
+ def check_rpc_error!(header)
104
+ fail RPCError, header['Error'] unless header['Error'].empty?
105
+ end
106
+
107
+
108
+ # read data from the tcp socket. and convert it to a [Response] object
109
+ #
110
+ # @param command [String] RPC command name for which response will be read
111
+ # @return [Response]
112
+ def read_response(command)
113
+ header = read_data
114
+ check_rpc_error!(header)
115
+ if COMMANDS[command].include?(:body)
116
+ body = read_data
117
+ Response.new(header, body)
118
+ else
119
+ Response.new(header)
120
+ end
121
+ end
122
+
123
+ # make an RPC request against the serf agent
124
+ #
125
+ # @param command [String] name of the RPC command
126
+ # @param body [Hash] an optional request body for the RPC command
127
+ # @return [Response]
128
+ def request(command, body = nil)
129
+ tcp_send(command, body)
130
+ read_response(command)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module Serfx
4
+ class RPCError < RuntimeError ; end
5
+ end
data/lib/serfx/log.rb ADDED
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'mixlib/log'
4
+
5
+ module Serfx
6
+ # Provides singleton logger using mixlib/log library
7
+ class Log
8
+ extend Mixlib::Log
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+ #
3
+ module Serfx
4
+ # Store agent rpc response data
5
+ # All RPC responses in Serf composed of an header and an optional
6
+ # body.
7
+ #
8
+ class Response
9
+
10
+ # Header is composed of two sub-parts
11
+ # - Seq : an integer representing the original request
12
+ # - Error: a string that represent whether the request made, was
13
+ # successfull or no. For all successful RPC requests, Error should
14
+ # be an empty string
15
+ #
16
+ # `{"Seq": 0, "Error": ""}`
17
+ #
18
+ Header = Struct.new(:seq, :error)
19
+ attr_reader :header, :body
20
+
21
+ # Constructs a response object from a given header and body.
22
+ #
23
+ # @param header [Hash] header of the response as hash
24
+ # @param body [Hash] body of the response as hash
25
+ def initialize(header, body = nil)
26
+ @header = Header.new(header['Seq'], header['Error'])
27
+ @body = body
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ module Serfx
4
+ # Helper methods for string manipulation
5
+ module Utils
6
+ # snakecase to camelcase converter. Taken from chef
7
+ # (mixin/convert_to_class_name.rb)
8
+ def camel_case(str)
9
+ str = str.dup
10
+ str.gsub!(/[^A-Za-z0-9_]/, '_')
11
+ rname = nil
12
+ regexp = /^(.+?)(_(.+))?$/
13
+ mn = str.match(regexp)
14
+ if mn
15
+ rname = mn[1].capitalize
16
+ while mn && mn[3]
17
+ mn = mn[3].match(regexp)
18
+ rname << mn[1].capitalize if mn
19
+ end
20
+ end
21
+ rname
22
+ end
23
+ # camelcase to snakecase converter
24
+ def snake_case(str)
25
+ str.gsub(/[A-Z]/) { |s| '_' + s }.downcase.sub(/^\_/, '')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Provides version as a contsant for the serf gem
4
+ module Serfx
5
+ VERSION = '0.0.1'
6
+ end
data/lib/serfx.rb ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'serfx/log'
4
+ require 'serfx/version'
5
+ require 'serfx/connection'
6
+
7
+ # Serfx is a minimal ruby client for serf.
8
+ module Serfx
9
+ # Creates a serf rpc connection, performs handshake and auth
10
+ # (if authkey is supplied), if
11
+ # a block if provided, the connection will be closed after the block's
12
+ # execution.
13
+ # Params:
14
+ # +opts+:: An optional hash which can have following keys:
15
+ # * host => Serf host's rpc bind address (127.0.0.1 by default)
16
+ # * port => Serf host's rpc port (7373 by default)
17
+ # * authkey => Encryption key for RPC communiction
18
+ def self.connect(opts = {})
19
+ conn = Serfx::Connection.new(opts)
20
+ conn.handshake
21
+ conn.auth if opts.key?(:authkey)
22
+ if block_given?
23
+ yield conn
24
+ conn.close unless conn.closed?
25
+ else
26
+ conn
27
+ end
28
+ end
29
+ end
data/serfx.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'serfx/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'serfx'
8
+ spec.version = Serfx::VERSION.dup
9
+ spec.authors = ['Rnjib Dey']
10
+ spec.email = ['dey.ranjib@gmail.com']
11
+ spec.summary = %q{A barebone ruby client for serf}
12
+ spec.description = %q{Serfx is a minimal ruby client for serf, an event based, distributed, fault tolerant orchestration system}
13
+ spec.homepage = 'https://github.com/ranjib/serfx'
14
+ spec.license = 'Apache'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'msgpack'
22
+ spec.add_dependency 'mixlib-log'
23
+
24
+ spec.add_development_dependency 'bundler'
25
+ spec.add_development_dependency 'rake'
26
+ spec.add_development_dependency 'rspec'
27
+ spec.add_development_dependency 'cucumber'
28
+ spec.add_development_dependency 'rubocop'
29
+ spec.add_development_dependency 'redcarpet'
30
+ spec.add_development_dependency 'yard'
31
+ end
@@ -0,0 +1,3 @@
1
+ {
2
+ "rpc_auth" : "awesomesecret"
3
+ }
@@ -0,0 +1,132 @@
1
+ # encoding: UTF-8
2
+ #
3
+ require 'spec_helper'
4
+ require 'timeout'
5
+ require 'thread'
6
+
7
+ describe Serfx do
8
+
9
+ before(:all) do
10
+ start_cluster(5)
11
+ end
12
+
13
+ after(:all) do
14
+ stop_cluster
15
+ end
16
+
17
+ before(:each) do
18
+ @conn = Serfx::Connection.new(port: 5000, authkey: 'awesomesecret')
19
+ @conn.handshake
20
+ @conn.auth
21
+ end
22
+
23
+ after(:each) do
24
+ @conn.close
25
+ end
26
+
27
+ let(:new_connection) do
28
+ Serfx::Connection.new(port: 5000, authkey: 'awesomesecret')
29
+ end
30
+
31
+ it '#handshake' do
32
+ conn = new_connection
33
+ response = conn.handshake
34
+ conn.close
35
+ expect(response.header.error).to be_empty
36
+ expect(response.header.seq).to eq(1)
37
+ end
38
+
39
+ it '#auth' do
40
+ conn = new_connection
41
+ conn.handshake
42
+ response = conn.auth
43
+ conn.close
44
+ expect(response.header.error).to be_empty
45
+ end
46
+
47
+ it '#event' do
48
+ response = @conn.event('seriously')
49
+ expect(response.header.error).to be_empty
50
+ end
51
+
52
+ it '#force-leave' do
53
+ last_pid = Serfx::SpecHelper::Spawner.instance.pids.pop
54
+ Process.kill('TERM', last_pid)
55
+ response = @conn.force_leave('node_4')
56
+ expect(response.header.error).to be_empty
57
+ time = 0
58
+ expect do
59
+ Timeout.timeout(10) do
60
+ node = @conn.members.body['Members'].find do |n|
61
+ n['Name'] == 'node_4'
62
+ end
63
+ until node['Status'] == 'left'
64
+ time += 1
65
+ sleep 1
66
+ node = @conn.members.body['Members'].find do |n|
67
+ n['Name'] == 'node_4'
68
+ end
69
+ end
70
+ end
71
+ end.to_not raise_error
72
+ end
73
+
74
+ it '#join' do
75
+ Serfx::SpecHelper::Spawner.instance.start(1, join: false)
76
+ sleep 3
77
+ @conn.join(['127.0.0.1:4000'])
78
+ sleep 2
79
+ expect(@conn.members.body['Members'].size).to eq(5)
80
+ end
81
+
82
+ it '#members' do
83
+ response = @conn.members
84
+ expect(response.header.error).to be_empty
85
+ expect(response.body['Members'].size).to eq(5)
86
+ end
87
+
88
+ it '#members-filtered' do
89
+ response = @conn.members_filtered('group' => 'odd')
90
+ expect(response.header.error).to be_empty
91
+ tags = response.body['Members'].map { |x|x['Tags']['group'] }
92
+ expect(tags.all? { |t| t == 'odd' }).to be_true
93
+ end
94
+
95
+ it '#tags' do
96
+ response = @conn.tags('service' => 'foo')
97
+ expect(response.header.error).to be_empty
98
+ response = @conn.members_filtered('service' => 'foo')
99
+ expect(response.body['Members']).to_not be_empty
100
+ end
101
+
102
+ it '#stream and stop' do
103
+ Serfx.connect(port: 5000, authkey: 'awesomesecret') do |c|
104
+ data = nil
105
+ _, t = c.stream('user:test') do |event|
106
+ data = event
107
+ end
108
+ sleep 2
109
+ @conn.event('test', 'whoa')
110
+ sleep 2
111
+ t.kill
112
+ expect(data['Name']).to eq('test')
113
+ expect(data['Payload']).to eq('whoa')
114
+ expect(data['Coalesce']).to be_true
115
+ expect(data['Event']).to eq('user')
116
+ end
117
+ end
118
+
119
+ it '#stream, query, respond and stop' do
120
+ Serfx.connect(port: 5000, authkey: 'awesomesecret') do |c|
121
+ res, t = c.stream('query') do |q|
122
+ c.respond(q['ID'], q['Payload'].to_s.upcase) if q['ID']
123
+ end
124
+ sleep 3
125
+ @conn.query('test', 'whoa') do |r|
126
+ expect(r['Payload']).to eq('WHOA')
127
+ end
128
+ c.stop(res.header.seq)
129
+ t.kill
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: UTF-8
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
4
+ $LOAD_PATH.unshift(File.expand_path('../spec', __FILE__))
5
+ require 'rspec'
6
+ require 'serfx'
7
+ require 'singleton'
8
+ require 'tmpdir'
9
+ require 'codeclimate-test-reporter'
10
+ CodeClimate::TestReporter.start if ENV['CODE_COVERAGE']
11
+
12
+ module Serfx
13
+ # adds helper method for unit testing
14
+ module SpecHelper
15
+ # provides serf cluster setup helper methods
16
+ class Spawner
17
+ include Singleton
18
+ attr_reader :pids
19
+
20
+ def initialize
21
+ @pids = []
22
+ end
23
+
24
+ def servers
25
+ @pids.size.times.reduce([]) { |a, e| a << { port: (7373 + e) } }
26
+ end
27
+
28
+ def start(numbers = 1, opts = {})
29
+ (numbers).times do |n|
30
+ join = if @pids.empty?
31
+ false
32
+ elsif opts.key?(:join)
33
+ opts[:join]
34
+ else
35
+ true
36
+ end
37
+ @pids << daemonize(Dir.mktmpdir, join)
38
+ end
39
+ end
40
+
41
+ def daemonize(dir, join = false)
42
+ command = serf_command(join)
43
+ pid = spawn(command, out: '/dev/null', chdir: dir)
44
+ Process.detach(pid)
45
+ pid
46
+ end
47
+
48
+ def serf_command(join)
49
+ n = @pids.size
50
+ group = n.even? ? 'even' : 'odd'
51
+ config = File.expand_path('../data/config.json', __FILE__)
52
+ args = " -bind 127.0.0.1:#{4000 + n} -rpc-addr 127.0.0.1:#{5000 + n} "
53
+ args << "-config-file #{config} -node node_#{n} -tag group=#{group}"
54
+ args << ' -join 127.0.0.01:4000' if join
55
+ serf_binary + ' agent ' + args
56
+ end
57
+
58
+ def serf_binary
59
+ path = File.expand_path('../../serf_binaries/serf', __FILE__)
60
+ if File.exist?(path)
61
+ path
62
+ elsif ENV['SERF_BIN']
63
+ ENV['SERF_BIN']
64
+ else
65
+ fail 'serf binary not found., you need to set SERF_BIN'
66
+ end
67
+ end
68
+
69
+ def stop
70
+ @pids.each do |pid|
71
+ Process.kill('TERM', pid)
72
+ end
73
+ FileUtils.remove_entry_secure(@tmpdir, true)
74
+ @pids.clear
75
+ end
76
+ end
77
+
78
+ def start_cluster(numbers = 1, opts = {})
79
+ Spawner.instance.start(numbers, opts)
80
+ sleep 1
81
+ end
82
+
83
+ def stop_cluster
84
+ Spawner.instance.stop
85
+ end
86
+ end
87
+ end
88
+
89
+ RSpec.configure do |c|
90
+ c.include Serfx::SpecHelper
91
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: serfx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Rnjib Dey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: msgpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mixlib-log
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: cucumber
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: redcarpet
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Serfx is a minimal ruby client for serf, an event based, distributed,
140
+ fault tolerant orchestration system
141
+ email:
142
+ - dey.ranjib@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".rubocop.yml"
149
+ - ".travis.yml"
150
+ - ".yardops"
151
+ - Gemfile
152
+ - LICENSE
153
+ - LICENSE.txt
154
+ - README.md
155
+ - Rakefile
156
+ - download_serf
157
+ - lib/serfx.rb
158
+ - lib/serfx/commands.rb
159
+ - lib/serfx/connection.rb
160
+ - lib/serfx/exceptions.rb
161
+ - lib/serfx/log.rb
162
+ - lib/serfx/response.rb
163
+ - lib/serfx/utils.rb
164
+ - lib/serfx/version.rb
165
+ - serfx.gemspec
166
+ - spec/data/config.json
167
+ - spec/serfx/client_spec.rb
168
+ - spec/spec_helper.rb
169
+ homepage: https://github.com/ranjib/serfx
170
+ licenses:
171
+ - Apache
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubyforge_project:
189
+ rubygems_version: 2.2.2
190
+ signing_key:
191
+ specification_version: 4
192
+ summary: A barebone ruby client for serf
193
+ test_files:
194
+ - spec/data/config.json
195
+ - spec/serfx/client_spec.rb
196
+ - spec/spec_helper.rb
197
+ has_rdoc: