emissary 1.3.0

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.
@@ -0,0 +1,26 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::Error < Agent
18
+ def valid_methods
19
+ [ :any ]
20
+ end
21
+
22
+ def activate
23
+ message
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::File < Agent
18
+ def valid_methods
19
+ [ :any ]
20
+ end
21
+
22
+ def activate
23
+ throw :skip_implicit_response
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+
17
+ module Emissary
18
+ class Agent::Gem < Agent
19
+ def valid_methods
20
+ [ :update, :install, :remove, :uninstall, :version ]
21
+ end
22
+
23
+ def version gem_name
24
+ ::Emissary.GemHelper.new(gem_name).version
25
+ end
26
+
27
+ # Updates Emissary from the given source to the given version
28
+ def install gem_name, version = :latest, source_url = :default
29
+ ::Emissary::GemHelper.new(gem_name).install(version, source_url)
30
+ end
31
+
32
+ def update gem_name, version = :latest, source_url = :default
33
+ ::Emissary::GemHelper.new(gem_name).update(version, source_url)
34
+ end
35
+
36
+ def uninstall gem_name, version = :latest, ignore_dependencies = true, remove_executables = false
37
+ ::Emissary::GemHelper.new(gem_name).uninstall(version, ignore_dependencies, remove_executables)
38
+ end
39
+ alias :remove :uninstall
40
+ end
41
+
42
+ end
@@ -0,0 +1,219 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require "mysql"
17
+ require "monitor"
18
+ require 'timeout'
19
+
20
+ module Emissary
21
+ class Agent::Mysql < Agent
22
+ DEFAULT_COORDINATES_FILE = '/var/nyt/mysql/master.coordinates'.freeze
23
+
24
+ def valid_methods
25
+ [ :lock, :unlock, :status ]
26
+ end
27
+
28
+ attr_accessor :coordinates_file
29
+
30
+ def lock(host, user, password, timeout = Agent::Mysql::Helper::DEFAULT_TIMEOUT, coordinates_file = nil)
31
+ @coordinates_file ||= coordinates_file
32
+ @coordinates_file ||= config[:agents][:mysql][:coordinates_file] rescue nil
33
+ @coordinates_file ||= DEFAULT_COORDINATES_FILE
34
+
35
+ locker = ::Emissary::Agent::Mysql::Helper.new(host, user, password, timeout)
36
+ locker.lock!
37
+
38
+ filename, position = locker.get_binlog_info
39
+
40
+ unless filename.nil?
41
+ write_lock_info(filename, position)
42
+ response = message.response
43
+ response.args = [ filename, position ]
44
+ response.status_note = 'Locked'
45
+ else
46
+ response = message.response
47
+ response.status_note = "No binlog information - can't lock."
48
+ end
49
+
50
+ response
51
+ end
52
+
53
+ def unlock(host, user, password)
54
+ locker = ::Emissary::Agent::Mysql::Helper.new(host, user, password)
55
+ raise "The database was not locked! (Possibly timed out.)" unless locker.locked?
56
+
57
+ locker.unlock!
58
+
59
+ response = message.response
60
+ response.status_note = 'Unlocked'
61
+ response
62
+ end
63
+
64
+ def status(host, user, password)
65
+ locker = ::Emissary::Agent::Mysql::Helper.new(host, user, password)
66
+
67
+ response = message.response
68
+ response.status_note = locker.locked? ? 'Locked' : 'Unlocked'
69
+ response
70
+ end
71
+
72
+ private
73
+
74
+ def write_lock_info(filename, position)
75
+ File.open(coordinates_file, "w") do |file|
76
+ file << "#{filename},#{position}"
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ class Agent::Mysql::Helper
83
+ DEFAULT_TIMEOUT = 30
84
+
85
+ @@class_monitor = Monitor.new
86
+
87
+ # only return one locker per host+user combination
88
+ def self.new(host, user, password, timeout = nil)
89
+ @@class_monitor.synchronize do
90
+ (@@lockers||={})["#{host}:#{user}"] ||= begin
91
+ allocate.instance_eval(<<-EOS, __FILE__, __LINE__)
92
+ initialize(host, user, password, timeout || DEFAULT_TIMEOUT)
93
+ self
94
+ EOS
95
+ end
96
+ @@lockers["#{host}:#{user}"].timeout = timeout unless timeout.nil?
97
+ @@lockers["#{host}:#{user}"]
98
+ end
99
+ end
100
+
101
+ @@locked_M = Mutex.new
102
+ def locked_M() @@locked_M; end
103
+
104
+ private
105
+
106
+ def initialize(host, user, password, timeout = DEFAULT_TIMEOUT)
107
+ @host = host
108
+ @user = user
109
+ @password = password
110
+ @timeout = timeout
111
+
112
+ @watcher = nil
113
+ @connection = nil
114
+ @locked = false
115
+ end
116
+
117
+
118
+ def connection
119
+ @connection ||= ::Mysql.real_connect(@host, @user, @password)
120
+ end
121
+
122
+ def disconnect
123
+ unless not connected?
124
+ puts "disconnecting.."
125
+ @connection.close
126
+ @connection = nil
127
+ end
128
+ end
129
+
130
+ public
131
+ attr_accessor :timeout
132
+
133
+ def connected?
134
+ !!@connection
135
+ end
136
+
137
+ # Acquire a lock and, with that lock, run a block/closure.
138
+ def with_lock
139
+ begin
140
+ lock! && yield
141
+ ensure
142
+ unlock!
143
+ end
144
+ end
145
+
146
+ def locked?
147
+ !!@locked
148
+ end
149
+
150
+ def lock!
151
+ unless locked?
152
+ kill_watcher_thread! # make sure we have a new thread for watching
153
+ locked_M.synchronize { @locked = true }
154
+ connection.query("FLUSH TABLES WITH READ LOCK")
155
+ spawn_lockwatch_thread!
156
+ end
157
+ end
158
+
159
+ def unlock!
160
+ begin
161
+ unless not locked?
162
+ locked_M.synchronize {
163
+ connection.query("UNLOCK TABLES")
164
+ @locked = false
165
+ }
166
+ end
167
+ ensure
168
+ kill_watcher_thread!
169
+ disconnect
170
+ end
171
+ end
172
+
173
+ # Test whether our login info is valid by attempting a database
174
+ # connection.
175
+ def valid?
176
+ begin
177
+ !!connection
178
+ rescue => e
179
+ false # Don't throw an exception, just return false.
180
+ ensure
181
+ disconnect if connected?
182
+ end
183
+ end
184
+
185
+ # Returns [file, position]
186
+ def get_binlog_info
187
+ raise "get_binlog_info must be called from within a lock." unless locked?
188
+ (result = connection.query("SHOW MASTER STATUS")).fetch_row[0,2]
189
+ ensure
190
+ result.free
191
+ end
192
+
193
+ def spawn_lockwatch_thread!
194
+ if @watcher.is_a?(Thread) and not @watcher.alive?
195
+ puts "Watcher is dead - restarting"
196
+ @watcher = nil
197
+ end
198
+
199
+ @watcher ||= Thread.new {
200
+ begin
201
+ puts "Entering Watcher Loop"
202
+ Timeout.timeout(@timeout) do
203
+ loop { break unless locked? }
204
+ end
205
+ rescue Timeout::Error
206
+ ensure
207
+ unlock!
208
+ Thread.exit
209
+ end
210
+ }
211
+ end
212
+
213
+ def kill_watcher_thread!
214
+ @watcher.kill unless not @watcher.is_a?(Thread) or not @watcher.alive?
215
+ @watcher = nil
216
+ end
217
+ end
218
+ end
219
+
@@ -0,0 +1,37 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::Ping < Agent
18
+ def valid_methods
19
+ [:ping, :pong]
20
+ end
21
+
22
+ def ping
23
+ reply = message.response
24
+ reply.method = :pong
25
+
26
+ ::Emissary.logger.debug "Received PING: originator: #{message.originator}"
27
+ ::Emissary.logger.debug "Sending PONG : originator: #{reply.originator}"
28
+
29
+ reply
30
+ end
31
+
32
+ def pong
33
+ ::Emissary.logger.debug "Received PONG"
34
+ throw :skip_implicit_response
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ module Emissary
17
+ class Agent::Proxy < Agent
18
+ def valid_methods
19
+ [ :any ]
20
+ end
21
+
22
+ def activate
23
+ throw :skip_implicit_response
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,233 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'escape'
17
+
18
+ module Emissary
19
+ class Agent::Rabbitmq < Agent
20
+ NIMBUL_VHOST = '/nimbul'
21
+
22
+ NODE_CONFIG_ACL = '^i-[a-f0-9.]+$'
23
+ NODE_READ_ACL = '^(amq.*|i-[a-f0-9.]+|request.%%ID%%.*)$'
24
+ NODE_WRITE_ACL = '^(amq.*|i-[a-f0-9.]+|(startup|info|shutdown).%%ID%%.*|nimbul)$'
25
+
26
+ QUEUE_INFO_ITEMS = %w[
27
+ name durable auto_delete arguments pid owner_pid
28
+ exclusive_consumer_pid exclusive_consumer_tag
29
+ messages_ready messages_unacknowledged messages_uncommitted
30
+ messages acks_uncommitted consumers transactions memory
31
+ ]
32
+
33
+ EXCHANGE_INFO_ITEMS = %w[
34
+ name type durable auto_delete arguments
35
+ ]
36
+
37
+ CONNECTION_INFO_ITEMS = %w[
38
+ pid address port peer_address peer_port state channels user
39
+ vhost timeout frame_max client_properties recv_oct recv_cnt
40
+ send_oct send_cnt send_pend
41
+ ]
42
+
43
+ CHANNEL_INFO_ITEMS = %w[
44
+ pid connection number user vhost transactional consumer_count
45
+ messages_unacknowledged acks_uncommitted prefetch_count
46
+ ]
47
+
48
+ BINDINGS_INFO_COLUMNS = %w[ exchange_name queue_name routing_key arguments ]
49
+ CONSUMER_INFO_COLUMNS = %w[ queue_name channel_process_id consumer_tag must_acknowledge ]
50
+
51
+ class CommandExecutionError < StandardError; end
52
+
53
+ def valid_methods
54
+ [
55
+ :add_user,
56
+ :delete_user,
57
+ :change_password,
58
+ :list_users,
59
+
60
+ :add_vhost,
61
+ :delete_vhost,
62
+ :list_vhosts,
63
+
64
+ :add_node_account,
65
+ :del_node_account,
66
+
67
+ :list_user_vhosts,
68
+ :list_vhost_users,
69
+
70
+ :list_queues,
71
+ :list_bindings,
72
+ :list_exchanges,
73
+ :list_connections,
74
+ :list_channels,
75
+ :list_consumers
76
+ ]
77
+ end
78
+
79
+ def list_queues(vhost)
80
+ vhost = vhost.empty? ? '/' : vhost
81
+ rabbitmqctl(:list_queues, '-p', vhost, QUEUE_INFO_ITEMS.join(" ")).collect do |line|
82
+ Hash[*QUEUE_INFO_ITEMS.zip(line.split(/\s+/)).flatten]
83
+ end
84
+ end
85
+
86
+ def list_bindings(vhost)
87
+ vhost = vhost.empty? ? '/' : vhost
88
+ rabbitmqctl(:list_bindings, '-p', vhost).collect do |line|
89
+ Hash[*BINDINGS_INFO_COLUMNS.zip(line.split(/\s+/)).flatten]
90
+ end
91
+ end
92
+
93
+ def list_exchanges(vhost)
94
+ vhost = vhost.empty? ? '/' : vhost
95
+ rabbitmqctl(:list_exchanges, '-p', vhost, EXCHANGE_INFO_ITEMS.join(" ")).collect do |line|
96
+ Hash[*EXCHANGE_INFO_ITEMS.zip(line.split(/\s+/)).flatten]
97
+ end
98
+ end
99
+
100
+ def list_connections
101
+ rabbitmqctl(:list_connections, CONNECTION_INFO_ITEMS.join(" ")).collect do |line|
102
+ Hash[*CONNECTION_INFO_ITEMS.zip(line.split(/\s+/)).flatten]
103
+ end
104
+ end
105
+
106
+ def list_channels
107
+ rabbitmqctl(:list_channels, CHANNEL_INFO_ITEMS.join(" ")).collect do |line|
108
+ Hash[*CHANNEL_INFO_ITEMS.zip(line.split(/\s+/)).flatten]
109
+ end
110
+ end
111
+
112
+ def list_consumers(vhost)
113
+ vhost = vhost.empty? ? '/' : vhost
114
+ rabbitmqctl(:list_consumers, '-p', vhost).collect do |line|
115
+ Hash[*CONSUMER_INFO_COLUMNS.zip(line.split(/\s+/)).flatten]
116
+ end
117
+ end
118
+
119
+ def list_users
120
+ rabbitmqctl(:list_users)
121
+ end
122
+
123
+ def list_vhosts
124
+ rabbitmqctl(:list_vhosts)
125
+ end
126
+
127
+ def list_vhost_users(vhost)
128
+ vhost = vhost.empty? ? '/' : vhost
129
+ rabbitmqctl(:list_permissions, '-p', vhost).flatten.select { |l|
130
+ !l.nil?
131
+ }.collect {
132
+ |l| l.split(/\s+/)[0]
133
+ }
134
+ end
135
+
136
+ def list_user_vhosts(user)
137
+ list_vhosts.select { |vhost| list_vhost_users(vhost).include? user }
138
+ end
139
+
140
+ def set_vhost_permissions(user, vhost, config, write, read)
141
+ vhost = vhost.empty? ? '/' : vhost
142
+ rabbitmqctl(:set_permissions, '-p', vhost, user, config, write, read)
143
+ end
144
+
145
+ def del_vhost_permissions(user, vhost)
146
+ vhost = vhost.empty? ? '/' : vhost
147
+ rabbitmqctl(:clear_permissions, '-p', vhost, user)
148
+ end
149
+
150
+ def add_node_account_acl(user, namespace_id)
151
+ config_acl = NODE_CONFIG_ACL.gsub('%%ID%%', namespace_id.to_s)
152
+ write_acl = NODE_WRITE_ACL.gsub('%%ID%%', namespace_id.to_s)
153
+ read_acl = NODE_READ_ACL.gsub('%%ID%%', namespace_id.to_s)
154
+
155
+ begin
156
+ set_vhost_permissions(user, NIMBUL_VHOST, config_acl, write_acl, read_acl)
157
+ rescue CommandExecutionError => e
158
+ "problem adding account acls for user: #{user}: #{e.message}"
159
+ else
160
+ "successfully added account acls for user: #{user}"
161
+ end
162
+ end
163
+
164
+ def add_node_account(user, password, namespace_id)
165
+ begin
166
+ add_user(user, password)
167
+ add_node_account_acl(user, namespace_id.to_s)
168
+ rescue CommandExecutionError => e
169
+ "failed to add new node account: #{user}:#{namespace_id.to_s}"
170
+ end
171
+ end
172
+
173
+ def del_node_account_acl(user, vhost)
174
+ begin
175
+ del_vhost_permissions(user, vhost)
176
+ rescue CommandExecutionError => e
177
+ "problem unmapping user from vhost: #{user}:#{vhost} #{e.message}"
178
+ else
179
+ "successfully unmapped user from vhost: #{user}:#{vhost}"
180
+ end
181
+ end
182
+
183
+ def add_vhost(path)
184
+ begin
185
+ !!rabbitmqctl(:add_vhost, path)
186
+ rescue CommandExecutionError => e
187
+ raise e unless e.message.include? 'vhost_already_exists'
188
+ end
189
+ end
190
+
191
+ def add_user(user, pass)
192
+ begin
193
+ !!rabbitmqctl(:add_user, user, pass)
194
+ rescue CommandExecutionError => e
195
+ raise e unless e.message.include? 'user_already_exists'
196
+ end
197
+ end
198
+
199
+ def change_password(user, pass)
200
+ begin
201
+ !!rabbitmqctl(:change_password, user, pass)
202
+ rescue CommandExecutionError => e
203
+ return false if e.message.include? 'no_such_user'
204
+ raise e
205
+ end
206
+ end
207
+
208
+ def delete_user(user)
209
+ begin
210
+ !!rabbitmqctl(:delete_user, user)
211
+ rescue CommandExecutionError => e
212
+ raise e unless e.message.include? 'no_such_user'
213
+ end
214
+ end
215
+
216
+ def delete_vhost(path)
217
+ begin
218
+ !!rabbitmqctl(:delete_vhost, path)
219
+ rescue CommandExecutionError => e
220
+ raise e unless e.message.include? 'no_such_vhost'
221
+ end
222
+ end
223
+
224
+ def rabbitmqctl(*args)
225
+ result = []
226
+ `rabbitmqctl #{Escape.shell_command([*args.collect{|a| a.to_s}])} 2>&1`.each do |line|
227
+ raise CommandExecutionError, $1 if line =~ /Error: (.*)/
228
+ result << line.chomp unless line =~ /\.\.\./
229
+ end
230
+ result
231
+ end
232
+ end
233
+ end