right_agent 0.6.3 → 0.6.6

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.
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