emissary 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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