emissary 1.3.20 → 1.3.21

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
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
18
+ attr_reader :name, :message, :method, :config, :operator
19
+ attr_accessor :args
20
+
21
+ def initialize message, config, operator
22
+ @message = message
23
+ @operator = operator
24
+ @config = config
25
+
26
+ @method = message.method.to_sym rescue :__bad_method__
27
+ @args = message.args.clone
28
+
29
+ unless valid_methods.first == :any or valid_methods.include? @method
30
+ raise ArgumentError, "Invalid method '#{@method.to_s}' for agent '#{message.agent}'"
31
+ end
32
+
33
+ post_init
34
+ end
35
+
36
+ def post_init(); end
37
+
38
+ def valid_methods
39
+ raise StandardError, 'Not implemented'
40
+ end
41
+
42
+ def activate
43
+ catch(:skip_implicit_response) do
44
+ result = self.__send__(method, *args)
45
+ response = if not result.kind_of? ::Emissary::Message
46
+ response = message.response
47
+ response.status = [ :ok, (result == true || result.nil? ? 'Succeeded.' : result ) ]
48
+ response
49
+ else
50
+ result
51
+ end
52
+
53
+ send response
54
+ end
55
+ end
56
+
57
+ def send message
58
+ operator.send message
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,163 @@
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 'tempfile'
17
+ require 'fileutils'
18
+
19
+ module Emissary
20
+ class Agent::Emissary < Agent
21
+ INIT_DATA = [
22
+ ::Emissary.identity.name,
23
+ ::Emissary.identity.public_ip,
24
+ ::Emissary.identity.local_ip,
25
+ ::Emissary.identity.instance_id,
26
+ ::Emissary.identity.server_id,
27
+ ::Emissary.identity.cluster_id,
28
+ ::Emissary.identity.account_id,
29
+ ::Emissary.identity.queue_name,
30
+ ::Emissary.version
31
+ ]
32
+
33
+ def valid_methods
34
+ [ :reconfig, :selfupdate, :startup, :shutdown, :initdata, :reinit ]
35
+ end
36
+
37
+ def reconfig new_config
38
+ throw :skip_implicit_response if new_config.strip.empty?
39
+
40
+ if (test(?w, config[:agents][:emissary][:config_path]))
41
+ begin
42
+ ((tmp = Tempfile.new('new_config')) << new_config).flush
43
+ Emissary::Daemon.get_config(tmp.path)
44
+ rescue Exception => e
45
+ resonse = message.response
46
+ response.status_type = :error
47
+ response.status_note = e.message
48
+ return response
49
+ else
50
+ FileUtils.mv tmp.path, config[:agents][:emissary][:config_path]
51
+ # signal a USR1 to our parent, which will cause it to kill the
52
+ # children and restart them after rereading it's configuration
53
+ Process.kill('HUP', config[:parent_pid])
54
+ ensure
55
+ tmp.close
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ def selfupdate version = :latest
62
+ begin
63
+ emissary_gem = ::Emissary::GemHelper.new('emissary')
64
+ request_version, source_uri = emissary_gem.versions(version).flatten
65
+ current_version, _ = emissary_gem.versions(:current).flatten
66
+
67
+ unless not emissary_gem.installable? request_version
68
+ ::Emissary.logger.debug "Emissary SelfUpdate to version '#{request_version.to_s}' requested."
69
+ new_version = emissary_gem.update(request_version, source_uri)
70
+ ::Emissary.logger.debug "Emissary gem updated from '#{::Emissary.version}' to '#{new_version}'"
71
+ else
72
+ ::Emissary.logger.debug " -- SELFUPDATE -- [ VERSION: #{version}]"
73
+ ::Emissary.logger.debug " -- SELFUPDATE -- [ CURRENT_VERSION: #{current_version}]"
74
+ ::Emissary.logger.debug " -- SELFUPDATE -- [ REQUEST_VERSION: #{request_version}]"
75
+
76
+ notice = 'Emissary selfupdate skipped - ' + case true
77
+ when request_version.nil?
78
+ if version == :latest
79
+ "already at latest version."
80
+ else
81
+ "non-existent version '#{version}'"
82
+ end
83
+ when current_version == request_version
84
+ "already at specified version #{version}."
85
+ when current_version > request_version
86
+ "downgrade to version #{request_version} not allowed."
87
+ else
88
+ "unable to update from #{::Emissary.version} to requested version #{request_version}."
89
+ end
90
+ ::Emissary.logger.warn notice
91
+ response = message.response
92
+ response.status_note = notice
93
+ return response
94
+ end
95
+ rescue ::Gem::InstallError, ::Gem::GemNotFoundException => e
96
+ ::Emissary.logger.error "Emissary selfupdate failed with reason: #{e.message}"
97
+ return message.error(e)
98
+ else
99
+ ::Emissary.logger.debug "SelfUpdate: About to detach and run commands"
100
+ with_detached_process('emissary-selfupdate') do
101
+ %x{
102
+ emissary stop;
103
+ sleep 2;
104
+ ps uxa | grep -v grep | grep '(emissary|emop_)' | awk '{ print $2 }' | xargs kill -9;
105
+ sleep 1;
106
+ source /etc/cloudrc;
107
+ emissary start -d;
108
+ }
109
+ end
110
+ ::Emissary.logger.debug "SelfUpdate: Child detached"
111
+ throw :skip_implicit_response
112
+ end
113
+ end
114
+
115
+ def startup
116
+ message = initdata
117
+ message.recipient = config[:startup]
118
+ ::Emissary.logger.notice "Sending Startup message with args: #{message.args.inspect}"
119
+ message
120
+ end
121
+ alias :reinit :startup
122
+
123
+ def initdata
124
+ response = message.response
125
+ response.args = INIT_DATA
126
+ response
127
+ end
128
+
129
+ def shutdown
130
+ message.recipient = config[:shutdown]
131
+ message.args = [
132
+ ::Emissary.identity.server_id,
133
+ ::Emissary.identity.cluster_id,
134
+ ::Emissary.identity.account_id,
135
+ ::Emissary.identity.instance_id
136
+ ]
137
+ ::Emissary.logger.notice "Sending Shutdown message with args: #{message.args.inspect}"
138
+ message
139
+ end
140
+
141
+ private
142
+
143
+ def with_detached_process(name = nil)
144
+ raise Exception, 'Block missing for with_detached_process call' unless block_given?
145
+
146
+ # completely seperate from our parent process
147
+ pid = Kernel.fork do
148
+ Process.setsid
149
+ exit!(0) if fork
150
+ $0 = name unless name.nil?
151
+ Dir.chdir '/'
152
+ ::Emissary.logger.debug "SelfUpdate: Detached and running update command block now..."
153
+ yield
154
+ ::Emissary.logger.debug "SelfUpdate: Finished running update command block - exiting..."
155
+ exit!(0)
156
+ end
157
+
158
+ ::Emissary.logger.debug "SelfUpdate: Detaching child process now."
159
+ #don't worry about the child anymore - it's on it's own
160
+ Process.detach(pid)
161
+ end
162
+ end
163
+ 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::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,230 @@
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
+ begin
120
+ @connection.ping() unless @connection.nil?
121
+ rescue ::Mysql::Error => e
122
+ if e.message =~ /server has gone away/
123
+ ::Emissary.logger.notice "Agent::MySQL: MySQL server went away - reconnecting..."
124
+ @connection = nil
125
+ else
126
+ raise e
127
+ end
128
+ end
129
+
130
+ @connection ||= ::Mysql.real_connect(@host, @user, @password)
131
+ end
132
+
133
+ def disconnect
134
+ unless not connected?
135
+ ::Emissary.logger.notice "Agent::MySQL: disconnecting from MySQL Server .."
136
+ @connection.close
137
+ @connection = nil
138
+ end
139
+ end
140
+
141
+ public
142
+ attr_accessor :timeout
143
+
144
+ def connected?
145
+ !!@connection
146
+ end
147
+
148
+ # Acquire a lock and, with that lock, run a block/closure.
149
+ def with_lock
150
+ begin
151
+ lock! && yield
152
+ ensure
153
+ unlock!
154
+ end
155
+ end
156
+
157
+ def locked?
158
+ !!@locked
159
+ end
160
+
161
+ def lock!
162
+ unless locked?
163
+ kill_watcher_thread! # make sure we have a new thread for watching
164
+ locked_M.synchronize { @locked = true }
165
+ connection.query("FLUSH TABLES WITH READ LOCK")
166
+ spawn_lockwatch_thread!
167
+ end
168
+ end
169
+
170
+ def unlock!
171
+ begin
172
+ unless not locked?
173
+ locked_M.synchronize {
174
+ connection.query("UNLOCK TABLES")
175
+ @locked = false
176
+ }
177
+ end
178
+ ensure
179
+ disconnect
180
+ kill_watcher_thread!
181
+ end
182
+ end
183
+
184
+ # Test whether our login info is valid by attempting a database
185
+ # connection.
186
+ def valid?
187
+ begin
188
+ !!connection
189
+ rescue => e
190
+ false # Don't throw an exception, just return false.
191
+ ensure
192
+ disconnect if connected?
193
+ end
194
+ end
195
+
196
+ # Returns [file, position]
197
+ def get_binlog_info
198
+ raise "get_binlog_info must be called from within a lock." unless locked?
199
+ (result = connection.query("SHOW MASTER STATUS")).fetch_row[0,2]
200
+ ensure
201
+ result.free unless result.nil?
202
+ end
203
+
204
+ def spawn_lockwatch_thread!
205
+ if @watcher.is_a?(Thread) and not @watcher.alive?
206
+ ::Emissary.logger.notice "Agent::MySQL: Watcher is dead - restarting..."
207
+ @watcher = nil
208
+ end
209
+
210
+ @watcher ||= Thread.new {
211
+ begin
212
+ ::Emissary.logger.debug "Agent::MySQL: Entering Watcher Loop"
213
+ Timeout.timeout(@timeout) do
214
+ loop { break unless locked? }
215
+ end
216
+ rescue Timeout::Error
217
+ ensure
218
+ unlock!
219
+ Thread.exit
220
+ end
221
+ }
222
+ end
223
+
224
+ def kill_watcher_thread!
225
+ @watcher.kill unless not @watcher.is_a?(Thread) or not @watcher.alive?
226
+ @watcher = nil
227
+ end
228
+ end
229
+ end
230
+