right_agent 0.6.3 → 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
data/lib/right_agent.rb CHANGED
@@ -24,6 +24,7 @@
24
24
  require File.expand_path(File.join(File.dirname(__FILE__), 'right_agent', 'minimal'))
25
25
 
26
26
  require 'amqp'
27
+ require 'mq'
27
28
  require 'json'
28
29
  require 'yaml'
29
30
  require 'openssl'
@@ -184,7 +184,7 @@ module RightScale
184
184
  request = RightScale::IdempotentRequest.new("/mapper/query_tags", payload)
185
185
  request.callback { |result| yield raw ? request.raw_response : result }
186
186
  request.errback do |message|
187
- RightScale::RightLinkLog.error("Failed to query tags: #{message}")
187
+ Log.error("Failed to query tags: #{message}")
188
188
  yield((raw ? request.raw_response : nil) || message)
189
189
  end
190
190
  request.run
@@ -543,6 +543,11 @@ module RightScale
543
543
  # Do not let closed connection regress to failed
544
544
  return true if status == :failed && @status == :closed
545
545
 
546
+ # Wait until connection is ready (i.e. handshake with broker is completed) before
547
+ # changing our status to connected
548
+ return true if status == :connected
549
+ status = :connected if status == :ready
550
+
546
551
  before = @status
547
552
  @status = status
548
553
 
@@ -587,7 +592,7 @@ module RightScale
587
592
  :heartbeat => @options[:heartbeat],
588
593
  :reconnect_delay => lambda { rand(reconnect_interval) },
589
594
  :reconnect_interval => reconnect_interval)
590
- @channel = AMQP::Channel.new(@connection)
595
+ @channel = MQ.new(@connection)
591
596
  @channel.__send__(:connection).connection_status { |status| update_status(status) }
592
597
  @channel.prefetch(@options[:prefetch]) if @options[:prefetch]
593
598
  rescue Exception => e
@@ -27,6 +27,7 @@ require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'cookbook'))
27
27
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'cookbook_repository'))
28
28
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'cookbook_sequence'))
29
29
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'cookbook_position'))
30
+ require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'dev_repository'))
30
31
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'dev_repositories'))
31
32
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'executable_bundle'))
32
33
  require File.normalize_path(File.join(CORE_PAYLOAD_TYPES_BASE_DIR, 'event_categories'))
@@ -21,6 +21,8 @@
21
21
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
+ require File.join(File.dirname(__FILE__), 'dev_repository')
25
+
24
26
  module RightScale
25
27
  # Sequence of cookbooks to be checked out on the instance.
26
28
  class DevRepositories
@@ -76,7 +78,15 @@ module RightScale
76
78
  # result(Hash):: The entry added to the collection of repositories
77
79
  def add_repo(repo_sha, repo_detail, cookbook_positions)
78
80
  @repositories ||= {}
79
- @repositories[repo_sha] = { :repo => repo_detail, :positions => cookbook_positions }
81
+ @repositories[repo_sha] = DevRepository.new(repo_detail[:repo_type],
82
+ repo_detail[:url],
83
+ repo_detail[:tag],
84
+ repo_detail[:cookboooks_path],
85
+ repo_detail[:ssh_key],
86
+ repo_detail[:username],
87
+ repo_detail[:password],
88
+ repo_sha,
89
+ cookbook_positions)
80
90
  end
81
91
 
82
92
  #
@@ -0,0 +1,76 @@
1
+ #--
2
+ # Copyright: Copyright (c) 2010-2011 RightScale, Inc.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # 'Software'), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightScale
25
+ # Sequence of cookbooks to be checked out on the instance.
26
+ class DevRepository
27
+ include Serializable
28
+
29
+ # (Symbol) Type of repository: one of :git, :svn, :download or :local
30
+ # * :git denotes a 'git' repository that should be retrieved via 'git clone'
31
+ # * :svn denotes a 'svn' repository that should be retrieved via 'svn checkout'
32
+ # * :download denotes a tar ball that should be retrieved via HTTP GET (HTTPS if uri starts with https://)
33
+ # * :local denotes cookbook that is already local and doesn't need to be retrieved
34
+ attr_accessor :repo_type
35
+ # (String) URL to repository (e.g. git://github.com/opscode/chef-repo.git)
36
+ attr_accessor :url
37
+ # (String) git commit or svn branch that should be used to retrieve repository
38
+ # Optional, use 'master' for git and 'trunk' for svn if tag is nil.
39
+ # Not used for raw repositories.
40
+ attr_accessor :tag
41
+ # (Array) Path to cookbooks inside repostory
42
+ # Optional (use location of repository as cookbook path if nil)
43
+ attr_accessor :cookbooks_path
44
+ # (String) Private SSH key used to retrieve git repositories
45
+ # Optional, not used for svn and raw repositories.
46
+ attr_accessor :ssh_key
47
+ # (String) Username used to retrieve svn and raw repositories
48
+ # Optional, not used for git repositories.
49
+ attr_accessor :username
50
+ # (String) Password used to retrieve svn and raw repositories
51
+ # Optional, not used for git repositories.
52
+ attr_accessor :password
53
+ # (String) hash of the CookbookSequence that corresponds to the repo
54
+ attr_accessor :repo_sha
55
+ # (Array) List of cookbook <name, position> pairs
56
+ attr_accessor :positions
57
+
58
+ # Initialize fields from given arguments
59
+ def initialize(*args)
60
+ @repo_type = args[0]
61
+ @url = args[1] if args.size > 1
62
+ @tag = args[2] if args.size > 2
63
+ @cookbooks_path = args[3] if args.size > 3
64
+ @ssh_key = args[4] if args.size > 4
65
+ @username = args[5] if args.size > 5
66
+ @password = args[6] if args.size > 6
67
+ @repo_sha = args[7] if args.size > 7
68
+ @positions = args[8] if args.size > 8
69
+ end
70
+
71
+ # Array of serialized fields given to constructor
72
+ def serialized_members
73
+ [ @repo_type, @url, @tag, @cookbooks_path, @ssh_key, @username, @password, @repo_sha, @positions ]
74
+ end
75
+ end
76
+ end
@@ -319,8 +319,9 @@ module RightScale
319
319
  if new_level != @level
320
320
  @logger.info("[setup] Setting log level to #{level_to_sym(new_level).to_s.upcase}")
321
321
  @logger.level = @level = new_level
322
- @notify.each { |n| n.call(@level) } if @notify
323
322
  end
323
+ # Notify even if unchanged since don't know when callback was set
324
+ @notify.each { |n| n.call(@level) } if @notify
324
325
  end
325
326
  level = level_to_sym(@level)
326
327
  end
@@ -20,48 +20,170 @@
20
20
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
 
23
- begin
24
- # Clean up AMQP connection when an error is raised after a broker request failure,
25
- # otherwise AMQP becomes unusable
26
- AMQP.module_eval do
27
- def self.start *args, &blk
28
- begin
29
- EM.run{
30
- @conn ||= connect *args
31
- @conn.callback { AMQP.channel = AMQP::Channel.new(@conn) }
23
+ class MQ
32
24
 
33
- # callback passed to .start must come last
34
- @conn.callback(&blk) if blk
35
- @conn
25
+ class Queue
26
+ # Asks the broker to redeliver all unacknowledged messages on a
27
+ # specified channel. Zero or more messages may be redelivered.
28
+ #
29
+ # * requeue (default false)
30
+ # If this parameter is false, the message will be redelivered to the original recipient.
31
+ # If this flag is true, the server will attempt to requeue the message, potentially then
32
+ # delivering it to an alternative subscriber.
33
+ #
34
+ def recover(requeue = false)
35
+ @mq.callback{
36
+ @mq.send Protocol::Basic::Recover.new({ :requeue => requeue })
37
+ }
38
+ self
39
+ end
40
+ end
41
+
42
+ # May raise a MQ::Error exception when the frame payload contains a
43
+ # Protocol::Channel::Close object.
44
+ #
45
+ # This usually occurs when a client attempts to perform an illegal
46
+ # operation. A short, and incomplete, list of potential illegal operations
47
+ # follows:
48
+ # * publish a message to a deleted exchange (NOT_FOUND)
49
+ # * declare an exchange using the reserved 'amq.' naming structure (ACCESS_REFUSED)
50
+ #
51
+ def process_frame frame
52
+ log :received, frame
53
+
54
+ case frame
55
+ when Frame::Header
56
+ @header = frame.payload
57
+ @body = ''
58
+
59
+ when Frame::Body
60
+ @body << frame.payload
61
+ if @body.length >= @header.size
62
+ if @method.is_a? Protocol::Basic::Return
63
+ @on_return_message.call @method, @body if @on_return_message
64
+ else
65
+ @header.properties.update(@method.arguments)
66
+ @consumer.receive @header, @body if @consumer
67
+ end
68
+ @body = @header = @consumer = @method = nil
69
+ end
70
+
71
+ when Frame::Method
72
+ case method = frame.payload
73
+ when Protocol::Channel::OpenOk
74
+ send Protocol::Access::Request.new(:realm => '/data',
75
+ :read => true,
76
+ :write => true,
77
+ :active => true,
78
+ :passive => true)
79
+
80
+ when Protocol::Access::RequestOk
81
+ @ticket = method.ticket
82
+ callback{
83
+ send Protocol::Channel::Close.new(:reply_code => 200,
84
+ :reply_text => 'bye',
85
+ :method_id => 0,
86
+ :class_id => 0)
87
+ } if @closing
88
+ succeed
89
+
90
+ when Protocol::Basic::CancelOk
91
+ if @consumer = consumers[ method.consumer_tag ]
92
+ @consumer.cancelled
93
+ else
94
+ MQ.error "Basic.CancelOk for invalid consumer tag: #{method.consumer_tag}"
95
+ end
96
+
97
+ when Protocol::Queue::DeclareOk
98
+ queues[ method.queue ].receive_status method
99
+
100
+ when Protocol::Basic::Deliver, Protocol::Basic::GetOk
101
+ @method = method
102
+ @header = nil
103
+ @body = ''
104
+
105
+ if method.is_a? Protocol::Basic::GetOk
106
+ @consumer = get_queue{|q| q.shift }
107
+ MQ.error "No pending Basic.GetOk requests" unless @consumer
108
+ else
109
+ @consumer = consumers[ method.consumer_tag ]
110
+ MQ.error "Basic.Deliver for invalid consumer tag: #{method.consumer_tag}" unless @consumer
111
+ end
112
+
113
+ when Protocol::Basic::GetEmpty
114
+ if @consumer = get_queue{|q| q.shift }
115
+ @consumer.receive nil, nil
116
+ else
117
+ MQ.error "Basic.GetEmpty for invalid consumer"
118
+ end
119
+
120
+ when Protocol::Basic::Return
121
+ @method = method
122
+ @header = nil
123
+ @body = ''
124
+
125
+ when Protocol::Channel::Close
126
+ raise Error, "#{method.reply_text} in #{Protocol.classes[method.class_id].methods[method.method_id]} on #{@channel}"
127
+
128
+ when Protocol::Channel::CloseOk
129
+ @closing = false
130
+ conn.callback{ |c|
131
+ c.channels.delete @channel
132
+ c.close if c.channels.empty?
36
133
  }
37
- rescue Exception => e
38
- @conn = nil
39
- raise e
134
+
135
+ when Protocol::Basic::ConsumeOk
136
+ if @consumer = consumers[ method.consumer_tag ]
137
+ @consumer.confirm_subscribe
138
+ else
139
+ MQ.error "Basic.ConsumeOk for invalid consumer tag: #{method.consumer_tag}"
140
+ end
40
141
  end
41
142
  end
42
143
  end
43
144
 
145
+ # Provide callback to be activated when a message is returned
146
+ def return_message(&blk)
147
+ @on_return_message = blk
148
+ end
149
+
150
+ end
151
+
152
+ # monkey patch to the amqp gem that adds :no_declare => true option for new Queue objects.
153
+ # This allows an instance that has no configuration privileges to enroll without blowing
154
+ # up the AMQP gem when it tries to subscribe to its queue before it has been created.
155
+ # Exchange :no_declare support is already in the eventmachine-0.12.10 gem.
156
+ # temporary until we get this into amqp proper
157
+ MQ::Queue.class_eval do
158
+ def initialize mq, name, opts = {}
159
+ @mq = mq
160
+ @opts = opts
161
+ @bindings ||= {}
162
+ @mq.queues[@name = name] ||= self
163
+ unless opts[:no_declare]
164
+ @mq.callback{
165
+ @mq.send AMQP::Protocol::Queue::Declare.new({ :queue => name,
166
+ :nowait => true }.merge(opts))
167
+ }
168
+ end
169
+ end
170
+ end
171
+
172
+ begin
173
+ # Monkey patch AMQP reconnect backoff
44
174
  AMQP::Client.module_eval do
45
- # Add callback for connection failure
46
175
  def initialize opts = {}
47
176
  @settings = opts
48
177
  extend AMQP.client
49
178
 
50
- @_channel_mutex = Mutex.new
51
-
52
179
  @on_disconnect ||= proc{ @connection_status.call(:failed) if @connection_status }
53
180
 
54
181
  timeout @settings[:timeout] if @settings[:timeout]
55
182
  errback{ @on_disconnect.call } unless @reconnecting
56
- @connection_status = @settings[:connection_status]
57
183
 
58
- # TCP connection "openness"
59
- @tcp_connection_established = false
60
- # AMQP connection "openness"
61
- @connected = false
184
+ @connected = false
62
185
  end
63
186
 
64
- # Add backoff controls to the reconnect algorithm
65
187
  def reconnect(force = false)
66
188
  if @reconnecting and not force
67
189
  # Wait after first reconnect attempt and in between each subsequent attempt
@@ -94,96 +216,53 @@ begin
94
216
  "#{RightScale::AgentIdentity.new('rs', 'broker', @settings[:port].to_i, @settings[:host].gsub('-', '~')).to_s}")
95
217
  log 'reconnecting'
96
218
  EM.reconnect(@settings[:host], @settings[:port], self)
97
- rescue Exception => e
98
- RightScale::Log.error("Exception caught during AMQP reconnect", e, :trace)
99
- reconnect if @reconnecting
100
219
  end
220
+ end
101
221
 
102
- # Catch exceptions that would otherwise cause EM to stop or be in a bad state if a top
103
- # level EM error handler was setup. Instead close the connection and leave EM alone.
104
- # Don't log an error if the environment variable IGNORE_AMQP_FAILURES is set
105
- alias :orig_receive_data :receive_data
106
- def receive_data(*args)
222
+ # Monkey patch AMQP to clean up @conn when an error is raised after a broker request failure,
223
+ # otherwise AMQP becomes unusable
224
+ AMQP.module_eval do
225
+ def self.start *args, &blk
107
226
  begin
108
- orig_receive_data(*args)
227
+ EM.run{
228
+ @conn ||= connect *args
229
+ @conn.callback(&blk) if blk
230
+ @conn
231
+ }
109
232
  rescue Exception => e
110
- unless ENV['IGNORE_AMQP_FAILURES']
111
- RightScale::Log.error("Exception caught while processing AMQP frame, closing connection", e, :trace)
112
- end
113
- close_connection
233
+ @conn = nil
234
+ raise e
114
235
  end
115
236
  end
116
-
117
- # Make it log to RightScale when logging enabled
118
- def log(*args)
119
- return unless @settings[:logging] or AMQP.logging
120
- require 'pp'
121
- RightScale::Log.info("AMQP #{args.pretty_inspect.chomp}")
122
- end
123
237
  end
124
238
 
125
- AMQP::Channel.class_eval do
126
- # Detect message return and make callback
127
- def check_content_completion
128
- if @body.length >= @header.size
129
- if @method.is_a? AMQP::Protocol::Basic::Return
130
- @on_return_message.call @method, @body if @on_return_message
131
- else
132
- @header.properties.update(@method.arguments)
133
- @consumer.receive @header, @body if @consumer
134
- end
135
- @body = @header = @consumer = @method = nil
136
- end
137
- end
138
-
139
- # Provide callback to be activated when a message is returned
140
- def return_message(&blk)
141
- @on_return_message = blk
142
- end
143
-
144
- # Apply :no_declare option
145
- def validate_parameters_match!(entity, parameters)
146
- unless entity.opts == parameters || parameters[:passive] || parameters[:no_declare] || entity.opts[:no_declare]
147
- raise AMQP::IncompatibleOptionsError.new(entity.name, entity.opts, parameters)
239
+ # This monkey patch catches exceptions that would otherwise cause EM to stop or be in a bad
240
+ # state if a top level EM error handler was setup. Instead close the connection and leave EM
241
+ # alone.
242
+ # Don't log an error if the environment variable IGNORE_AMQP_FAILURES is set (used in the
243
+ # enroll script)
244
+ AMQP::Client.module_eval do
245
+ alias :orig_receive_data :receive_data
246
+ def receive_data(*args)
247
+ begin
248
+ orig_receive_data(*args)
249
+ rescue Exception => e
250
+ RightScale::Log.error("Exception caught while processing AMQP frame, closing connection",
251
+ e, :trace) unless ENV['IGNORE_AMQP_FAILURES']
252
+ close_connection
148
253
  end
149
254
  end
150
-
151
- # Make it log to RightScale when logging enabled
152
- def log(*args)
153
- return unless AMQP.logging
154
- require 'pp'
155
- RightScale::Log.info("AMQP #{args.pretty_inspect.chomp}")
156
- end
157
255
  end
158
256
 
159
- # Add :no_declare => true option for new Queue objects to allow an instance that has
160
- # no configuration privileges to enroll without blowing up the AMQP gem when it tries
161
- # to subscribe to its queue before it has been created (already supported in gem for
162
- # Exchange)
163
- AMQP::Queue.class_eval do
164
- def initialize(mq, name, opts = {}, &block)
165
- raise ArgumentError, "queue name must not be nil. Use '' (empty string) for server-named queues." if name.nil?
166
-
167
- @mq = mq
168
- @opts = self.class.add_default_options(name, opts, block)
169
- @bindings ||= {}
170
- @status = @opts[:nowait] ? :unknown : :unfinished
171
-
172
- if name.empty?
173
- @mq.queues_awaiting_declare_ok.push(self)
174
- else
175
- @name = name
176
- end
177
-
178
- unless opts[:no_declare]
179
- @mq.callback{
180
- @mq.send AMQP::Protocol::Queue::Declare.new(@opts)
181
- }
182
- end
183
-
184
- self.callback = block
185
-
186
- block.call(self) if @opts[:nowait] && block
257
+ # Add a new callback to amqp gem that triggers once the handshake with the broker completed
258
+ # The 'connected' status callback happens before the handshake is done and if it results in
259
+ # a lot of activity it might prevent EM from being able to call the code handling the
260
+ # incoming handshake packet in a timely fashion causing the broker to close the connection
261
+ AMQP::BasicClient.module_eval do
262
+ alias :orig_process_frame :process_frame
263
+ def process_frame(frame)
264
+ orig_process_frame(frame)
265
+ @connection_status.call(:ready) if @connection_status && frame.payload.is_a?(AMQP::Protocol::Connection::Start)
187
266
  end
188
267
  end
189
268