right_agent 0.14.0 → 0.16.2

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.
Files changed (35) hide show
  1. data/README.rdoc +2 -0
  2. data/lib/right_agent/actors/agent_manager.rb +1 -1
  3. data/lib/right_agent/agent.rb +28 -14
  4. data/lib/right_agent/agent_config.rb +1 -1
  5. data/lib/right_agent/agent_identity.rb +4 -5
  6. data/lib/right_agent/agent_tag_manager.rb +21 -24
  7. data/lib/right_agent/core_payload_types/executable_bundle.rb +1 -1
  8. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +7 -0
  9. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +20 -5
  10. data/lib/right_agent/exceptions.rb +44 -1
  11. data/lib/right_agent/history.rb +4 -1
  12. data/lib/right_agent/packets.rb +2 -1
  13. data/lib/right_agent/platform/darwin.rb +6 -0
  14. data/lib/right_agent/platform/linux.rb +5 -1
  15. data/lib/right_agent/platform/windows.rb +8 -4
  16. data/lib/right_agent/scripts/stats_manager.rb +3 -3
  17. data/lib/right_agent/security/cached_certificate_store_proxy.rb +27 -13
  18. data/lib/right_agent/security/encrypted_document.rb +1 -2
  19. data/lib/right_agent/security/static_certificate_store.rb +30 -14
  20. data/lib/right_agent/sender.rb +101 -47
  21. data/lib/right_agent/serialize/secure_serializer.rb +29 -27
  22. data/lib/right_agent/serialize/secure_serializer_initializer.rb +3 -3
  23. data/lib/right_agent/serialize/serializable.rb +1 -1
  24. data/lib/right_agent/serialize/serializer.rb +15 -6
  25. data/right_agent.gemspec +4 -5
  26. data/spec/agent_spec.rb +2 -2
  27. data/spec/agent_tag_manager_spec.rb +330 -0
  28. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  29. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  30. data/spec/security/cached_certificate_store_proxy_spec.rb +14 -8
  31. data/spec/security/static_certificate_store_spec.rb +13 -7
  32. data/spec/sender_spec.rb +114 -17
  33. data/spec/serialize/secure_serializer_spec.rb +78 -49
  34. data/spec/serialize/serializer_spec.rb +21 -2
  35. metadata +90 -36
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -26,12 +26,13 @@ module RightScale
26
26
  # X.509 certificate signing
27
27
  class SecureSerializer
28
28
 
29
+ class MissingPrivateKey < Exception; end
29
30
  class MissingCertificate < Exception; end
30
31
  class InvalidSignature < Exception; end
31
32
 
32
- # create the one and only SecureSerializer
33
- def self.init(serializer, identity, cert, key, store, encrypt = true)
34
- @serializer = SecureSerializer.new(serializer, identity, cert, key, store, encrypt)
33
+ # Create the one and only SecureSerializer
34
+ def self.init(serializer, identity, store, encrypt = true)
35
+ @serializer = SecureSerializer.new(serializer, identity, store, encrypt)
35
36
  true
36
37
  end
37
38
 
@@ -40,14 +41,16 @@ module RightScale
40
41
  !@serializer.nil?
41
42
  end
42
43
 
43
- # see SecureSerializer::dump
44
+ # See SecureSerializer#dump
44
45
  def self.dump(obj, encrypt = nil)
46
+ raise "Secure serializer not initialized" unless initialized?
45
47
  @serializer.dump(obj, encrypt)
46
48
  end
47
49
 
48
- # see SecureSerializer::load
49
- def self.load(msg)
50
- @serializer.load(msg)
50
+ # See SecureSerializer#load
51
+ def self.load(msg, id = nil)
52
+ raise "Secure serializer not initialized" unless initialized?
53
+ @serializer.load(msg, id)
51
54
  end
52
55
 
53
56
  # Initialize serializer, must be called prior to using it
@@ -55,17 +58,19 @@ module RightScale
55
58
  # === Parameters
56
59
  # serializer(Serializer):: Object serializer
57
60
  # identity(String):: Serialized identity associated with serialized messages
58
- # cert(String):: Certificate used to sign and decrypt serialized messages
59
- # key(RsaKeyPair):: Private key corresponding to specified cert
60
- # store(Object):: Certificate store exposing certificates used for
61
- # encryption (get_recipients) and signature validation (get_signer)
61
+ # store(Object):: Credentials store exposing certificates used for
62
+ # encryption (:get_target), signature validation (:get_signer), and
63
+ # certificate(s)/key(s) used for decryption (:get_receiver)
62
64
  # encrypt(Boolean):: true if data should be signed and encrypted, otherwise
63
65
  # just signed, true by default
64
- def initialize(serializer, identity, cert, key, store, encrypt = true)
66
+ def initialize(serializer, identity, store, encrypt = true)
65
67
  @identity = identity
66
- @cert = cert
67
- @key = key
68
+ raise "Missing local agent identity" unless @identity
68
69
  @store = store
70
+ raise "Missing credentials store" unless @store
71
+ @cert, @key = @store.get_receiver(@identity)
72
+ raise "Missing local agent public certificate" unless @cert
73
+ raise "Missing local agent private key" unless @key
69
74
  @encrypt = encrypt
70
75
  @serializer = serializer
71
76
  end
@@ -84,10 +89,6 @@ module RightScale
84
89
  # === Raise
85
90
  # Exception:: If certificate identity, certificate store, certificate, or private key missing
86
91
  def dump(obj, encrypt = nil)
87
- raise "Missing certificate identity" unless @identity
88
- raise "Missing certificate" unless @cert
89
- raise "Missing certificate key" unless @key
90
- raise "Missing certificate store" unless @store || !@encrypt
91
92
  must_encrypt = encrypt || @encrypt
92
93
  serialize_format = if obj.respond_to?(:send_version) && obj.send_version >= 12
93
94
  @serializer.format
@@ -97,12 +98,12 @@ module RightScale
97
98
  encode_format = serialize_format == :json ? :pem : :der
98
99
  msg = @serializer.dump(obj, serialize_format)
99
100
  if must_encrypt
100
- certs = @store.get_recipients(obj)
101
+ certs = @store.get_target(obj)
101
102
  if certs
102
103
  msg = EncryptedDocument.new(msg, certs).encrypted_data(encode_format)
103
104
  else
104
105
  target = obj.target_for_encryption if obj.respond_to?(:target_for_encryption)
105
- Log.warning("No certs available for object #{obj.class} being sent to #{target.inspect}\n") if target
106
+ Log.error("No certs available for object #{obj.class} being sent to #{target.inspect}\n") if target
106
107
  end
107
108
  end
108
109
  sig = Signature.new(msg, @cert, @key).data(encode_format)
@@ -114,6 +115,8 @@ module RightScale
114
115
  #
115
116
  # === Parameters
116
117
  # msg(String):: Serialized and optionally encrypted object using MessagePack or JSON
118
+ # id(String|nil):: Optional identifier of source of data for use
119
+ # in determining who is the receiver
117
120
  #
118
121
  # === Return
119
122
  # (Object):: Unserialized object
@@ -122,11 +125,7 @@ module RightScale
122
125
  # Exception:: If certificate store, certificate, or private key missing
123
126
  # MissingCertificate:: If could not find certificate for message signer
124
127
  # InvalidSignature:: If message signature check failed for message
125
- def load(msg)
126
- raise "Missing certificate store" unless @store
127
- raise "Missing certificate" unless @cert || !@encrypt
128
- raise "Missing certificate key" unless @key || !@encrypt
129
-
128
+ def load(msg, id = nil)
130
129
  msg = @serializer.load(msg)
131
130
  sig = Signature.from_data(msg['signature'])
132
131
  certs = @store.get_signer(msg['id'])
@@ -137,7 +136,10 @@ module RightScale
137
136
 
138
137
  data = msg['data']
139
138
  if data && msg['encrypted']
140
- data = EncryptedDocument.from_data(data).decrypted_data(@key, @cert)
139
+ cert, key = @store.get_receiver(id)
140
+ raise MissingCertificate.new("Could not find a certificate for #{id.inspect}") unless cert
141
+ raise MissingPrivateKey.new("Could not find a private key for #{id.inspect}") unless key
142
+ data = EncryptedDocument.from_data(data).decrypted_data(key, cert)
141
143
  end
142
144
  @serializer.load(data) if data
143
145
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -37,8 +37,8 @@ module RightScale
37
37
  cert = Certificate.load(AgentConfig.certs_file("#{agent_type}.cert"))
38
38
  key = RsaKeyPair.load(AgentConfig.certs_file("#{agent_type}.key"))
39
39
  mapper_cert = Certificate.load(AgentConfig.certs_file("mapper.cert"))
40
- store = StaticCertificateStore.new(mapper_cert, mapper_cert)
41
- SecureSerializer.init(Serializer.new, agent_id, cert, key, store)
40
+ store = StaticCertificateStore.new(cert, key, mapper_cert, mapper_cert)
41
+ SecureSerializer.init(Serializer.new, agent_id, store)
42
42
  true
43
43
  end
44
44
 
@@ -70,7 +70,7 @@ module RightScale
70
70
  {
71
71
  'msgpack_class' => self.class.name,
72
72
  'data' => serialized_members
73
- }.to_msgpack(*a)
73
+ }.to_msgpack(*a).to_s
74
74
  end
75
75
 
76
76
  # Called by JSON serializer to serialise object's members
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -90,11 +90,13 @@ module RightScale
90
90
  #
91
91
  # === Parameters
92
92
  # packet(String):: Data representing serialized object
93
+ # id(String|nil):: Optional identifier of source of data for use
94
+ # in determining who is the receiver
93
95
  #
94
96
  # === Return
95
97
  # (Object):: Unserialized object
96
- def load(packet)
97
- cascade_serializers(:load, packet, @secure ? [SecureSerializer] : order_serializers(packet))
98
+ def load(packet, id = nil)
99
+ cascade_serializers(:load, packet, @secure ? [SecureSerializer] : order_serializers(packet), id)
98
100
  end
99
101
 
100
102
  private
@@ -112,18 +114,19 @@ module RightScale
112
114
  # action(Symbol):: Serialization action: :dump or :load
113
115
  # packet(Object|String):: Object or serialized data on which action is to be performed
114
116
  # serializers(Array):: Serializers to apply in order
117
+ # id(String):: Optional identifier of source of data for use in determining who is the receiver
115
118
  #
116
119
  # === Return
117
120
  # (String|Object):: Result of serialization action
118
121
  #
119
122
  # === Raises
120
123
  # SerializationError:: If none of the serializers can perform the requested action
121
- def cascade_serializers(action, packet, serializers)
124
+ def cascade_serializers(action, packet, serializers, id = nil)
122
125
  errors = []
123
126
  serializers.map do |serializer|
124
127
  obj = nil
125
128
  begin
126
- obj = serializer.__send__(action, packet)
129
+ obj = serializer == SecureSerializer ? serializer.send(action, packet, id) : serializer.send(action, packet)
127
130
  rescue SecureSerializer::MissingCertificate, SecureSerializer::InvalidSignature => e
128
131
  errors << Log.format("Failed to #{action} with #{serializer.name}", e)
129
132
  rescue Exception => e
@@ -142,7 +145,13 @@ module RightScale
142
145
  # === Return
143
146
  # (Array):: Ordered serializers
144
147
  def order_serializers(packet)
145
- packet.getbyte(0) > 127 ? MSGPACK_FIRST_SERIALIZERS : JSON_FIRST_SERIALIZERS
148
+ # note the following code for getting the ascii value of the first byte is
149
+ # efficient for a large packet because it returns an enumerator for the
150
+ # internal byte array. it is actually more efficient than extracting the
151
+ # first character as a string and converting it to bytes.
152
+ # also, the following line works for both ruby 1.8 and ruby 1.9 since the
153
+ # definition of the bracket operator has changed.
154
+ packet.bytes.first > 127 ? MSGPACK_FIRST_SERIALIZERS : JSON_FIRST_SERIALIZERS
146
155
  end
147
156
 
148
157
  end # Serializer
data/right_agent.gemspec CHANGED
@@ -24,8 +24,8 @@ require 'rubygems'
24
24
 
25
25
  Gem::Specification.new do |spec|
26
26
  spec.name = 'right_agent'
27
- spec.version = '0.14.0'
28
- spec.date = '2012-10-01'
27
+ spec.version = '0.16.2'
28
+ spec.date = '2013-07-17'
29
29
  spec.authors = ['Lee Kirchhoff', 'Raphael Simon', 'Tony Spataro']
30
30
  spec.email = 'lee@rightscale.com'
31
31
  spec.homepage = 'https://github.com/rightscale/right_agent'
@@ -39,10 +39,9 @@ Gem::Specification.new do |spec|
39
39
 
40
40
  spec.add_dependency('right_support', ['>= 2.4.1', '< 3.0'])
41
41
  spec.add_dependency('right_amqp', '~> 0.4')
42
- spec.add_dependency('json', ['~> 1.4'])
42
+ spec.add_dependency('json', ['>= 1.4', '<= 1.7.6']) # json_create behavior change in 1.7.7
43
43
  spec.add_dependency('eventmachine', ['>= 0.12.10', '< 2.0'])
44
- spec.add_dependency('right_popen', '~> 1.0.11')
45
- spec.add_dependency('msgpack', '0.4.4')
44
+ spec.add_dependency('msgpack', ['>= 0.4.4', '< 0.6'])
46
45
  spec.add_dependency('net-ssh', '~> 2.0')
47
46
 
48
47
  if spec.platform.to_s =~ /mswin|mingw/
data/spec/agent_spec.rb CHANGED
@@ -426,9 +426,9 @@ describe RightScale::Agent do
426
426
  end
427
427
  end
428
428
 
429
- it "should publish result from dispatched request to response queue" do
429
+ it "should publish result from dispatched request to request reply_to" do
430
430
  run_in_em do
431
- request = RightScale::Request.new("/foo/bar", "payload")
431
+ request = RightScale::Request.new("/foo/bar", "payload", {:reply_to => "response"})
432
432
  @broker.should_receive(:subscribe).with(hsh(:name => @identity), nil, Hash, Proc).
433
433
  and_return(@broker_ids).and_yield(@broker_id, request, @header).once
434
434
  result = flexmock("result")
@@ -0,0 +1,330 @@
1
+ #
2
+ # Copyright (c) 2013 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
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
24
+
25
+ describe RightScale::AgentTagManager do
26
+
27
+ include FlexMock::ArgumentTypes
28
+
29
+ before(:each) do
30
+ @log = flexmock(RightScale::Log)
31
+ @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
32
+ @identity = "rs-agent-0-0"
33
+ @agent = flexmock("agent", :identity => @identity)
34
+ @agent_id1 = "rs-agent-1-1"
35
+ @agent_id2 = "rs-agent-2-2"
36
+ @agent_ids = [@agent_id2, @agent_id3]
37
+ @manager = RightScale::AgentTagManager.instance
38
+ @manager.agent = @agent
39
+ @request = flexmock("request", :run => true)
40
+ @request.should_receive(:callback).and_yield("result").by_default
41
+ @request.should_receive(:errback).by_default
42
+ @request.should_receive(:raw_response).and_return("raw response").by_default
43
+ @idempotent_request = flexmock(RightScale::IdempotentRequest)
44
+ @tag = "some:tag=value"
45
+ @tag1 = "other:tag=value"
46
+ @tags = [@tag, @tag1]
47
+ @result = nil
48
+ end
49
+
50
+ context :tags do
51
+
52
+ before(:each) do
53
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
54
+ {:agent_identity => @identity, :agent_ids => [@identity]},
55
+ {}).and_return(@request).once.by_default
56
+ end
57
+
58
+ it "retrieves current agent tags" do
59
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}}).once
60
+ @manager.tags { |r| @result = r }
61
+ @result.should == [@tag]
62
+ end
63
+
64
+ it "returns empty array when there is no results hash" do
65
+ @request.should_receive(:callback).and_yield({}).once
66
+ @manager.tags { |r| @result = r }
67
+ @result.should == []
68
+ end
69
+
70
+ it "returns the raw result when result is not a hash" do
71
+ @request.should_receive(:callback).and_yield("result").once
72
+ @manager.tags { |r| @result = r }
73
+ @result.should == "result"
74
+ end
75
+
76
+ it "returns raw result when :raw option specified" do
77
+ @request.should_receive(:callback).and_yield("result").once
78
+ @request.should_receive(:raw_response).and_return("raw response").once
79
+ @manager.tags(:raw => true) { |r| @result = r }
80
+ @result.should == "raw response"
81
+ end
82
+
83
+ it "forwards timeout option" do
84
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
85
+ {:agent_identity => @identity, :agent_ids => [@identity]},
86
+ {:timeout => 9}).and_return(@request).once
87
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}}).once
88
+ @manager.tags(:timeout => 9) { |r| @result = r }
89
+ @result.should == [@tag]
90
+ end
91
+
92
+ it "yields error result and logs error" do
93
+ @log.should_receive(:error).with(/Failed to query tags/).once
94
+ @request.should_receive(:errback).and_yield("error").once
95
+ @request.should_receive(:callback).once
96
+ @manager.tags { |r| @result = r }
97
+ @result.should == "error"
98
+ end
99
+ end
100
+
101
+ context :query_tags do
102
+
103
+ it "queries for agents having individual tag" do
104
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
105
+ {:agent_identity => @identity, :tags => [@tag]},
106
+ {}).and_return(@request).once
107
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}, @agent_id1 => {"tags" => [@tag]}}).once
108
+ @manager.query_tags(@tag) { |r| @result = r }
109
+ @result.should == {@identity => {"tags" => [@tag]}, @agent_id1 => {"tags" => [@tag]}}
110
+ end
111
+
112
+ it "queries for agents having multiple tags" do
113
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
114
+ {:agent_identity => @identity, :tags => @tags},
115
+ {}).and_return(@request).once
116
+ @request.should_receive(:callback).and_yield({@agent_id1 => {"tags" => @tags}}).once
117
+ @manager.query_tags(@tags) { |r| @result = r }
118
+ @result.should == {@agent_id1 => {"tags" => @tags}}
119
+ end
120
+
121
+ it "forwards options" do
122
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
123
+ {:agent_identity => @identity, :tags => @tags},
124
+ {:timeout => 9}).and_return(@request).once
125
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}}).once
126
+ @request.should_receive(:raw_response).and_return("raw response").once
127
+ @manager.query_tags(@tags, :raw => true, :timeout => 9) { |r| @result = r }
128
+ @result.should == "raw response"
129
+ end
130
+
131
+ it "yields error result and logs error" do
132
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
133
+ {:agent_identity => @identity, :tags => [@tag]},
134
+ {}).and_return(@request).once
135
+ @log.should_receive(:error).with(/Failed to query tags/).once
136
+ @request.should_receive(:errback).and_yield("error").once
137
+ @request.should_receive(:callback).once
138
+ @manager.query_tags(@tag) { |r| @result = r }
139
+ @result.should == "error"
140
+ end
141
+ end
142
+
143
+ context :query_tags_raw do
144
+
145
+ before(:each) do
146
+ @request.should_receive(:raw_response).and_return("raw response").once
147
+ end
148
+
149
+ it "always yields raw response" do
150
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
151
+ {:agent_identity => @identity, :tags => [@tag]},
152
+ {}).and_return(@request).once
153
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}, @agent_id1 => {"tags" => [@tag]}}).once
154
+ @manager.query_tags_raw(@tag) { |r| @result = r }
155
+ @result.should == "raw response"
156
+ end
157
+
158
+ it "queries for agents having individual tag and always yields raw response" do
159
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
160
+ {:agent_identity => @identity, :tags => [@tag]},
161
+ {}).and_return(@request).once
162
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}, @agent_id1 => {"tags" => [@tag]}}).once
163
+ @manager.query_tags_raw(@tag) { |r| @result = r }
164
+ @result.should == "raw response"
165
+ end
166
+
167
+ it "queries for agents having multiple tags always yields raw response" do
168
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
169
+ {:agent_identity => @identity, :tags => @tags},
170
+ {}).and_return(@request).once
171
+ @request.should_receive(:callback).and_yield({@agent_id1 => {"tags" => @tags}}).once
172
+ @manager.query_tags_raw(@tags) { |r| @result = r }
173
+ @result.should == "raw response"
174
+ end
175
+
176
+ it "queries for selected agents" do
177
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
178
+ {:agent_identity => @identity, :agent_ids => @agent_ids, :tags => @tags},
179
+ {}).and_return(@request).once
180
+ @request.should_receive(:callback).and_yield({@agent_id1 => {"tags" => @tags}}).once
181
+ @manager.query_tags_raw(@tags, @agent_ids) { |r| @result = r }
182
+ @result.should == "raw response"
183
+ end
184
+
185
+ it "forwards timeout option" do
186
+ @idempotent_request.should_receive(:new).with("/mapper/query_tags",
187
+ {:agent_identity => @identity, :tags => @tags},
188
+ {:timeout => 9}).and_return(@request).once
189
+ @request.should_receive(:callback).and_yield({@identity => {"tags" => [@tag]}}).once
190
+ @manager.query_tags_raw(@tags, nil, :timeout => 9) { |r| @result = r }
191
+ @result.should == "raw response"
192
+ end
193
+ end
194
+
195
+ context :add_tags do
196
+
197
+ before(:each) do
198
+ @request.should_receive(:raw_response).and_return("raw response").once
199
+ @agent.should_receive(:tags).and_return([]).once.by_default
200
+ @agent.should_receive(:tags=).once.by_default
201
+ end
202
+
203
+ it "adds individual tag to agent" do
204
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
205
+ {:new_tags => [@tag], :obsolete_tags => []}).and_return(@request).once
206
+ @manager.add_tags(@tag).should be_true
207
+ end
208
+
209
+ it "adds multiple tags to agent" do
210
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
211
+ {:new_tags => @tags, :obsolete_tags => []}).and_return(@request).once
212
+ @manager.add_tags(@tags).should be_true
213
+ end
214
+
215
+ it "optionally yields raw response" do
216
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
217
+ {:new_tags => @tags, :obsolete_tags => []}).and_return(@request).once
218
+ @request.should_receive(:callback).and_yield("result").once
219
+ @manager.add_tags(@tags) { |r| @result = r }
220
+ @result.should == "raw response"
221
+ end
222
+
223
+ it "updates local tags" do
224
+ @agent.should_receive(:tags).and_return([@tag1]).once
225
+ @agent.should_receive(:tags=).should_receive([@tag]).once
226
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
227
+ {:new_tags => [@tag], :obsolete_tags => []}).and_return(@request).once
228
+ @manager.add_tags(@tag).should be_true
229
+ end
230
+ end
231
+
232
+ context :remove_tags do
233
+
234
+ before(:each) do
235
+ @request.should_receive(:raw_response).and_return("raw response").once
236
+ @agent.should_receive(:tags).and_return([@tag]).once.by_default
237
+ @agent.should_receive(:tags=).once.by_default
238
+ end
239
+
240
+ it "removes individual tag to agent" do
241
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
242
+ {:new_tags => [], :obsolete_tags => [@tag]}).and_return(@request).once
243
+ @manager.remove_tags(@tag).should be_true
244
+ end
245
+
246
+ it "removes multiple tags to agent" do
247
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
248
+ {:new_tags => [], :obsolete_tags => @tags}).and_return(@request).once
249
+ @manager.remove_tags(@tags).should be_true
250
+ end
251
+
252
+ it "optionally yields raw response" do
253
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
254
+ {:new_tags => [], :obsolete_tags => @tags}).and_return(@request).once
255
+ @request.should_receive(:callback).and_yield("result").once
256
+ @manager.remove_tags(@tags) { |r| @result = r }
257
+ @result.should == "raw response"
258
+ end
259
+
260
+ it "updates local tags" do
261
+ @agent.should_receive(:tags).and_return([]).once
262
+ @agent.should_receive(:tags=).should_receive([@tag]).once
263
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
264
+ {:new_tags => [], :obsolete_tags => [@tag]}).and_return(@request).once
265
+ @manager.remove_tags(@tag).should be_true
266
+ end
267
+ end
268
+
269
+ context :update_tags do
270
+
271
+ before(:each) do
272
+ @agent.should_receive(:tags).and_return([]).once.by_default
273
+ end
274
+
275
+ it "checks that agent has been set" do
276
+ @manager.agent = nil
277
+ @agent.should_receive(:tags).never
278
+ lambda { @manager.update_tags([@tag], [@tag1]) }.should raise_error(ArgumentError, "Must set agent= before using tag manager")
279
+ end
280
+
281
+ it "adds and removes tags for agent" do
282
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
283
+ {:new_tags => [@tag], :obsolete_tags => [@tag1]}).and_return(@request).once
284
+ @agent.should_receive(:tags=).never
285
+ @manager.update_tags([@tag], [@tag1]).should be_true
286
+ end
287
+
288
+ it "yields raw response if block given" do
289
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
290
+ {:new_tags => [@tag], :obsolete_tags => [@tag1]}).and_return(@request).once
291
+ @request.should_receive(:raw_response).and_return("raw response").once
292
+ @agent.should_receive(:tags=).once
293
+ @manager.update_tags([@tag], [@tag1]) { |r| @result = r }
294
+ @result.should == "raw response"
295
+ end
296
+
297
+ it "updates local tags if block given and successful" do
298
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
299
+ {:new_tags => [@tag], :obsolete_tags => [@tag1]}).and_return(@request).once
300
+ @request.should_receive(:raw_response).and_return("raw response").once
301
+ @agent.should_receive(:tags=).with([@tag]).once
302
+ @manager.update_tags([@tag], [@tag1]) { |r| @result = r }
303
+ @result.should == "raw response"
304
+ end
305
+
306
+ it "yields error result and does not update local tags" do
307
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
308
+ {:new_tags => [@tag], :obsolete_tags => [@tag1]}).and_return(@request).once
309
+ @request.should_receive(:raw_response).and_return("error").once
310
+ @request.should_receive(:errback).and_yield("error").once
311
+ @request.should_receive(:callback).once
312
+ @agent.should_receive(:tags=).never
313
+ @manager.update_tags([@tag], [@tag1]) { |r| @result = r }
314
+ @result.should == "error"
315
+ end
316
+ end
317
+
318
+ context :clear do
319
+
320
+ it "clears all agent tags" do
321
+ @request.should_receive(:raw_response).and_return("raw response").once
322
+ @agent.should_receive(:tags).and_return(@tags).twice
323
+ @agent.should_receive(:tags=).with([]).once
324
+ @idempotent_request.should_receive(:new).with("/mapper/update_tags",
325
+ {:new_tags => [], :obsolete_tags => @tags}).and_return(@request).once
326
+ @manager.clear { |r| @result = r }
327
+ @result.should == "raw response"
328
+ end
329
+ end
330
+ end