Merge lp:~allenap/maas/more-better-things--bug-1389007 into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~allenap/maas/more-better-things--bug-1389007
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 7895 lines (+6361/-339)
29 files modified
LICENSE.Twisted (+65/-0)
docs/development/rpc.rst (+12/-8)
scripts/ampclient.py (+1/-1)
src/maasserver/clusterrpc/power.py (+1/-1)
src/maasserver/clusterrpc/utils.py (+1/-1)
src/maasserver/models/node.py (+2/-2)
src/maasserver/models/tests/test_node.py (+6/-4)
src/maasserver/rpc/regionservice.py (+3/-3)
src/maasserver/rpc/testing/fixtures.py (+2/-2)
src/maasserver/rpc/tests/test_regionservice.py (+5/-5)
src/provisioningserver/pserv_services/dhcp_probe_service.py (+1/-1)
src/provisioningserver/rpc/amp32.py (+2646/-0)
src/provisioningserver/rpc/arguments.py (+11/-11)
src/provisioningserver/rpc/cluster.py (+119/-117)
src/provisioningserver/rpc/clusterservice.py (+4/-4)
src/provisioningserver/rpc/common.py (+17/-17)
src/provisioningserver/rpc/monitors.py (+5/-3)
src/provisioningserver/rpc/region.py (+127/-127)
src/provisioningserver/rpc/testing/__init__.py (+10/-8)
src/provisioningserver/rpc/testing/tls.py (+1/-1)
src/provisioningserver/rpc/tests/test_amp32.py (+3270/-0)
src/provisioningserver/rpc/tests/test_arguments.py (+6/-4)
src/provisioningserver/rpc/tests/test_clusterservice.py (+7/-7)
src/provisioningserver/rpc/tests/test_common.py (+5/-3)
src/provisioningserver/rpc/tests/test_docs.py (+3/-3)
src/provisioningserver/rpc/tests/test_monitors.py (+6/-4)
src/provisioningserver/utils/__init__.py (+1/-1)
src/provisioningserver/utils/shell.py (+11/-0)
src/provisioningserver/utils/tests/test_shell.py (+13/-1)
To merge this branch: bzr merge lp:~allenap/maas/more-better-things--bug-1389007
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Christian Reis (community) Needs Information
Review via email: mp+241467@code.launchpad.net

Commit message

Adapt AMP to use 32-bit length prefixes for on-wire serialization.

This allows us to have much larger arguments and responses over RPC, though it's limited to 2MiB for now, up from 64kiB.

Description of the change

This is a *big* branch, I know. There's no good way of doing this in stages. I copied the AMP code and tests from Twisted, adapted them for 32-bit-length-prefixed strings, then fixed up the code to pass muster in MAAS's coding style, investigated a test isolation failure, and then also adapted the tests to run using MAASTestCase instead of Twisted's Trial code.

I think the best way to verify/test/review this code is in CI and use. I've not changed the behaviour of the AMP code except the switch from 16 to 32 bit, so that can be reviewed solely for style, rather than substance. I did have to write AMPTestCase for the tests, but again most of the behaviour is intact.

The bulk of the rest is mechanical, which can be skipped through fairly quickly.

Please review this?

To post a comment you must log in.
Revision history for this message
Christian Reis (kiko) wrote :

Hmm, do we really want to copy the code wholesale instead of changing AMT upstream to be fixed, parametrizable or monkey-patchable?

review: Needs Information
Revision history for this message
Christian Reis (kiko) wrote :

AMT on my mind, I obviously menat AMP.

Revision history for this message
Gavin Panella (allenap) wrote :

There's no way we can change upstream AMP to 32-bit prefixes; it's an incompatible change. We could propose amp32 as an additional protocol, or we could write negotiation code that starts on 16-bit and switches to 32-bit if supported.

The former would be fine: it's something we can try though it may be rejected, but it doesn't block this branch.

The latter is, at a guess, several days work instead of ~6 hours that went into this. I'm okay to do it, but I guess we've got more important things to do.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Not a full review yet, but some notes:

.

Typo: "This contain the".

.

If you're going to include Twisted code here, it may not be valid to say "See LICENSE for details." You may need to include their licence file under a recognisable name, and make the copyright header refer to it as such. Also, if you're creating a derived work with substantial changes, I guess it needs our copyright notice as well.

.

AMPTestCase (not AMP32TestCase?) and everything in it needs docstrings.

Although to be honest, I'd prefer not to have yet another test-case class in the first place! I see very little in there that actually needs to be in a class: shortDescription might be generic or it could be in a fixture. setUp *is* a fixture. getLoggedFailures could be in MAASTestCase. And assertWarns is a generic matcher disguised as a specific helper which makes non-transparent decisions about whether the test should continue after failure.

.

In AMPTestCase.assertWarns, if you know you've got a list of one item and you want the item, prefer extracting it using pattern-matching ("[item] = mylist") over indexing ("mylist[0]").

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

More notes:

.

It'd be worth noting in the documentation (or did I miss it?) that this is a modified version of the Twisted implementation, and that it does not change the on-the-wire protocol

.

I'd stick the module-level verifyClass invocations in tests.

.

I wasn't sure about AMPTestCase, but the AMPTest class almost certainly should say AMP32 instead of plain AMP.

.

For the bulk of the code, I can only assume that it's as good as the code we've been relying on anyway. So I'm voting Approve for the code as such — though I'll leave it to others (including Jenkins) to figure out how well the landing would fit into the grand plan.

review: Approve
3372. By Gavin Panella

Fix typo.

3373. By Gavin Panella

Include Twisted's LICENSE file which is references by amp32 and its tests.

3374. By Gavin Panella

shortDescription() is no longer needed.

3375. By Gavin Panella

Rename AMPTestCase to AMP32TestCase.

3376. By Gavin Panella

Docstrings for AMP32TestCase.

3377. By Gavin Panella

Use the pattern match, Luke.

Revision history for this message
Gavin Panella (allenap) wrote :

> Typo: "This contain the".

Well spotted, thanks. Fixed.

>
> .
>
> If you're going to include Twisted code here, it may not be valid to
> say "See LICENSE for details." You may need to include their licence
> file under a recognisable name, and make the copyright header refer to
> it as such. Also, if you're creating a derived work with substantial
> changes, I guess it needs our copyright notice as well.

I've pulled Twisted's LICENSE file in now, as LICENSE.Twisted, and
updated the headers in those files. Canonical is already listed in
LICENSE.Twisted so I think we can probably leave it as is.

>
> .
>
> AMPTestCase (not AMP32TestCase?) and everything in it needs
> docstrings.
>
> Although to be honest, I'd prefer not to have yet another test-case
> class in the first place! I see very little in there that actually
> needs to be in a class: shortDescription might be generic or it could
> be in a fixture. setUp *is* a fixture. getLoggedFailures could be in
> MAASTestCase. And assertWarns is a generic matcher disguised as a
> specific helper which makes non- transparent decisions about whether
> the test should continue after failure.

shortDescription() is a unittest method that gets called before the test
runs, but now that I've rebased on MAASTestCase instead of Trial's
TestCase it can go.

I've added docstrings which explain the other functions, and justify
their continued existence. I could spend time on getting rid of
AMP32TestCase, but the pay off doesn't seem worth it right now. I'm
biased, but I think it's already an improvement over the tests that work
with Trial.

>
> .
>
> In AMPTestCase.assertWarns, if you know you've got a list of one item
> and you want the item, prefer extracting it using pattern-matching
> ("[item] = mylist") over indexing ("mylist[0]").

Done.

Thanks!

3378. By Gavin Panella

Update amp32's docstring to draw attention to the differences from Twisted's amp.

3379. By Gavin Panella

Draw more attention to the difference between amp and amp32 in the docs.

3380. By Gavin Panella

Move module-level interface tests into a test case.

3381. By Gavin Panella

Rename AMPTest to AMP32Test.

Revision history for this message
Gavin Panella (allenap) wrote :

> It'd be worth noting in the documentation (or did I miss it?) that
> this is a modified version of the Twisted implementation, and that it
> does not change the on-the-wire protocol

Done.

>
> .
>
> I'd stick the module-level verifyClass invocations in tests.

Done.

>
> .
>
> I wasn't sure about AMPTestCase, but the AMPTest class almost
> certainly should say AMP32 instead of plain AMP.

Done.

>
> .
>
> For the bulk of the code, I can only assume that it's as good as the
> code we've been relying on anyway. So I'm voting Approve for the code
> as such — though I'll leave it to others (including Jenkins) to figure
> out how well the landing would fit into the grand plan.

Yeah, it's the same code, modified to our style conventions. The tests
have been modified to use testtools instead of Trial, but otherwise
they're the same.

Thanks again!

Revision history for this message
Christian Reis (kiko) wrote :

I think this needs a discussion upstream in order to establish the best path forward.

If upstream is willing to accept an upgrade-to-32bit patch, then we should invest energy into that because long-term it's much more sustainable for us. If they have another suggested solution we should use it.

We should only really take this fork in if there's really no other viable solution, which should be quite unlikely.

Revision history for this message
Gavin Panella (allenap) wrote :

> I think this needs a discussion upstream in order to establish the
> best path forward.
>
> If upstream is willing to accept an upgrade-to-32bit patch, then we
> should invest energy into that because long-term it's much more
> sustainable for us. If they have another suggested solution we should
> use it.
>
> We should only really take this fork in if there's really no other
> viable solution, which should be quite unlikely.

I think it's the right and proper thing to do to talk to the Twisted
project, and see if this can go upstream.

Twisted is a community project; we or I would likely be the de facto
maintainers.

Sustainably, my guess is that we're not in for a lot of work in either
case. AMP in Twisted is essentially done. This branch doesn't add new
features, just a change to an on-the-wire detail.

Even so, being upstream would give others the chance to use the code,
and give greater visibility to bugs and security issues.

Anecdotally, though from my own experience of the Twisted development
process, I can say that it's onerous to get a patch reviewed and landed.

If we get it upstream it could be a long time before that release makes
it back into Ubuntu. It's unlikely to get into the current LTS. We'd
need to carry this code anyway, for a while.

In all, I think we can make it a goal to get this upstream, but I don't
think we should block on it, except just to check there isn't a
ready-made alternative.

Revision history for this message
Christian Reis (kiko) wrote :

Yes, if we do agree that we want this patch and that it can go upstream,
then we should carry this in-tree until it is in a released version.

Please let us know what the immediate reaction upstream is and we'll
figure out what to do; I assume as you do that there's no obvious other
solution, but it would be nice to be surprised.
--
Christian Robottom Reis | [+1] 612 888 4935 | http://launchpad.net/~kiko
Canonical VP Hyperscale | [+55 16] 9 9112 6430 | http://async.com.br/~kiko

Revision history for this message
Gavin Panella (allenap) wrote :

The conversation with upstream developer exarkun [1] has put me off this branch. Not in a never-ever sense, but in the sense that I think this 64k limit is actually making us think. We can actually improve the power-poller (the trigger for this branch) by working within this limitation.

[1] http://twistedmatrix.com/pipermail/twisted-python/2014-November/028941.html

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Friday 14 Nov 2014 12:05:28 you wrote:
> The conversation with upstream developer exarkun [1] has put me off this
> branch. Not in a never-ever sense, but in the sense that I think this 64k
> limit is actually making us think. We can actually improve the power-poller
> (the trigger for this branch) by working within this limitation.
>
> [1]
> http://twistedmatrix.com/pipermail/twisted-python/2014-November/028941.html

I saw your mention of a "priority queue". I'm not sure we need that sort of
solution; we talked today about only querying the oldest 10 (say) power
statuses at a time. This will fit easily in the message size.

Revision history for this message
Gavin Panella (allenap) wrote :

> I saw your mention of a "priority queue". I'm not sure we need that
> sort of solution; we talked today about only querying the oldest 10
> (say) power statuses at a time. This will fit easily in the message
> size.

I was using big words to mean just that :)

Revision history for this message
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

3381. By Gavin Panella

Rename AMPTest to AMP32Test.

3380. By Gavin Panella

Move module-level interface tests into a test case.

3379. By Gavin Panella

Draw more attention to the difference between amp and amp32 in the docs.

3378. By Gavin Panella

Update amp32's docstring to draw attention to the differences from Twisted's amp.

3377. By Gavin Panella

Use the pattern match, Luke.

3376. By Gavin Panella

Docstrings for AMP32TestCase.

3375. By Gavin Panella

Rename AMPTestCase to AMP32TestCase.

3374. By Gavin Panella

shortDescription() is no longer needed.

3373. By Gavin Panella

Include Twisted's LICENSE file which is references by amp32 and its tests.

3372. By Gavin Panella

Fix typo.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'LICENSE.Twisted'
--- LICENSE.Twisted 1970-01-01 00:00:00 +0000
+++ LICENSE.Twisted 2014-11-12 12:20:32 +0000
@@ -0,0 +1,65 @@
1Copyright (c) 2001-2014
2Allen Short
3Andy Gayton
4Andrew Bennetts
5Antoine Pitrou
6Apple Computer, Inc.
7Ashwini Oruganti
8Benjamin Bruheim
9Bob Ippolito
10Canonical Limited
11Christopher Armstrong
12David Reid
13Donovan Preston
14Eric Mangold
15Eyal Lotem
16Google Inc.
17Hybrid Logic Ltd.
18Hynek Schlawack
19Itamar Turner-Trauring
20James Knight
21Jason A. Mobarak
22Jean-Paul Calderone
23Jessica McKellar
24Jonathan Jacobs
25Jonathan Lange
26Jonathan D. Simms
27Jürgen Hermann
28Julian Berman
29Kevin Horn
30Kevin Turner
31Laurens Van Houtven
32Mary Gardiner
33Matthew Lefkowitz
34Massachusetts Institute of Technology
35Moshe Zadka
36Paul Swartz
37Pavel Pergamenshchik
38Ralph Meijer
39Richard Wall
40Sean Riley
41Software Freedom Conservancy
42Travis B. Hartwell
43Thijs Triemstra
44Thomas Herve
45Timothy Allen
46Tom Prince
47
48Permission is hereby granted, free of charge, to any person obtaining
49a copy of this software and associated documentation files (the
50"Software"), to deal in the Software without restriction, including
51without limitation the rights to use, copy, modify, merge, publish,
52distribute, sublicense, and/or sell copies of the Software, and to
53permit persons to whom the Software is furnished to do so, subject to
54the following conditions:
55
56The above copyright notice and this permission notice shall be
57included in all copies or substantial portions of the Software.
58
59THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
60EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
62NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
63LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
64OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
65WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
066
=== modified file 'docs/development/rpc.rst'
--- docs/development/rpc.rst 2014-08-21 20:10:47 +0000
+++ docs/development/rpc.rst 2014-11-12 12:20:32 +0000
@@ -5,7 +5,11 @@
55
6MAAS contains an RPC mechanism such that every process in the region is6MAAS contains an RPC mechanism such that every process in the region is
7connected to every process in the cluster (strictly, every pserv7connected to every process in the cluster (strictly, every pserv
8process). It's based on AMP_, specifically `Twisted's implementation`_.8process). It's based on AMP_, specifically `Twisted's implementation`_,
9but MAAS incorporates an extended version that can transmit much larger
10messages by using 32-bit length prefixes in place of 16-bit. This makes
11it on-the-wire incompatible with standard AMP_ implementations, but its
12essential behaviour is identical.
913
10.. _AMP:14.. _AMP:
11 http://amp-protocol.net/15 http://amp-protocol.net/
@@ -17,22 +21,22 @@
17Where do I start?21Where do I start?
18-----------------22-----------------
1923
20Start in the :py:mod:`provisioningserver.rpc` package. The first two files to24Start in the :py:mod:`provisioningserver.rpc` package. The first two
21look at are ``cluster.py`` and ``region.py``. This contain the25files to look at are ``cluster.py`` and ``region.py``. This contains the
22declarations of what commands are available on clusters and regions26declarations of what commands are available on clusters and regions
23respectively.27respectively.
2428
25A new command could be declared like so::29A new command could be declared like so::
2630
27 from twisted.protocols import amp31 from provisioningserver.rpc import amp32
2832
29 class EatCheez(amp.Command):33 class EatCheez(amp32.Command):
30 arguments = [34 arguments = [
31 (b"name", amp.Unicode()),35 (b"name", amp32.Unicode()),
32 (b"origin", amp.Unicode()),36 (b"origin", amp32.Unicode()),
33 ]37 ]
34 response = [38 response = [
35 (b"rating", amp.Integer()),39 (b"rating", amp32.Integer()),
36 ]40 ]
3741
38It's also possible to map exceptions across the wire using an ``errors``42It's also possible to map exceptions across the wire using an ``errors``
3943
=== modified file 'scripts/ampclient.py'
--- scripts/ampclient.py 2014-01-31 16:15:00 +0000
+++ scripts/ampclient.py 2014-11-12 12:20:32 +0000
@@ -16,6 +16,7 @@
1616
17import sys17import sys
1818
19from provisioningserver.rpc.amp32 import AMP
19from provisioningserver.rpc.cluster import ListBootImages20from provisioningserver.rpc.cluster import ListBootImages
20from provisioningserver.rpc.region import ReportBootImages21from provisioningserver.rpc.region import ReportBootImages
21from twisted.internet import reactor22from twisted.internet import reactor
@@ -23,7 +24,6 @@
23 connectProtocol,24 connectProtocol,
24 TCP4ClientEndpoint,25 TCP4ClientEndpoint,
25 )26 )
26from twisted.protocols.amp import AMP
2727
2828
29def callRemote(command, port, **kwargs):29def callRemote(command, port, **kwargs):
3030
=== modified file 'src/maasserver/clusterrpc/power.py'
--- src/maasserver/clusterrpc/power.py 2014-09-16 21:00:33 +0000
+++ src/maasserver/clusterrpc/power.py 2014-11-12 12:20:32 +0000
@@ -38,7 +38,7 @@
38 Nodes can be in any cluster; the power calls will be directed to their38 Nodes can be in any cluster; the power calls will be directed to their
39 owning cluster.39 owning cluster.
4040
41 :param command: The `amp.Command` to call.41 :param command: The `amp32.Command` to call.
42 :param nodes: A sequence of ``(system-id, hostname, cluster-uuid,42 :param nodes: A sequence of ``(system-id, hostname, cluster-uuid,
43 power-info)`` tuples.43 power-info)`` tuples.
44 :returns: A mapping of each node's system ID to a44 :returns: A mapping of each node's system ID to a
4545
=== modified file 'src/maasserver/clusterrpc/utils.py'
--- src/maasserver/clusterrpc/utils.py 2014-10-08 09:43:51 +0000
+++ src/maasserver/clusterrpc/utils.py 2014-11-12 12:20:32 +0000
@@ -38,7 +38,7 @@
3838
39 :param nodegroups: The :class:`NodeGroup`s on which to make the RPC39 :param nodegroups: The :class:`NodeGroup`s on which to make the RPC
40 call. If None, defaults to all :class:`NodeGroup`s.40 call. If None, defaults to all :class:`NodeGroup`s.
41 :param command: An :class:`amp.Command` to call on the clusters.41 :param command: An :class:`amp32.Command` to call on the clusters.
42 :param ignore_errors: If True, errors encountered whilst calling42 :param ignore_errors: If True, errors encountered whilst calling
43 `command` on the clusters won't raise an exception.43 `command` on the clusters won't raise an exception.
44 :return: A generator of results, i.e. the dicts returned by the RPC44 :return: A generator of results, i.e. the dicts returned by the RPC
4545
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2014-11-10 03:22:59 +0000
+++ src/maasserver/models/node.py 2014-11-12 12:20:32 +0000
@@ -111,6 +111,7 @@
111from piston.models import Token111from piston.models import Token
112from provisioningserver.logger import get_maas_logger112from provisioningserver.logger import get_maas_logger
113from provisioningserver.power.poweraction import UnknownPowerType113from provisioningserver.power.poweraction import UnknownPowerType
114from provisioningserver.rpc import amp32
114from provisioningserver.rpc.cluster import (115from provisioningserver.rpc.cluster import (
115 CancelMonitor,116 CancelMonitor,
116 StartMonitors,117 StartMonitors,
@@ -118,7 +119,6 @@
118from provisioningserver.rpc.power import QUERY_POWER_TYPES119from provisioningserver.rpc.power import QUERY_POWER_TYPES
119from provisioningserver.utils.enum import map_enum_reverse120from provisioningserver.utils.enum import map_enum_reverse
120from provisioningserver.utils.twisted import asynchronous121from provisioningserver.utils.twisted import asynchronous
121from twisted.protocols import amp
122122
123123
124maaslog = get_maas_logger("node")124maaslog = get_maas_logger("node")
@@ -584,7 +584,7 @@
584 'node_status': self.status,584 'node_status': self.status,
585 'timeout': timeout,585 'timeout': timeout,
586 }586 }
587 deadline = datetime.now(tz=amp.utc) + timedelta(seconds=timeout)587 deadline = datetime.now(tz=amp32.utc) + timedelta(seconds=timeout)
588 monitors = [{588 monitors = [{
589 'deadline': deadline,589 'deadline': deadline,
590 'id': self.system_id,590 'id': self.system_id,
591591
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2014-11-10 03:22:53 +0000
+++ src/maasserver/models/tests/test_node.py 2014-11-12 12:20:32 +0000
@@ -101,7 +101,10 @@
101 )101 )
102from provisioningserver.power.poweraction import UnknownPowerType102from provisioningserver.power.poweraction import UnknownPowerType
103from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS103from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
104from provisioningserver.rpc import cluster as cluster_module104from provisioningserver.rpc import (
105 amp32,
106 cluster as cluster_module,
107 )
105from provisioningserver.rpc.cluster import StartMonitors108from provisioningserver.rpc.cluster import StartMonitors
106from provisioningserver.rpc.exceptions import NoConnectionsAvailable109from provisioningserver.rpc.exceptions import NoConnectionsAvailable
107from provisioningserver.rpc.power import QUERY_POWER_TYPES110from provisioningserver.rpc.power import QUERY_POWER_TYPES
@@ -114,7 +117,6 @@
114 )117 )
115from twisted.internet import defer118from twisted.internet import defer
116from twisted.internet.defer import Deferred119from twisted.internet.defer import Deferred
117from twisted.protocols import amp
118from twisted.python.failure import Failure120from twisted.python.failure import Failure
119121
120122
@@ -2123,7 +2125,7 @@
21232125
2124 def test__start_transition_monitor_starts_monitor(self):2126 def test__start_transition_monitor_starts_monitor(self):
2125 rpc_fixture = self.prepare_rpc()2127 rpc_fixture = self.prepare_rpc()
2126 now = datetime.now(tz=amp.utc)2128 now = datetime.now(tz=amp32.utc)
2127 self.patch_datetime_now(now)2129 self.patch_datetime_now(now)
2128 node = factory.make_Node()2130 node = factory.make_Node()
2129 cluster = rpc_fixture.makeCluster(node.nodegroup, StartMonitors)2131 cluster = rpc_fixture.makeCluster(node.nodegroup, StartMonitors)
@@ -2142,7 +2144,7 @@
2142 )2144 )
21432145
2144 def test__start_transition_monitor_copes_with_timeouterror(self):2146 def test__start_transition_monitor_copes_with_timeouterror(self):
2145 now = datetime.now(tz=amp.utc)2147 now = datetime.now(tz=amp32.utc)
2146 self.patch_datetime_now(now)2148 self.patch_datetime_now(now)
2147 node = factory.make_Node()2149 node = factory.make_Node()
2148 mock_client = Mock()2150 mock_client = Mock()
21492151
=== modified file 'src/maasserver/rpc/regionservice.py'
--- src/maasserver/rpc/regionservice.py 2014-11-10 15:11:58 +0000
+++ src/maasserver/rpc/regionservice.py 2014-11-12 12:20:32 +0000
@@ -57,6 +57,7 @@
57from maasserver.utils.async import transactional57from maasserver.utils.async import transactional
58from netaddr import IPAddress58from netaddr import IPAddress
59from provisioningserver.rpc import (59from provisioningserver.rpc import (
60 amp32,
60 cluster,61 cluster,
61 common,62 common,
62 exceptions,63 exceptions,
@@ -86,7 +87,6 @@
86from twisted.internet.error import ConnectionClosed87from twisted.internet.error import ConnectionClosed
87from twisted.internet.protocol import Factory88from twisted.internet.protocol import Factory
88from twisted.internet.threads import deferToThread89from twisted.internet.threads import deferToThread
89from twisted.protocols import amp
90from twisted.python import log90from twisted.python import log
91from zope.interface import implementer91from zope.interface import implementer
9292
@@ -151,11 +151,11 @@
151 """151 """
152 return deferToThread(leases.update_leases, uuid, mappings)152 return deferToThread(leases.update_leases, uuid, mappings)
153153
154 @amp.StartTLS.responder154 @amp32.StartTLS.responder
155 def get_tls_parameters(self):155 def get_tls_parameters(self):
156 """get_tls_parameters()156 """get_tls_parameters()
157157
158 Implementation of :py:class:`~twisted.protocols.amp.StartTLS`.158 Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`.
159 """159 """
160 try:160 try:
161 from provisioningserver.rpc.testing import tls161 from provisioningserver.rpc.testing import tls
162162
=== modified file 'src/maasserver/rpc/testing/fixtures.py'
--- src/maasserver/rpc/testing/fixtures.py 2014-10-08 21:59:23 +0000
+++ src/maasserver/rpc/testing/fixtures.py 2014-11-12 12:20:32 +0000
@@ -191,7 +191,7 @@
191 def addCluster(self, protocol):191 def addCluster(self, protocol):
192 """Add a new stub cluster using the given `protocol`.192 """Add a new stub cluster using the given `protocol`.
193193
194 The `protocol` should be an instance of `amp.AMP`.194 The `protocol` should be an instance of `amp32.AMP`.
195195
196 :returns: py:class:`twisted.test.iosim.IOPump`196 :returns: py:class:`twisted.test.iosim.IOPump`
197 """197 """
@@ -315,7 +315,7 @@
315 def addCluster(self, protocol):315 def addCluster(self, protocol):
316 """Add a new stub cluster using the given `protocol`.316 """Add a new stub cluster using the given `protocol`.
317317
318 The `protocol` should be an instance of `amp.AMP`.318 The `protocol` should be an instance of `amp32.AMP`.
319319
320 :returns: A `Deferred` that fires with the connected protocol320 :returns: A `Deferred` that fires with the connected protocol
321 instance.321 instance.
322322
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2014-11-10 15:11:58 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2014-11-12 12:20:32 +0000
@@ -87,6 +87,7 @@
87import netaddr87import netaddr
88from provisioningserver.network import discover_networks88from provisioningserver.network import discover_networks
89from provisioningserver.rpc import (89from provisioningserver.rpc import (
90 amp32,
90 cluster,91 cluster,
91 common,92 common,
92 exceptions,93 exceptions,
@@ -163,7 +164,6 @@
163from twisted.internet.interfaces import IStreamServerEndpoint164from twisted.internet.interfaces import IStreamServerEndpoint
164from twisted.internet.protocol import Factory165from twisted.internet.protocol import Factory
165from twisted.internet.threads import deferToThread166from twisted.internet.threads import deferToThread
166from twisted.protocols import amp
167from twisted.python import log167from twisted.python import log
168from twisted.python.failure import Failure168from twisted.python.failure import Failure
169from zope.interface.verify import verifyObject169from zope.interface.verify import verifyObject
@@ -284,7 +284,7 @@
284284
285 def test_StartTLS_is_registered(self):285 def test_StartTLS_is_registered(self):
286 protocol = Region()286 protocol = Region()
287 responder = protocol.locateResponder(amp.StartTLS.commandName)287 responder = protocol.locateResponder(amp32.StartTLS.commandName)
288 self.assertIsNotNone(responder)288 self.assertIsNotNone(responder)
289289
290 def test_get_tls_parameters_returns_parameters(self):290 def test_get_tls_parameters_returns_parameters(self):
@@ -292,7 +292,7 @@
292 # However, locateResponder() returns a closure, so we have to292 # However, locateResponder() returns a closure, so we have to
293 # side-step it.293 # side-step it.
294 protocol = Region()294 protocol = Region()
295 cls, func = protocol._commandDispatch[amp.StartTLS.commandName]295 cls, func = protocol._commandDispatch[amp32.StartTLS.commandName]
296 self.assertThat(func(protocol), are_valid_tls_parameters)296 self.assertThat(func(protocol), are_valid_tls_parameters)
297297
298 @wait_for_reactor298 @wait_for_reactor
@@ -303,7 +303,7 @@
303 # travelling over the wire as part of an AMP message. However,303 # travelling over the wire as part of an AMP message. However,
304 # the responder is not aware of this, and is called just like304 # the responder is not aware of this, and is called just like
305 # any other.305 # any other.
306 d = call_responder(Region(), amp.StartTLS, {})306 d = call_responder(Region(), amp32.StartTLS, {})
307307
308 def check(response):308 def check(response):
309 self.assertEqual({}, response)309 self.assertEqual({}, response)
@@ -1119,7 +1119,7 @@
1119 service.factory.protocol = HandshakingRegionServer1119 service.factory.protocol = HandshakingRegionServer
1120 protocol = service.factory.buildProtocol(addr=None) # addr is unused.1120 protocol = service.factory.buildProtocol(addr=None) # addr is unused.
1121 protocol.connectionMade()1121 protocol.connectionMade()
1122 connectionLost_up_call = self.patch(amp.AMP, "connectionLost")1122 connectionLost_up_call = self.patch(amp32.AMP, "connectionLost")
1123 self.assertDictEqual(1123 self.assertDictEqual(
1124 {protocol.ident: {protocol}},1124 {protocol.ident: {protocol}},
1125 service.connections)1125 service.connections)
11261126
=== modified file 'src/provisioningserver/pserv_services/dhcp_probe_service.py'
--- src/provisioningserver/pserv_services/dhcp_probe_service.py 2014-10-02 11:53:59 +0000
+++ src/provisioningserver/pserv_services/dhcp_probe_service.py 2014-11-12 12:20:32 +0000
@@ -22,6 +22,7 @@
2222
23from provisioningserver.dhcp.detect import probe_interface23from provisioningserver.dhcp.detect import probe_interface
24from provisioningserver.logger.log import get_maas_logger24from provisioningserver.logger.log import get_maas_logger
25from provisioningserver.rpc.amp32 import UnhandledCommand
25from provisioningserver.rpc.exceptions import NoConnectionsAvailable26from provisioningserver.rpc.exceptions import NoConnectionsAvailable
26from provisioningserver.rpc.region import (27from provisioningserver.rpc.region import (
27 GetClusterInterfaces,28 GetClusterInterfaces,
@@ -37,7 +38,6 @@
37 returnValue,38 returnValue,
38 )39 )
39from twisted.internet.threads import deferToThread40from twisted.internet.threads import deferToThread
40from twisted.protocols.amp import UnhandledCommand
4141
4242
43maaslog = get_maas_logger("dhcp.probe")43maaslog = get_maas_logger("dhcp.probe")
4444
=== added file 'src/provisioningserver/rpc/amp32.py'
--- src/provisioningserver/rpc/amp32.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/rpc/amp32.py 2014-11-12 12:20:32 +0000
@@ -0,0 +1,2646 @@
1# Copyright (c) 2005 Divmod, Inc.
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE.Twisted for details.
4
5"""
6This module implements a modified version of AMP, the Asynchronous Messaging
7Protocol, which is referred to as AMP32 from now on where it's necessary to
8differentiate between the two.
9
10The original AMP uses 16-bit length prefixes when sending messages, which
11limits payloads to 64k. This version uses 32-bit prefixes so that messages can
12be much larger, though in practice they're limit them to something much
13smaller that the maximum. Unfortunately this makes AMP32 incompatible with
14AMP, though their essential behaviour is identical.
15
16AMP is a protocol for sending multiple asynchronous request/response pairs over
17the same connection. Requests and responses are both collections of key/value
18pairs.
19
20AMP is a very simple protocol which is not an application. This module is a
21"protocol construction kit" of sorts; it attempts to be the simplest wire-level
22implementation of Deferreds. AMP provides the following base-level features:
23
24 - Asynchronous request/response handling (hence the name)
25
26 - Requests and responses are both key/value pairs
27
28 - Binary transfer of all data: all data is length-prefixed. Your
29 application will never need to worry about quoting.
30
31 - Command dispatching (like HTTP Verbs): the protocol is extensible, and
32 multiple AMP sub-protocols can be grouped together easily.
33
34The protocol implementation also provides a few additional features which are
35not part of the core wire protocol, but are nevertheless very useful:
36
37 - Tight TLS integration, with an included StartTLS command.
38
39 - Handshaking to other protocols: because AMP has well-defined message
40 boundaries and maintains all incoming and outgoing requests for you, you
41 can start a connection over AMP and then switch to another protocol.
42 This makes it ideal for firewall-traversal applications where you may
43 have only one forwarded port but multiple applications that want to use
44 it.
45
46Using AMP with Twisted is simple. Each message is a command, with a response.
47You begin by defining a command type. Commands specify their input and output
48in terms of the types that they expect to see in the request and response
49key-value pairs. Here's an example of a command that adds two integers, 'a'
50and 'b'::
51
52 class Sum(amp32.Command):
53 arguments = [('a', amp32.Integer()),
54 ('b', amp32.Integer())]
55 response = [('total', amp32.Integer())]
56
57Once you have specified a command, you need to make it part of a protocol, and
58define a responder for it. Here's a 'JustSum' protocol that includes a
59responder for our 'Sum' command::
60
61 class JustSum(amp32.AMP):
62 def sum(self, a, b):
63 total = a + b
64 print 'Did a sum: %d + %d = %d' % (a, b, total)
65 return {'total': total}
66 Sum.responder(sum)
67
68Later, when you want to actually do a sum, the following expression will return
69a L{Deferred} which will fire with the result::
70
71 ClientCreator(reactor, amp32.AMP).connectTCP(...).addCallback(
72 lambda p: p.callRemote(Sum, a=13, b=81)).addCallback(
73 lambda result: result['total'])
74
75Command responders may also return Deferreds, causing the response to be
76sent only once the Deferred fires::
77
78 class DelayedSum(amp32.AMP):
79 def slowSum(self, a, b):
80 total = a + b
81 result = defer.Deferred()
82 reactor.callLater(3, result.callback, {'total': total})
83 return result
84 Sum.responder(slowSum)
85
86This is transparent to the caller.
87
88You can also define the propagation of specific errors in AMP. For example,
89for the slightly more complicated case of division, we might have to deal with
90division by zero::
91
92 class Divide(amp32.Command):
93 arguments = [('numerator', amp32.Integer()),
94 ('denominator', amp32.Integer())]
95 response = [('result', amp32.Float())]
96 errors = {ZeroDivisionError: 'ZERO_DIVISION'}
97
98The 'errors' mapping here tells AMP that if a responder to Divide emits a
99L{ZeroDivisionError}, then the other side should be informed that an error of
100the type 'ZERO_DIVISION' has occurred. Writing a responder which takes
101advantage of this is very simple - just raise your exception normally::
102
103 class JustDivide(amp32.AMP):
104 def divide(self, numerator, denominator):
105 result = numerator / denominator
106 print 'Divided: %d / %d = %d' % (numerator, denominator, total)
107 return {'result': result}
108 Divide.responder(divide)
109
110On the client side, the errors mapping will be used to determine what the
111'ZERO_DIVISION' error means, and translated into an asynchronous exception,
112which can be handled normally as any L{Deferred} would be::
113
114 def trapZero(result):
115 result.trap(ZeroDivisionError)
116 print "Divided by zero: returning INF"
117 return 1e1000
118 ClientCreator(reactor, amp32.AMP).connectTCP(...).addCallback(
119 lambda p: p.callRemote(Divide, numerator=1234,
120 denominator=0)
121 ).addErrback(trapZero)
122
123For a complete, runnable example of both of these commands, see the files in
124the Twisted repository::
125
126 doc/core/examples/ampserver.py
127 doc/core/examples/ampclient.py
128
129On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and
130values, and empty keys to separate messages::
131
132 <2-byte length><key><2-byte length><value>
133 <2-byte length><key><2-byte length><value>
134 ...
135 <2-byte length><key><2-byte length><value>
136 <NUL><NUL> # Empty Key == End of Message
137
138And so on. Because it's tedious to refer to lengths and NULs constantly, the
139documentation will refer to packets as if they were newline delimited, like
140so::
141
142 C: _command: sum
143 C: _ask: ef639e5c892ccb54
144 C: a: 13
145 C: b: 81
146
147 S: _answer: ef639e5c892ccb54
148 S: total: 94
149
150Notes:
151
152In general, the order of keys is arbitrary. Specific uses of AMP may impose an
153ordering requirement, but unless this is specified explicitly, any ordering may
154be generated and any ordering must be accepted. This applies to the
155command-related keys I{_command} and I{_ask} as well as any other keys.
156
157Values are limited to the maximum encodable size in a 32-bit length.
158
159Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes.
160Note that we still use 2-byte lengths to encode keys. This small redundancy
161has several features:
162
163 - If an implementation becomes confused and starts emitting corrupt data,
164 or gets keys confused with values, many common errors will be signalled
165 immediately instead of delivering obviously corrupt packets.
166
167 - A single NUL will separate every key, and a double NUL separates
168 messages. This provides some redundancy when debugging traffic dumps.
169
170 - NULs will be present at regular intervals along the protocol, providing
171 some padding for otherwise braindead C implementations of the protocol,
172 so that <stdio.h> string functions will see the NUL and stop.
173
174 - This makes it possible to run an AMP server on a port also used by a
175 plain-text protocol, and easily distinguish between non-AMP clients (like
176 web browsers) which issue non-NUL as the first byte, and AMP clients,
177 which always issue NUL as the first byte.
178
179"""
180
181from __future__ import (
182 absolute_import,
183 print_function,
184 # unicode_literals,
185 )
186
187str = None
188
189__metaclass__ = type
190
191import datetime
192import decimal
193from io import BytesIO
194from itertools import count
195from struct import pack
196import types
197import warnings
198
199from twisted.internet.defer import (
200 Deferred,
201 fail,
202 maybeDeferred,
203 )
204from twisted.internet.error import (
205 ConnectionClosed,
206 ConnectionLost,
207 PeerVerifyError,
208 )
209from twisted.internet.interfaces import IFileDescriptorReceiver
210from twisted.internet.main import CONNECTION_LOST
211from twisted.protocols.basic import (
212 Int32StringReceiver,
213 StatefulStringProtocol,
214 )
215from twisted.python import (
216 filepath,
217 log,
218 )
219from twisted.python.failure import Failure
220from twisted.python.reflect import accumulateClassDict
221from zope.interface import (
222 implements,
223 Interface,
224 )
225
226
227try:
228 from twisted.internet import ssl
229except ImportError:
230 ssl = None
231else:
232 if ssl.supported:
233 from twisted.internet.ssl import (
234 CertificateOptions,
235 Certificate,
236 DN,
237 KeyPair,
238 )
239 else:
240 ssl = None
241
242
243ASK = '_ask'
244ANSWER = '_answer'
245COMMAND = '_command'
246ERROR = '_error'
247ERROR_CODE = '_error_code'
248ERROR_DESCRIPTION = '_error_description'
249UNKNOWN_ERROR_CODE = 'UNKNOWN'
250UNHANDLED_ERROR_CODE = 'UNHANDLED'
251
252MAX_KEY_LENGTH = 0xff
253MAX_VALUE_LENGTH = 2 * 1024 * 1024 # 2MiB
254
255
256class IArgumentType(Interface):
257 """
258 An L{IArgumentType} can serialize a Python object into an AMP box and
259 deserialize information from an AMP box back into a Python object.
260
261 @since: 9.0
262 """
263
264 def fromBox(name, strings, objects, proto):
265 """
266 Given an argument name and an AMP box containing serialized values,
267 extract one or more Python objects and add them to the C{objects}
268 dictionary.
269
270 @param name: The name associated with this argument. Most commonly,
271 this is the key which can be used to find a serialized value in
272 C{strings} and which should be used as the key in C{objects} to
273 associate with a structured Python object.
274 @type name: C{bytes}
275
276 @param strings: The AMP box from which to extract one or more
277 values.
278 @type strings: C{dict}
279
280 @param objects: The output dictionary to populate with the value for
281 this argument.
282 @type objects: C{dict}
283
284 @param proto: The protocol instance which received the AMP box being
285 interpreted. Most likely this is an instance of L{AMP}, but
286 this is not guaranteed.
287
288 @return: C{None}
289 """
290
291 def toBox(name, strings, objects, proto):
292 """
293 Given an argument name and a dictionary containing structured Python
294 objects, serialize values into one or more strings and add them to
295 the C{strings} dictionary.
296
297 @param name: The name associated with this argument. Most commonly,
298 this is the key which can be used to find an object in
299 C{objects} and which should be used as the key in C{strings} to
300 associate with a C{bytes} giving the serialized form of that
301 object.
302 @type name: C{bytes}
303
304 @param strings: The AMP box into which to insert one or more
305 strings.
306 @type strings: C{dict}
307
308 @param objects: The input dictionary from which to extract Python
309 objects to serialize.
310 @type objects: C{dict}
311
312 @param proto: The protocol instance which will send the AMP box once
313 it is fully populated. Most likely this is an instance of
314 L{AMP}, but this is not guaranteed.
315
316 @return: C{None}
317 """
318
319
320class IBoxSender(Interface):
321 """
322 A transport which can send L{AmpBox} objects.
323 """
324
325 def sendBox(box):
326 """
327 Send an L{AmpBox}.
328
329 @raise ProtocolSwitched: if the underlying protocol has been
330 switched.
331
332 @raise ConnectionLost: if the underlying connection has already been
333 lost.
334 """
335
336 def unhandledError(failure):
337 """
338 An unhandled error occurred in response to a box. Log it
339 appropriately.
340
341 @param failure: a L{Failure} describing the error that occurred.
342 """
343
344
345class IBoxReceiver(Interface):
346 """
347 An application object which can receive L{AmpBox} objects and dispatch them
348 appropriately.
349 """
350
351 def startReceivingBoxes(boxSender):
352 """
353 The L{ampBoxReceived} method will start being called; boxes may be
354 responded to by responding to the given L{IBoxSender}.
355
356 @param boxSender: an L{IBoxSender} provider.
357 """
358
359 def ampBoxReceived(box):
360 """
361 A box was received from the transport; dispatch it appropriately.
362 """
363
364 def stopReceivingBoxes(reason):
365 """
366 No further boxes will be received on this connection.
367
368 @type reason: L{Failure}
369 """
370
371
372class IResponderLocator(Interface):
373 """
374 An application object which can look up appropriate responder methods for
375 AMP commands.
376 """
377
378 def locateResponder(name):
379 """
380 Locate a responder method appropriate for the named command.
381
382 @param name: the wire-level name (commandName) of the AMP command to be
383 responded to.
384
385 @return: a 1-argument callable that takes an L{AmpBox} with argument
386 values for the given command, and returns an L{AmpBox} containing
387 argument values for the named command, or a L{Deferred} that fires the
388 same.
389 """
390
391
392class AmpError(Exception):
393 """
394 Base class of all Amp-related exceptions.
395 """
396
397
398class ProtocolSwitched(Exception):
399 """
400 Connections which have been switched to other protocols can no longer
401 accept traffic at the AMP level. This is raised when you try to send it.
402 """
403
404
405class OnlyOneTLS(AmpError):
406 """
407 This is an implementation limitation; TLS may only be started once per
408 connection.
409 """
410
411
412class NoEmptyBoxes(AmpError):
413 """
414 You can't have empty boxes on the connection. This is raised when you
415 receive or attempt to send one.
416 """
417
418
419class InvalidSignature(AmpError):
420 """
421 You didn't pass all the required arguments.
422 """
423
424
425class TooLong(AmpError):
426 """
427 One of the protocol's length limitations was violated.
428
429 @ivar isKey: true if the string being encoded in a key position, false if
430 it was in a value position.
431
432 @ivar isLocal: Was the string encoded locally, or received too long from
433 the network? (It's only physically possible to encode "too long" values on
434 the network for keys.)
435
436 @ivar value: The string that was too long.
437
438 @ivar keyName: If the string being encoded was in a value position, what
439 key was it being encoded for?
440 """
441
442 def __init__(self, isKey, isLocal, value, keyName=None):
443 AmpError.__init__(self)
444 self.isKey = isKey
445 self.isLocal = isLocal
446 self.value = value
447 self.keyName = keyName
448
449 def __repr__(self):
450 hdr = self.isKey and "key" or "value"
451 if not self.isKey:
452 hdr += ' ' + repr(self.keyName)
453 lcl = self.isLocal and "local" or "remote"
454 return "%s %s too long: %d" % (lcl, hdr, len(self.value))
455
456
457class BadLocalReturn(AmpError):
458 """
459 A bad value was returned from a local command; we were unable to coerce it.
460 """
461
462 def __init__(self, message, enclosed):
463 AmpError.__init__(self)
464 self.message = message
465 self.enclosed = enclosed
466
467 def __repr__(self):
468 return self.message + " " + self.enclosed.getBriefTraceback()
469
470 __bytes__ = __repr__
471
472
473class RemoteAmpError(AmpError):
474 """
475 This error indicates that something went wrong on the remote end of the
476 connection, and the error was serialized and transmitted to you.
477 """
478
479 def __init__(self, errorCode, description, fatal=False, local=None):
480 """Create a remote error with an error code and description.
481
482 @param errorCode: the AMP error code of this error.
483
484 @param description: some text to show to the user.
485
486 @param fatal: a boolean, true if this error should terminate the
487 connection.
488
489 @param local: a local Failure, if one exists.
490 """
491 if local:
492 localwhat = ' (local)'
493 othertb = local.getBriefTraceback()
494 else:
495 localwhat = ''
496 othertb = ''
497 Exception.__init__(
498 self, "Code<%s>%s: %s%s" % (
499 errorCode, localwhat,
500 description, othertb))
501 self.local = local
502 self.errorCode = errorCode
503 self.description = description
504 self.fatal = fatal
505
506
507class UnknownRemoteError(RemoteAmpError):
508 """
509 This means that an error whose type we can't identify was raised from the
510 other side.
511 """
512
513 def __init__(self, description):
514 errorCode = UNKNOWN_ERROR_CODE
515 RemoteAmpError.__init__(self, errorCode, description)
516
517
518class MalformedAmpBox(AmpError):
519 """
520 This error indicates that the wire-level protocol was malformed.
521 """
522
523
524class UnhandledCommand(AmpError):
525 """
526 A command received via amp could not be dispatched.
527 """
528
529
530class IncompatibleVersions(AmpError):
531 """
532 It was impossible to negotiate a compatible version of the protocol with
533 the other end of the connection.
534 """
535
536
537PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand}
538
539
540class AmpBox(dict):
541 """
542 I am a packet in the AMP protocol, much like a regular bytes:bytes
543 dictionary.
544 """
545
546 # be like a regular dictionary, don't magically acquire a __dict__...
547 __slots__ = []
548
549 def copy(self):
550 """
551 Return another AmpBox just like me.
552 """
553 newBox = self.__class__()
554 newBox.update(self)
555 return newBox
556
557 def serialize(self):
558 """
559 Convert me into a wire-encoded string.
560
561 @return: a bytes encoded according to the rules described in the module
562 docstring.
563 """
564 i = sorted(self.viewitems())
565 L = []
566 w = L.append
567 for k, v in i:
568 if type(k) == unicode:
569 raise TypeError("Unicode key not allowed: %r" % k)
570 if type(v) == unicode:
571 raise TypeError(
572 "Unicode value for key %r not allowed: %r" % (k, v))
573 if len(k) > MAX_KEY_LENGTH:
574 raise TooLong(True, True, k, None)
575 if len(v) > MAX_VALUE_LENGTH:
576 raise TooLong(False, True, v, k)
577 for kv in k, v:
578 w(pack("!I", len(kv)))
579 w(kv)
580 w(pack("!I", 0))
581 return ''.join(L)
582
583 def _sendTo(self, proto):
584 """
585 Serialize and send this box to a Amp instance. By the time it is being
586 sent, several keys are required. I must have exactly ONE of::
587
588 _ask
589 _answer
590 _error
591
592 If the '_ask' key is set, then the '_command' key must also be
593 set.
594
595 @param proto: an AMP instance.
596 """
597 proto.sendBox(self)
598
599 def __repr__(self):
600 return 'AmpBox(%s)' % (dict.__repr__(self),)
601
602
603# amp32.Box => AmpBox
604Box = AmpBox
605
606
607class QuitBox(AmpBox):
608 """
609 I am an AmpBox that, upon being sent, terminates the connection.
610 """
611
612 __slots__ = []
613
614 def __repr__(self):
615 return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),)
616
617 def _sendTo(self, proto):
618 """
619 Immediately call loseConnection after sending.
620 """
621 super(QuitBox, self)._sendTo(proto)
622 proto.transport.loseConnection()
623
624
625class _SwitchBox(AmpBox):
626 """
627 Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets
628 up state for the protocol to switch.
629 """
630
631 # DON'T set __slots__ here; we do have an attribute.
632
633 def __init__(self, innerProto, **kw):
634 """
635 Create a _SwitchBox with the protocol to switch to after being sent.
636
637 @param innerProto: the protocol instance to switch to.
638 @type innerProto: an IProtocol provider.
639 """
640 super(_SwitchBox, self).__init__(**kw)
641 self.innerProto = innerProto
642
643 def __repr__(self):
644 return '_SwitchBox(%r, **%s)' % (self.innerProto,
645 dict.__repr__(self),)
646
647 def _sendTo(self, proto):
648 """
649 Send me; I am the last box on the connection. All further traffic will
650 be over the new protocol.
651 """
652 super(_SwitchBox, self)._sendTo(proto)
653 proto._lockForSwitch()
654 proto._switchTo(self.innerProto)
655
656
657class BoxDispatcher:
658 """
659 A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es,
660 both incoming and outgoing, to their appropriate destinations.
661
662 Outgoing commands are converted into L{Deferred}s and outgoing boxes, and
663 associated tracking state to fire those L{Deferred} when '_answer' boxes
664 come back. Incoming '_answer' and '_error' boxes are converted into
665 callbacks and errbacks on those L{Deferred}s, respectively.
666
667 Incoming '_ask' boxes are converted into method calls on a supplied method
668 locator.
669
670 @ivar _outstandingRequests: a dictionary mapping request IDs to
671 L{Deferred}s which were returned for those requests.
672
673 @ivar locator: an object with a L{locateResponder} method that locates a
674 responder function that takes a Box and returns a result (either a Box or a
675 Deferred which fires one).
676
677 @ivar boxSender: an object which can send boxes, via the L{_sendBox}
678 method, such as an L{AMP} instance.
679 @type boxSender: L{IBoxSender}
680 """
681
682 implements(IBoxReceiver)
683
684 _failAllReason = None
685 _outstandingRequests = None
686 _counter = 0
687 boxSender = None
688
689 def __init__(self, locator):
690 self._outstandingRequests = {}
691 self.locator = locator
692
693 def startReceivingBoxes(self, boxSender):
694 """
695 The given boxSender is going to start calling boxReceived on this
696 L{BoxDispatcher}.
697
698 @param boxSender: The L{IBoxSender} to send command responses to.
699 """
700 self.boxSender = boxSender
701
702 def stopReceivingBoxes(self, reason):
703 """
704 No further boxes will be received here. Terminate all currently
705 oustanding command deferreds with the given reason.
706 """
707 self.failAllOutgoing(reason)
708
709 def failAllOutgoing(self, reason):
710 """
711 Call the errback on all outstanding requests awaiting responses.
712
713 @param reason: the Failure instance to pass to those errbacks.
714 """
715 self._failAllReason = reason
716 OR = list(self._outstandingRequests.viewitems())
717 self._outstandingRequests = None # we can never send another request
718 for key, value in OR:
719 value.errback(reason)
720
721 def _nextTag(self):
722 """
723 Generate protocol-local serial numbers for _ask keys.
724
725 @return: a string that has not yet been used on this connection.
726 """
727 self._counter += 1
728 return '%x' % (self._counter,)
729
730 def _sendBoxCommand(self, command, box, requiresAnswer=True):
731 """
732 Send a command across the wire with the given C{amp32.Box}.
733
734 Mutate the given box to give it any additional keys (_command, _ask)
735 required for the command and request/response machinery, then send it.
736
737 If requiresAnswer is True, returns a C{Deferred} which fires when a
738 response is received. The C{Deferred} is fired with an C{amp32.Box} on
739 success, or with an C{amp32.RemoteAmpError} if an error is received.
740
741 If the Deferred fails and the error is not handled by the caller of
742 this method, the failure will be logged and the connection dropped.
743
744 @param command: a bytes, the name of the command to issue.
745
746 @param box: an AmpBox with the arguments for the command.
747
748 @param requiresAnswer: a boolean. Defaults to True. If True, return a
749 Deferred which will fire when the other side responds to this command.
750 If False, return None and do not ask the other side for
751 acknowledgement.
752
753 @return: a Deferred which fires the AmpBox that holds the response to
754 this command, or None, as specified by requiresAnswer.
755
756 @raise ProtocolSwitched: if the protocol has been switched.
757 """
758 if self._failAllReason is not None:
759 return fail(self._failAllReason)
760 box[COMMAND] = command
761 tag = self._nextTag()
762 if requiresAnswer:
763 box[ASK] = tag
764 box._sendTo(self.boxSender)
765 if requiresAnswer:
766 result = self._outstandingRequests[tag] = Deferred()
767 else:
768 result = None
769 return result
770
771 def callRemoteString(self, command, requiresAnswer=True, **kw):
772 """
773 This is a low-level API, designed only for optimizing simple messages
774 for which the overhead of parsing is too great.
775
776 @param command: a bytes naming the command.
777
778 @param kw: arguments to the amp box.
779
780 @param requiresAnswer: a boolean. Defaults to True. If True, return a
781 Deferred which will fire when the other side responds to this command.
782 If False, return None and do not ask the other side for
783 acknowledgement.
784
785 @return: a Deferred which fires the AmpBox that holds the response to
786 this command, or None, as specified by requiresAnswer.
787 """
788 box = Box(kw)
789 return self._sendBoxCommand(command, box, requiresAnswer)
790
791 def callRemote(self, commandType, *a, **kw):
792 """
793 This is the primary high-level API for sending messages via AMP.
794 Invoke it with a command and appropriate arguments to send a message
795 to this connection's peer.
796
797 @param commandType: a subclass of Command.
798 @type commandType: L{type}
799
800 @param a: Positional (special) parameters taken by the command.
801 Positional parameters will typically not be sent over the wire. The
802 only command included with AMP which uses positional parameters is
803 L{ProtocolSwitchCommand}, which takes the protocol that will be
804 switched to as its first argument.
805
806 @param kw: Keyword arguments taken by the command. These are the
807 arguments declared in the command's 'arguments' attribute. They will
808 be encoded and sent to the peer as arguments for the L{commandType}.
809
810 @return: If L{commandType} has a C{requiresAnswer} attribute set to
811 L{False}, then return L{None}. Otherwise, return a L{Deferred} which
812 fires with a dictionary of objects representing the result of this
813 call. Additionally, this L{Deferred} may fail with an exception
814 representing a connection failure, with L{UnknownRemoteError} if the
815 other end of the connection fails for an unknown reason, or with any
816 error specified as a key in L{commandType}'s C{errors} dictionary.
817 """
818
819 # XXX this takes command subclasses and not command objects on purpose.
820 # There's really no reason to have all this back-and-forth between
821 # command objects and the protocol, and the extra object being created
822 # (the Command instance) is pointless. Command is kind of like
823 # Interface, and should be more like it.
824
825 # In other words, the fact that commandType is instantiated here is an
826 # implementation detail. Don't rely on it.
827
828 try:
829 co = commandType(*a, **kw)
830 except:
831 return fail()
832 return co._doCommand(self)
833
834 def unhandledError(self, failure):
835 """
836 This is a terminal callback called after application code has had a
837 chance to quash any errors.
838 """
839 return self.boxSender.unhandledError(failure)
840
841 def _answerReceived(self, box):
842 """
843 An AMP box was received that answered a command previously sent with
844 L{callRemote}.
845
846 @param box: an AmpBox with a value for its L{ANSWER} key.
847 """
848 question = self._outstandingRequests.pop(box[ANSWER])
849 question.addErrback(self.unhandledError)
850 question.callback(box)
851
852 def _errorReceived(self, box):
853 """
854 An AMP box was received that answered a command previously sent with
855 L{callRemote}, with an error.
856
857 @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE},
858 and L{ERROR_DESCRIPTION} keys.
859 """
860 question = self._outstandingRequests.pop(box[ERROR])
861 question.addErrback(self.unhandledError)
862 errorCode = box[ERROR_CODE]
863 description = box[ERROR_DESCRIPTION]
864 if errorCode in PROTOCOL_ERRORS:
865 exc = PROTOCOL_ERRORS[errorCode](errorCode, description)
866 else:
867 exc = RemoteAmpError(errorCode, description)
868 question.errback(Failure(exc))
869
870 def _commandReceived(self, box):
871 """
872 @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK}
873 keys.
874 """
875 def formatAnswer(answerBox):
876 answerBox[ANSWER] = box[ASK]
877 return answerBox
878
879 def formatError(error):
880 if error.check(RemoteAmpError):
881 code = error.value.errorCode
882 desc = error.value.description
883 if error.value.fatal:
884 errorBox = QuitBox()
885 else:
886 errorBox = AmpBox()
887 else:
888 errorBox = QuitBox()
889 # here is where server-side logging happens if the error isn't
890 # handled
891 log.err(error)
892 code = UNKNOWN_ERROR_CODE
893 desc = "Unknown Error"
894 errorBox[ERROR] = box[ASK]
895 errorBox[ERROR_DESCRIPTION] = desc
896 errorBox[ERROR_CODE] = code
897 return errorBox
898
899 deferred = self.dispatchCommand(box)
900 if ASK in box:
901 deferred.addCallbacks(formatAnswer, formatError)
902 deferred.addCallback(self._safeEmit)
903 deferred.addErrback(self.unhandledError)
904
905 def ampBoxReceived(self, box):
906 """
907 An AmpBox was received, representing a command, or an answer to a
908 previously issued command (either successful or erroneous). Respond to
909 it according to its contents.
910
911 @param box: an AmpBox
912
913 @raise NoEmptyBoxes: when a box is received that does not contain an
914 '_answer', '_command' / '_ask', or '_error' key; i.e. one which does
915 not fit into the command / response protocol defined by AMP.
916 """
917 if ANSWER in box:
918 self._answerReceived(box)
919 elif ERROR in box:
920 self._errorReceived(box)
921 elif COMMAND in box:
922 self._commandReceived(box)
923 else:
924 raise NoEmptyBoxes(box)
925
926 def _safeEmit(self, aBox):
927 """
928 Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors
929 which cannot be usefully handled.
930 """
931 try:
932 aBox._sendTo(self.boxSender)
933 except (ProtocolSwitched, ConnectionLost):
934 pass
935
936 def dispatchCommand(self, box):
937 """
938 A box with a _command key was received.
939
940 Dispatch it to a local handler call it.
941
942 @param proto: an AMP instance.
943 @param box: an AmpBox to be dispatched.
944 """
945 cmd = box[COMMAND]
946 responder = self.locator.locateResponder(cmd)
947 if responder is None:
948 return fail(RemoteAmpError(
949 UNHANDLED_ERROR_CODE, "Unhandled Command: %r" % (cmd,),
950 False, local=Failure(UnhandledCommand())))
951 return maybeDeferred(responder, box)
952
953
954class CommandLocator:
955 """
956 A L{CommandLocator} is a collection of responders to AMP L{Command}s, with
957 the help of the L{Command.responder} decorator.
958 """
959
960 class __metaclass__(type):
961 """
962 This metaclass keeps track of all of the Command.responder-decorated
963 methods defined since the last CommandLocator subclass was defined. It
964 assumes (usually correctly, but unfortunately not necessarily so) that
965 those commands responders were all declared as methods of the class
966 being defined. Note that this list can be incorrect if users use the
967 Command.responder decorator outside the context of a CommandLocator
968 class declaration.
969
970 Command responders defined on subclasses are given precedence over
971 those inherited from a base class.
972
973 The Command.responder decorator explicitly cooperates with this
974 metaclass.
975 """
976
977 _currentClassCommands = []
978
979 def __new__(cls, name, bases, attrs):
980 commands = cls._currentClassCommands[:]
981 cls._currentClassCommands[:] = []
982 cd = attrs['_commandDispatch'] = {}
983 subcls = type.__new__(cls, name, bases, attrs)
984 ancestors = list(subcls.__mro__[1:])
985 ancestors.reverse()
986 for ancestor in ancestors:
987 cd.update(getattr(ancestor, '_commandDispatch', {}))
988 for commandClass, responderFunc in commands:
989 cd[commandClass.commandName] = (commandClass, responderFunc)
990 if (bases and (
991 subcls.lookupFunction != CommandLocator.lookupFunction)):
992 def locateResponder(self, name):
993 warnings.warn(
994 "Override locateResponder, not lookupFunction.",
995 category=PendingDeprecationWarning,
996 stacklevel=2)
997 return self.lookupFunction(name)
998 subcls.locateResponder = locateResponder
999 return subcls
1000
1001 implements(IResponderLocator)
1002
1003 def _wrapWithSerialization(self, aCallable, command):
1004 """
1005 Wrap aCallable with its command's argument de-serialization
1006 and result serialization logic.
1007
1008 @param aCallable: a callable with a 'command' attribute, designed to be
1009 called with keyword arguments.
1010
1011 @param command: the command class whose serialization to use.
1012
1013 @return: a 1-arg callable which, when invoked with an AmpBox, will
1014 deserialize the argument list and invoke appropriate user code for the
1015 callable's command, returning a Deferred which fires with the result or
1016 fails with an error.
1017 """
1018 def doit(box):
1019 kw = command.parseArguments(box, self)
1020
1021 def checkKnownErrors(error):
1022 key = error.trap(*command.allErrors)
1023 code = command.allErrors[key]
1024 desc = bytes(error.value)
1025 return Failure(RemoteAmpError(
1026 code, desc, key in command.fatalErrors, local=error))
1027
1028 def makeResponseFor(objects):
1029 try:
1030 return command.makeResponse(objects, self)
1031 except:
1032 # let's helpfully log this.
1033 originalFailure = Failure()
1034 raise BadLocalReturn(
1035 "%r returned %r and %r could not serialize it" % (
1036 aCallable,
1037 objects,
1038 command),
1039 originalFailure)
1040
1041 return maybeDeferred(aCallable, **kw).addCallback(
1042 makeResponseFor).addErrback(
1043 checkKnownErrors)
1044
1045 return doit
1046
1047 def lookupFunction(self, name):
1048 """
1049 Deprecated synonym for L{locateResponder}
1050 """
1051 if self.__class__.lookupFunction != CommandLocator.lookupFunction:
1052 return CommandLocator.locateResponder(self, name)
1053 else:
1054 warnings.warn("Call locateResponder, not lookupFunction.",
1055 category=PendingDeprecationWarning,
1056 stacklevel=2)
1057 return self.locateResponder(name)
1058
1059 def locateResponder(self, name):
1060 """
1061 Locate a callable to invoke when executing the named command.
1062
1063 @param name: the normalized name (from the wire) of the command.
1064
1065 @return: a 1-argument function that takes a Box and returns a box or a
1066 Deferred which fires a Box, for handling the command identified by the
1067 given name, or None, if no appropriate responder can be found.
1068 """
1069 # Try to find a high-level method to invoke, and if we can't find one,
1070 # fall back to a low-level one.
1071 cd = self._commandDispatch
1072 if name in cd:
1073 commandClass, responderFunc = cd[name]
1074 responderMethod = types.MethodType(
1075 responderFunc, self, self.__class__)
1076 return self._wrapWithSerialization(responderMethod, commandClass)
1077
1078
1079class SimpleStringLocator(object):
1080 """
1081 Implement the L{locateResponder} method to do simple, string-based
1082 dispatch.
1083 """
1084
1085 implements(IResponderLocator)
1086
1087 baseDispatchPrefix = 'amp_'
1088
1089 def locateResponder(self, name):
1090 """
1091 Locate a callable to invoke when executing the named command.
1092
1093 @return: a function with the name C{"amp_" + name} on L{self}, or None
1094 if no such function exists. This function will then be called with the
1095 L{AmpBox} itself as an argument.
1096
1097 @param name: the normalized name (from the wire) of the command.
1098 """
1099 fName = self.baseDispatchPrefix + (name.upper())
1100 return getattr(self, fName, None)
1101
1102
1103PYTHON_KEYWORDS = [
1104 'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda',
1105 'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except',
1106 'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield',
1107 'def', 'finally', 'in', 'print']
1108
1109
1110def _wireNameToPythonIdentifier(key):
1111 """
1112 (Private) Normalize an argument name from the wire for use with Python
1113 code. If the return value is going to be a python keyword it will be
1114 capitalized. If it contains any dashes they will be replaced with
1115 underscores.
1116
1117 The rationale behind this method is that AMP should be an inherently
1118 multi-language protocol, so message keys may contain all manner of bizarre
1119 bytes. This is not a complete solution; there are still forms of arguments
1120 that this implementation will be unable to parse. However, Python
1121 identifiers share a huge raft of properties with identifiers from many
1122 other languages, so this is a 'good enough' effort for now. We deal
1123 explicitly with dashes because that is the most likely departure: Lisps
1124 commonly use dashes to separate method names, so protocols initially
1125 implemented in a lisp amp dialect may use dashes in argument or command
1126 names.
1127
1128 @param key: a bytes, looking something like 'foo-bar-baz' or 'from'
1129
1130 @return: a bytes which is a valid python identifier, looking something like
1131 'foo_bar_baz' or 'From'.
1132 """
1133 lkey = key.replace("-", "_")
1134 if lkey in PYTHON_KEYWORDS:
1135 return lkey.title()
1136 return lkey
1137
1138
1139class Argument:
1140 """
1141 Base-class of all objects that take values from Amp packets and convert
1142 them into objects for Python functions.
1143
1144 This implementation of L{IArgumentType} provides several higher-level
1145 hooks for subclasses to override. See L{toString} and L{fromString}
1146 which will be used to define the behavior of L{IArgumentType.toBox} and
1147 L{IArgumentType.fromBox}, respectively.
1148 """
1149
1150 implements(IArgumentType)
1151
1152 optional = False
1153
1154 def __init__(self, optional=False):
1155 """
1156 Create an Argument.
1157
1158 @param optional: a boolean indicating whether this argument can be
1159 omitted in the protocol.
1160 """
1161 self.optional = optional
1162
1163 def retrieve(self, d, name, proto):
1164 """
1165 Retrieve the given key from the given dictionary, removing it if found.
1166
1167 @param d: a dictionary.
1168
1169 @param name: a key in L{d}.
1170
1171 @param proto: an instance of an AMP.
1172
1173 @raise KeyError: if I am not optional and no value was found.
1174
1175 @return: d[name].
1176 """
1177 if self.optional:
1178 value = d.get(name)
1179 if value is not None:
1180 del d[name]
1181 else:
1182 value = d.pop(name)
1183 return value
1184
1185 def fromBox(self, name, strings, objects, proto):
1186 """
1187 Populate an 'out' dictionary with mapping names to Python values
1188 decoded from an 'in' AmpBox mapping strings to string values.
1189
1190 @param name: the argument name to retrieve
1191 @type name: bytes
1192
1193 @param strings: The AmpBox to read string(s) from, a mapping of
1194 argument names to string values.
1195 @type strings: AmpBox
1196
1197 @param objects: The dictionary to write object(s) to, a mapping of
1198 names to Python objects.
1199 @type objects: dict
1200
1201 @param proto: an AMP instance.
1202 """
1203 st = self.retrieve(strings, name, proto)
1204 nk = _wireNameToPythonIdentifier(name)
1205 if self.optional and st is None:
1206 objects[nk] = None
1207 else:
1208 objects[nk] = self.fromStringProto(st, proto)
1209
1210 def toBox(self, name, strings, objects, proto):
1211 """
1212 Populate an 'out' AmpBox with strings encoded from an 'in' dictionary
1213 mapping names to Python values.
1214
1215 @param name: the argument name to retrieve
1216 @type name: bytes
1217
1218 @param strings: The AmpBox to write string(s) to, a mapping of
1219 argument names to string values.
1220 @type strings: AmpBox
1221
1222 @param objects: The dictionary to read object(s) from, a mapping of
1223 names to Python objects.
1224
1225 @type objects: dict
1226
1227 @param proto: the protocol we are converting for.
1228 @type proto: AMP
1229 """
1230 obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto)
1231 if self.optional and obj is None:
1232 # strings[name] = None
1233 pass
1234 else:
1235 strings[name] = self.toStringProto(obj, proto)
1236
1237 def fromStringProto(self, inString, proto):
1238 """
1239 Convert a string to a Python value.
1240
1241 @param inString: the string to convert.
1242
1243 @param proto: the protocol we are converting for.
1244 @type proto: AMP
1245
1246 @return: a Python object.
1247 """
1248 return self.fromString(inString)
1249
1250 def toStringProto(self, inObject, proto):
1251 """
1252 Convert a Python object to a string.
1253
1254 @param inObject: the object to convert.
1255
1256 @param proto: the protocol we are converting for.
1257 @type proto: AMP
1258 """
1259 return self.toString(inObject)
1260
1261 def fromString(self, inString):
1262 """
1263 Convert a string to a Python object. Subclasses must implement this.
1264
1265 @param inString: the string to convert.
1266 @type inString: bytes
1267
1268 @return: the decoded value from inString
1269 """
1270
1271 def toString(self, inObject):
1272 """
1273 Convert a Python object into a string for passing over the network.
1274
1275 @param inObject: an object of the type that this Argument is intended
1276 to deal with.
1277
1278 @return: the wire encoding of inObject
1279 @rtype: bytes
1280 """
1281
1282
1283class Integer(Argument):
1284 """
1285 Encode any integer values of any size on the wire as the string
1286 representation.
1287
1288 Example: C{123} becomes C{"123"}
1289 """
1290
1291 fromString = int
1292
1293 def toString(self, inObject):
1294 return bytes(int(inObject))
1295
1296
1297class String(Argument):
1298 """
1299 Don't do any conversion at all; just pass through 'bytes'.
1300 """
1301
1302 def toString(self, inObject):
1303 return inObject
1304
1305 def fromString(self, inString):
1306 return inString
1307
1308
1309class Float(Argument):
1310 """
1311 Encode floating-point values on the wire as their repr.
1312 """
1313
1314 fromString = float
1315 toString = repr
1316
1317
1318class Boolean(Argument):
1319 """
1320 Encode True or False as "True" or "False" on the wire.
1321 """
1322
1323 def fromString(self, inString):
1324 if inString == 'True':
1325 return True
1326 elif inString == 'False':
1327 return False
1328 else:
1329 raise TypeError("Bad boolean value: %r" % (inString,))
1330
1331 def toString(self, inObject):
1332 if inObject:
1333 return 'True'
1334 else:
1335 return 'False'
1336
1337
1338class Unicode(String):
1339 """
1340 Encode a unicode string on the wire as UTF-8.
1341 """
1342
1343 def toString(self, inObject):
1344 # assert isinstance(inObject, unicode)
1345 return String.toString(self, inObject.encode('utf-8'))
1346
1347 def fromString(self, inString):
1348 # assert isinstance(inString, bytes)
1349 return String.fromString(self, inString).decode('utf-8')
1350
1351
1352class Path(Unicode):
1353 """
1354 Encode and decode L{filepath.FilePath} instances as paths on the wire.
1355
1356 This is really intended for use with subprocess communication tools:
1357 exchanging pathnames on different machines over a network is not generally
1358 meaningful, but neither is it disallowed; you can use this to communicate
1359 about NFS paths, for example.
1360 """
1361
1362 def fromString(self, inString):
1363 return filepath.FilePath(Unicode.fromString(self, inString))
1364
1365 def toString(self, inObject):
1366 return Unicode.toString(self, inObject.path)
1367
1368
1369class ListOf(Argument):
1370 """
1371 Encode and decode lists of instances of a single other argument type.
1372
1373 For example, if you want to pass::
1374
1375 [3, 7, 9, 15]
1376
1377 You can create an argument like this::
1378
1379 ListOf(Integer())
1380
1381 The serialized form of the entire list is subject to the limit imposed by
1382 L{MAX_VALUE_LENGTH}. List elements are represented as 32-bit length
1383 prefixed strings. The argument type passed to the L{ListOf} initializer is
1384 responsible for producing the serialized form of each element.
1385
1386 @ivar elementType: The L{Argument} instance used to encode and decode list
1387 elements (note, not an arbitrary L{IArgument} implementation:
1388 arguments must be implemented using only the C{fromString} and
1389 C{toString} methods, not the C{fromBox} and C{toBox} methods).
1390
1391 @param optional: a boolean indicating whether this argument can be
1392 omitted in the protocol.
1393
1394 @since: 10.0
1395 """
1396
1397 def __init__(self, elementType, optional=False):
1398 self.elementType = elementType
1399 Argument.__init__(self, optional)
1400
1401 def fromString(self, inString):
1402 """
1403 Convert the serialized form of a list of instances of some type back
1404 into that list.
1405 """
1406 strings = []
1407 parser = Int32StringReceiver()
1408 parser.MAX_LENGTH = MAX_VALUE_LENGTH
1409 parser.stringReceived = strings.append
1410 parser.dataReceived(inString)
1411 return map(self.elementType.fromString, strings)
1412
1413 def toString(self, inObject):
1414 """
1415 Serialize the given list of objects to a single string.
1416 """
1417 strings = []
1418 for obj in inObject:
1419 serialized = self.elementType.toString(obj)
1420 strings.append(pack('!I', len(serialized)))
1421 strings.append(serialized)
1422 return ''.join(strings)
1423
1424
1425class AmpList(Argument):
1426 """
1427 Convert a list of dictionaries into a list of AMP boxes on the wire.
1428
1429 For example, if you want to pass::
1430
1431 [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}]
1432
1433 You might use an AmpList like this in your arguments or response list::
1434
1435 AmpList([('a', Integer()),
1436 ('b', Unicode())])
1437 """
1438
1439 def __init__(self, subargs, optional=False):
1440 """
1441 Create an AmpList.
1442
1443 @param subargs: a list of 2-tuples of ('name', argument) describing the
1444 schema of the dictionaries in the sequence of amp boxes.
1445
1446 @param optional: a boolean indicating whether this argument can be
1447 omitted in the protocol.
1448 """
1449 self.subargs = subargs
1450 Argument.__init__(self, optional)
1451
1452 def fromStringProto(self, inString, proto):
1453 boxes = parseString(inString)
1454 values = [_stringsToObjects(box, self.subargs, proto)
1455 for box in boxes]
1456 return values
1457
1458 def toStringProto(self, inObject, proto):
1459 return ''.join(
1460 _objectsToStrings(objects, self.subargs, Box(), proto).serialize()
1461 for objects in inObject)
1462
1463
1464class Descriptor(Integer):
1465 """
1466 Encode and decode file descriptors for exchange over a UNIX domain socket.
1467
1468 This argument type requires an AMP connection set up over an
1469 L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>} provider (for
1470 example, the kind of connection created by
1471 L{IReactorUNIX.connectUNIX<
1472 twisted.internet.interfaces.IReactorUNIX.connectUNIX>}
1473 and L{UNIXClientEndpoint<twisted.internet.endpoints.UNIXClientEndpoint>}).
1474
1475 There is no correspondence between the integer value of the file
1476 descriptor on the sending and receiving sides, therefore an alternate
1477 approach is taken to matching up received descriptors with particular
1478 L{Descriptor} parameters. The argument is encoded to an ordinal (unique
1479 per connection) for inclusion in the AMP command or response box. The
1480 descriptor itself is sent using
1481 L{IUNIXTransport.sendFileDescriptor<
1482 twisted.internet.interfaces.IUNIXTransport.sendFileDescriptor>}.
1483 The receiver uses the order in which file descriptors are received and the
1484 ordinal value to come up with the received copy of the descriptor.
1485 """
1486
1487 def fromStringProto(self, inString, proto):
1488 """
1489 Take a unique identifier associated with a file descriptor which must
1490 have been received by now and use it to look up that descriptor in a
1491 dictionary where they are kept.
1492
1493 @param inString: The base representation (as a byte string) of an
1494 ordinal indicating which file descriptor corresponds to this usage
1495 of this argument.
1496 @type inString: C{bytes}
1497
1498 @param proto: The protocol used to receive this descriptor. This
1499 protocol must be connected via a transport providing
1500 L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
1501 @type proto: L{BinaryBoxProtocol}
1502
1503 @return: The file descriptor represented by C{inString}.
1504 @rtype: C{int}
1505 """
1506 return proto._getDescriptor(int(inString))
1507
1508 def toStringProto(self, inObject, proto):
1509 """
1510 Send C{inObject}, an integer file descriptor, over C{proto}'s
1511 connection and return a unique identifier which will allow the
1512 receiver to associate the file descriptor with this argument.
1513
1514 @param inObject: A file descriptor to duplicate over an AMP connection
1515 as the value for this argument.
1516 @type inObject: C{int}
1517
1518 @param proto: The protocol which will be used to send this descriptor.
1519 This protocol must be connected via a transport providing
1520 L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
1521
1522 @return: A byte string which can be used by the receiver to reconstruct
1523 the file descriptor.
1524 @type: C{bytes}
1525 """
1526 identifier = proto._sendFileDescriptor(inObject)
1527 outString = Integer.toStringProto(self, identifier, proto)
1528 return outString
1529
1530
1531class Command:
1532 """
1533 Subclass me to specify an AMP Command.
1534
1535 @cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance),
1536 specifying the names and values of the parameters which are required for
1537 this command.
1538
1539 @cvar response: A list like L{arguments}, but instead used for the return
1540 value.
1541
1542 @cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags
1543 for errors represented as L{bytes}s. Responders which raise keys from this
1544 dictionary will have the error translated to the corresponding tag on the
1545 wire. Invokers which receive Deferreds from invoking this command with
1546 L{AMP.callRemote} will potentially receive Failures with keys from this
1547 mapping as their value. This mapping is inherited; if you declare a
1548 command which handles C{FooError} as 'FOO_ERROR', then subclass it and
1549 specify C{BarError} as 'BAR_ERROR', responders to the subclass may raise
1550 either C{FooError} or C{BarError}, and invokers must be able to deal with
1551 either of those exceptions.
1552
1553 @cvar fatalErrors: like 'errors', but errors in this list will always
1554 terminate the connection, despite being of a recognizable error type.
1555
1556 @cvar commandType: The type of Box used to issue commands; useful only for
1557 protocol-modifying behavior like startTLS or protocol switching. Defaults
1558 to a plain vanilla L{Box}.
1559
1560 @cvar responseType: The type of Box used to respond to this command; only
1561 useful for protocol-modifying behavior like startTLS or protocol switching.
1562 Defaults to a plain vanilla L{Box}.
1563
1564 @ivar requiresAnswer: a boolean; defaults to True. Set it to False on your
1565 subclass if you want callRemote to return None. Note: this is a hint only
1566 to the client side of the protocol. The return-type of a command responder
1567 method must always be a dictionary adhering to the contract specified by
1568 L{response}, because clients are always free to request a response if they
1569 want one.
1570 """
1571
1572 class __metaclass__(type):
1573 """
1574 Metaclass hack to establish reverse-mappings for 'errors' and
1575 'fatalErrors' as class vars.
1576 """
1577 def __new__(cls, name, bases, attrs):
1578 reverseErrors = attrs['reverseErrors'] = {}
1579 er = attrs['allErrors'] = {}
1580 if 'commandName' not in attrs:
1581 attrs['commandName'] = name
1582 newtype = type.__new__(cls, name, bases, attrs)
1583 errors = {}
1584 fatalErrors = {}
1585 accumulateClassDict(newtype, 'errors', errors)
1586 accumulateClassDict(newtype, 'fatalErrors', fatalErrors)
1587 for v, k in errors.viewitems():
1588 reverseErrors[k] = v
1589 er[v] = k
1590 for v, k in fatalErrors.viewitems():
1591 reverseErrors[k] = v
1592 er[v] = k
1593 return newtype
1594
1595 arguments = []
1596 response = []
1597 extra = []
1598 errors = {}
1599 fatalErrors = {}
1600
1601 commandType = Box
1602 responseType = Box
1603
1604 requiresAnswer = True
1605
1606 def __init__(self, **kw):
1607 """
1608 Create an instance of this command with specified values for its
1609 parameters.
1610
1611 @param kw: a dict containing an appropriate value for each name
1612 specified in the L{arguments} attribute of my class.
1613
1614 @raise InvalidSignature: if you forgot any required arguments.
1615 """
1616 self.structured = kw
1617 givenArgs = set(kw)
1618 forgotten = []
1619 for name, arg in self.arguments:
1620 pythonName = _wireNameToPythonIdentifier(name)
1621 if pythonName not in givenArgs and not arg.optional:
1622 forgotten.append(pythonName)
1623 if forgotten:
1624 raise InvalidSignature("forgot %s for %s" % (
1625 ', '.join(forgotten), self.commandName))
1626 forgotten = []
1627
1628 @classmethod
1629 def makeResponse(cls, objects, proto):
1630 """
1631 Serialize a mapping of arguments using this L{Command}'s
1632 response schema.
1633
1634 @param objects: a dict with keys matching the names specified in
1635 self.response, having values of the types that the Argument objects in
1636 self.response can format.
1637
1638 @param proto: an L{AMP}.
1639
1640 @return: an L{AmpBox}.
1641 """
1642 try:
1643 responseType = cls.responseType()
1644 except:
1645 return fail()
1646 return _objectsToStrings(objects, cls.response, responseType, proto)
1647
1648 @classmethod
1649 def makeArguments(cls, objects, proto):
1650 """
1651 Serialize a mapping of arguments using this L{Command}'s
1652 argument schema.
1653
1654 @param objects: a dict with keys similar to the names specified in
1655 self.arguments, having values of the types that the Argument objects in
1656 self.arguments can parse.
1657
1658 @param proto: an L{AMP}.
1659
1660 @return: An instance of this L{Command}'s C{commandType}.
1661 """
1662 allowedNames = set()
1663 for (argName, ignored) in cls.arguments:
1664 allowedNames.add(_wireNameToPythonIdentifier(argName))
1665
1666 for intendedArg in objects:
1667 if intendedArg not in allowedNames:
1668 raise InvalidSignature(
1669 "%s is not a valid argument" % (intendedArg,))
1670 return _objectsToStrings(objects, cls.arguments, cls.commandType(),
1671 proto)
1672
1673 @classmethod
1674 def parseResponse(cls, box, protocol):
1675 """
1676 Parse a mapping of serialized arguments using this
1677 L{Command}'s response schema.
1678
1679 @param box: A mapping of response-argument names to the
1680 serialized forms of those arguments.
1681 @param protocol: The L{AMP} protocol.
1682
1683 @return: A mapping of response-argument names to the parsed
1684 forms.
1685 """
1686 return _stringsToObjects(box, cls.response, protocol)
1687
1688 @classmethod
1689 def parseArguments(cls, box, protocol):
1690 """
1691 Parse a mapping of serialized arguments using this
1692 L{Command}'s argument schema.
1693
1694 @param box: A mapping of argument names to the seralized forms
1695 of those arguments.
1696 @param protocol: The L{AMP} protocol.
1697
1698 @return: A mapping of argument names to the parsed forms.
1699 """
1700 return _stringsToObjects(box, cls.arguments, protocol)
1701
1702 @classmethod
1703 def responder(cls, methodfunc):
1704 """
1705 Declare a method to be a responder for a particular command.
1706
1707 This is a decorator.
1708
1709 Use like so::
1710
1711 class MyCommand(Command):
1712 arguments = [('a', ...), ('b', ...)]
1713
1714 class MyProto(AMP):
1715 def myFunMethod(self, a, b):
1716 ...
1717 MyCommand.responder(myFunMethod)
1718
1719 Notes: Although decorator syntax is not used within Twisted, this
1720 function returns its argument and is therefore safe to use with
1721 decorator syntax.
1722
1723 This is not thread safe. Don't declare AMP subclasses in other
1724 threads. Don't declare responders outside the scope of AMP subclasses;
1725 the behavior is undefined.
1726
1727 @param methodfunc: A function which will later become a method, which
1728 has a keyword signature compatible with this command's L{argument} list
1729 and returns a dictionary with a set of keys compatible with this
1730 command's L{response} list.
1731
1732 @return: the methodfunc parameter.
1733 """
1734 CommandLocator._currentClassCommands.append((cls, methodfunc))
1735 return methodfunc
1736
1737 # Our only instance method
1738 def _doCommand(self, proto):
1739 """
1740 Encode and send this Command to the given protocol.
1741
1742 @param proto: an AMP, representing the connection to send to.
1743
1744 @return: a Deferred which will fire or error appropriately when the
1745 other side responds to the command (or error if the connection is lost
1746 before it is responded to).
1747 """
1748
1749 def _massageError(error):
1750 error.trap(RemoteAmpError)
1751 rje = error.value
1752 errorType = self.reverseErrors.get(rje.errorCode,
1753 UnknownRemoteError)
1754 return Failure(errorType(rje.description))
1755
1756 d = proto._sendBoxCommand(self.commandName,
1757 self.makeArguments(self.structured, proto),
1758 self.requiresAnswer)
1759
1760 if self.requiresAnswer:
1761 d.addCallback(self.parseResponse, proto)
1762 d.addErrback(_massageError)
1763
1764 return d
1765
1766
1767class _NoCertificate:
1768 """
1769 This is for peers which don't want to use a local certificate. Used by
1770 AMP because AMP's internal language is all about certificates and this
1771 duck-types in the appropriate place; this API isn't really stable though,
1772 so it's not exposed anywhere public.
1773
1774 For clients, it will use ephemeral DH keys, or whatever the default is for
1775 certificate-less clients in OpenSSL. For servers, it will generate a
1776 temporary self-signed certificate with garbage values in the DN and use
1777 that.
1778 """
1779
1780 def __init__(self, client):
1781 """
1782 Create a _NoCertificate which either is or isn't for the client side of
1783 the connection.
1784
1785 @param client: True if we are a client and should truly have no
1786 certificate and be anonymous, False if we are a server and actually
1787 have to generate a temporary certificate.
1788
1789 @type client: bool
1790 """
1791 self.client = client
1792
1793 def options(self, *authorities):
1794 """
1795 Behaves like L{twisted.internet.ssl.PrivateCertificate.options}().
1796 """
1797 if not self.client:
1798 # do some crud with sslverify to generate a temporary self-signed
1799 # certificate. This is SLOOOWWWWW so it is only in the absolute
1800 # worst, most naive case.
1801
1802 # We have to do this because OpenSSL will not let both the server
1803 # and client be anonymous.
1804 sharedDN = DN(CN='TEMPORARY CERTIFICATE')
1805 key = KeyPair.generate()
1806 cr = key.certificateRequest(sharedDN)
1807 sscrd = key.signCertificateRequest(
1808 sharedDN, cr, lambda dn: True, 1)
1809 cert = key.newCertificate(sscrd)
1810 return cert.options(*authorities)
1811 options = dict()
1812 if authorities:
1813 options.update(
1814 verify=True, requireCertificate=True,
1815 caCerts=[auth.original for auth in authorities])
1816 occo = CertificateOptions(**options)
1817 return occo
1818
1819
1820class _TLSBox(AmpBox):
1821 """
1822 I am an AmpBox that, upon being sent, initiates a TLS connection.
1823 """
1824
1825 __slots__ = []
1826
1827 def __init__(self):
1828 if ssl is None:
1829 raise RemoteAmpError("TLS_ERROR", "TLS not available")
1830 AmpBox.__init__(self)
1831
1832 def _keyprop(k, default):
1833 return property(lambda self: self.get(k, default))
1834
1835 # These properties are described in startTLS
1836 certificate = _keyprop('tls_localCertificate', _NoCertificate(False))
1837 verify = _keyprop('tls_verifyAuthorities', None)
1838
1839 def _sendTo(self, proto):
1840 """
1841 Send my encoded value to the protocol, then initiate TLS.
1842 """
1843 ab = AmpBox(self)
1844 for k in ['tls_localCertificate',
1845 'tls_verifyAuthorities']:
1846 ab.pop(k, None)
1847 ab._sendTo(proto)
1848 proto._startTLS(self.certificate, self.verify)
1849
1850
1851class _LocalArgument(String):
1852 """
1853 Local arguments are never actually relayed across the wire. This is just a
1854 shim so that StartTLS can pretend to have some arguments: if arguments
1855 acquire documentation properties, replace this with something nicer later.
1856 """
1857
1858 def fromBox(self, name, strings, objects, proto):
1859 pass
1860
1861
1862class StartTLS(Command):
1863 """
1864 Use, or subclass, me to implement a command that starts TLS.
1865
1866 Callers of StartTLS may pass several special arguments, which affect the
1867 TLS negotiation:
1868
1869 - tls_localCertificate: This is a
1870 twisted.internet.ssl.PrivateCertificate which will be used to secure
1871 the side of the connection it is returned on.
1872
1873 - tls_verifyAuthorities: This is a list of
1874 twisted.internet.ssl.Certificate objects that will be used as the
1875 certificate authorities to verify our peer's certificate.
1876
1877 Each of those special parameters may also be present as a key in the
1878 response dictionary.
1879 """
1880
1881 arguments = [("tls_localCertificate", _LocalArgument(optional=True)),
1882 ("tls_verifyAuthorities", _LocalArgument(optional=True))]
1883
1884 response = [("tls_localCertificate", _LocalArgument(optional=True)),
1885 ("tls_verifyAuthorities", _LocalArgument(optional=True))]
1886
1887 responseType = _TLSBox
1888
1889 def __init__(self, **kw):
1890 """
1891 Create a StartTLS command. (This is private. Use AMP.callRemote.)
1892
1893 @param tls_localCertificate: the PrivateCertificate object to use to
1894 secure the connection. If it's None, or unspecified, an ephemeral DH
1895 key is used instead.
1896
1897 @param tls_verifyAuthorities: a list of Certificate objects which
1898 represent root certificates to verify our peer with.
1899 """
1900 if ssl is None:
1901 raise RuntimeError("TLS not available.")
1902 self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True))
1903 self.authorities = kw.pop('tls_verifyAuthorities', None)
1904 Command.__init__(self, **kw)
1905
1906 def _doCommand(self, proto):
1907 """
1908 When a StartTLS command is sent, prepare to start TLS, but don't
1909 actually do it; wait for the acknowledgement, then initiate the TLS
1910 handshake.
1911 """
1912 d = Command._doCommand(self, proto)
1913 proto._prepareTLS(self.certificate, self.authorities)
1914
1915 # XXX before we get back to user code we are going to start TLS...
1916 def actuallystart(response):
1917 proto._startTLS(self.certificate, self.authorities)
1918 return response
1919 d.addCallback(actuallystart)
1920
1921 return d
1922
1923
1924class ProtocolSwitchCommand(Command):
1925 """
1926 Use this command to switch from something Amp-derived to a different
1927 protocol mid-connection. This can be useful to use amp as the
1928 connection-startup negotiation phase. Since TLS is a different layer
1929 entirely, you can use Amp to negotiate the security parameters of your
1930 connection, then switch to a different protocol, and the connection will
1931 remain secured.
1932 """
1933
1934 def __init__(self, _protoToSwitchToFactory, **kw):
1935 """
1936 Create a ProtocolSwitchCommand.
1937
1938 @param _protoToSwitchToFactory: a ProtocolFactory which will generate
1939 the Protocol to switch to.
1940
1941 @param kw: Keyword arguments, encoded and handled normally as
1942 L{Command} would.
1943 """
1944
1945 self.protoToSwitchToFactory = _protoToSwitchToFactory
1946 super(ProtocolSwitchCommand, self).__init__(**kw)
1947
1948 @classmethod
1949 def makeResponse(cls, innerProto, proto):
1950 return _SwitchBox(innerProto)
1951
1952 def _doCommand(self, proto):
1953 """
1954 When we emit a ProtocolSwitchCommand, lock the protocol, but don't
1955 actually switch to the new protocol unless an acknowledgement is
1956 received. If an error is received, switch back.
1957 """
1958 d = super(ProtocolSwitchCommand, self)._doCommand(proto)
1959 proto._lockForSwitch()
1960
1961 def switchNow(ign):
1962 innerProto = self.protoToSwitchToFactory.buildProtocol(
1963 proto.transport.getPeer())
1964 proto._switchTo(innerProto, self.protoToSwitchToFactory)
1965 return ign
1966
1967 def handle(ign):
1968 proto._unlockFromSwitch()
1969 self.protoToSwitchToFactory.clientConnectionFailed(
1970 None, Failure(CONNECTION_LOST))
1971 return ign
1972
1973 return d.addCallbacks(switchNow, handle)
1974
1975
1976class _DescriptorExchanger(object):
1977 """
1978 L{_DescriptorExchanger} is a mixin for L{BinaryBoxProtocol} which adds
1979 support for receiving file descriptors, a feature offered by
1980 L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}.
1981
1982 @ivar _descriptors: Temporary storage for all file descriptors received.
1983 Values in this dictionary are the file descriptors (as integers). Keys
1984 in this dictionary are ordinals giving the order in which each
1985 descriptor was received. The ordering information is used to allow
1986 L{Descriptor} to determine which is the correct descriptor for any
1987 particular usage of that argument type.
1988 @type _descriptors: C{dict}
1989
1990 @ivar _sendingDescriptorCounter: A no-argument callable which returns the
1991 ordinals, starting from 0. This is used to construct values for
1992 C{_sendFileDescriptor}.
1993
1994 @ivar _receivingDescriptorCounter: A no-argument callable which returns the
1995 ordinals, starting from 0. This is used to construct values for
1996 C{fileDescriptorReceived}.
1997 """
1998
1999 implements(IFileDescriptorReceiver)
2000
2001 def __init__(self):
2002 self._descriptors = {}
2003 self._getDescriptor = self._descriptors.pop
2004 self._sendingDescriptorCounter = count()
2005 self._receivingDescriptorCounter = count()
2006
2007 def _sendFileDescriptor(self, descriptor):
2008 """
2009 Assign and return the next ordinal to the given descriptor after
2010 sending the descriptor over this protocol's transport.
2011 """
2012 self.transport.sendFileDescriptor(descriptor)
2013 return next(self._sendingDescriptorCounter)
2014
2015 def fileDescriptorReceived(self, descriptor):
2016 """
2017 Collect received file descriptors to be claimed later by L{Descriptor}.
2018
2019 @param descriptor: The received file descriptor.
2020 @type descriptor: C{int}
2021 """
2022 self._descriptors[next(self._receivingDescriptorCounter)] = descriptor
2023
2024
2025class BinaryBoxProtocol(StatefulStringProtocol, Int32StringReceiver,
2026 _DescriptorExchanger):
2027 """
2028 A protocol for receiving L{AmpBox}es - key/value pairs - via
2029 length-prefixed strings. A box is composed of:
2030
2031 - any number of key-value pairs, described by:
2032 - a 4-byte network-endian packed key length (of which the first
2033 3 bytes must be null, and the last must be non-null: i.e. the
2034 value of the length must be 1-255)
2035 - a key, comprised of that many bytes
2036 - a 4-byte network-endian unsigned value length (up to the maximum
2037 of C{MAX_VALUE_LENGTH})
2038 - a value, comprised of that many bytes
2039 - 4 null bytes
2040
2041 In other words, an even number of strings prefixed with packed unsigned
2042 32-bit integers, and then a 0-length string to indicate the end of the box.
2043
2044 This protocol also implements 2 extra private bits of functionality related
2045 to the byte boundaries between messages; it can start TLS between two given
2046 boxes or switch to an entirely different protocol. However, due to some
2047 tricky elements of the implementation, the public interface to this
2048 functionality is L{ProtocolSwitchCommand} and L{StartTLS}.
2049
2050 @ivar _keyLengthLimitExceeded: A flag which is only true when the
2051 connection is being closed because a key length prefix which was longer
2052 than allowed by the protocol was received.
2053
2054 @ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived}
2055 method will be invoked for each L{AmpBox} that is received.
2056 """
2057
2058 implements(IBoxSender)
2059
2060 _justStartedTLS = False
2061 _startingTLSBuffer = None
2062 _locked = False
2063 _currentKey = None
2064 _currentBox = None
2065
2066 _keyLengthLimitExceeded = False
2067
2068 hostCertificate = None
2069 noPeerCertificate = False # for tests
2070 innerProtocol = None
2071 innerProtocolClientFactory = None
2072
2073 def __init__(self, boxReceiver):
2074 _DescriptorExchanger.__init__(self)
2075 self.boxReceiver = boxReceiver
2076
2077 def _switchTo(self, newProto, clientFactory=None):
2078 """
2079 Switch this BinaryBoxProtocol's transport to a new protocol. You need
2080 to do this 'simultaneously' on both ends of a connection; the easiest
2081 way to do this is to use a subclass of ProtocolSwitchCommand.
2082
2083 @param newProto: the new protocol instance to switch to.
2084
2085 @param clientFactory: the ClientFactory to send the
2086 L{clientConnectionLost} notification to.
2087 """
2088 # All the data that Int32StringReceiver has not yet dealt with belongs
2089 # to our new protocol: luckily it's keeping that in a handy (although
2090 # ostensibly internal) variable for us:
2091 newProtoData = self.recvd
2092 # We're quite possibly in the middle of a 'dataReceived' loop in
2093 # Int32StringReceiver: let's make sure that the next iteration, the
2094 # loop will break and not attempt to look at something that isn't a
2095 # length prefix.
2096 self.recvd = ''
2097 # Finally, do the actual work of setting up the protocol and delivering
2098 # its first chunk of data, if one is available.
2099 self.innerProtocol = newProto
2100 self.innerProtocolClientFactory = clientFactory
2101 newProto.makeConnection(self.transport)
2102 if newProtoData:
2103 newProto.dataReceived(newProtoData)
2104
2105 def sendBox(self, box):
2106 """
2107 Send a amp32.Box to my peer.
2108
2109 Note: transport.write is never called outside of this method.
2110
2111 @param box: an AmpBox.
2112
2113 @raise ProtocolSwitched: if the protocol has previously been switched.
2114
2115 @raise ConnectionLost: if the connection has previously been lost.
2116 """
2117 if self._locked:
2118 raise ProtocolSwitched(
2119 "This connection has switched: no AMP traffic allowed.")
2120 if self.transport is None:
2121 raise ConnectionLost()
2122 if self._startingTLSBuffer is not None:
2123 self._startingTLSBuffer.append(box)
2124 else:
2125 self.transport.write(box.serialize())
2126
2127 def makeConnection(self, transport):
2128 """
2129 Notify L{boxReceiver} that it is about to receive boxes from this
2130 protocol by invoking L{startReceivingBoxes}.
2131 """
2132 self.transport = transport
2133 self.boxReceiver.startReceivingBoxes(self)
2134 self.connectionMade()
2135
2136 def dataReceived(self, data):
2137 """
2138 Either parse incoming data as L{AmpBox}es or relay it to our nested
2139 protocol.
2140 """
2141 if self._justStartedTLS:
2142 self._justStartedTLS = False
2143 # If we already have an inner protocol, then we don't deliver data to
2144 # the protocol parser any more; we just hand it off.
2145 if self.innerProtocol is not None:
2146 self.innerProtocol.dataReceived(data)
2147 return
2148 return Int32StringReceiver.dataReceived(self, data)
2149
2150 def connectionLost(self, reason):
2151 """
2152 The connection was lost; notify any nested protocol.
2153 """
2154 if self.innerProtocol is not None:
2155 self.innerProtocol.connectionLost(reason)
2156 if self.innerProtocolClientFactory is not None:
2157 self.innerProtocolClientFactory.clientConnectionLost(
2158 None, reason)
2159 if self._keyLengthLimitExceeded:
2160 failReason = Failure(TooLong(True, False, None, None))
2161 elif reason.check(ConnectionClosed) and self._justStartedTLS:
2162 # We just started TLS and haven't received any data. This means
2163 # the other connection didn't like our cert (although they may not
2164 # have told us why - later Twisted should make 'reason' into a TLS
2165 # error.)
2166 failReason = PeerVerifyError(
2167 "Peer rejected our certificate for an unknown reason.")
2168 else:
2169 failReason = reason
2170 self.boxReceiver.stopReceivingBoxes(failReason)
2171
2172 # The longest key allowed
2173 _MAX_KEY_LENGTH = MAX_KEY_LENGTH
2174
2175 # The longest value allowed (this is somewhat redundant, as longer values
2176 # cannot be encoded - ah well).
2177 _MAX_VALUE_LENGTH = MAX_VALUE_LENGTH
2178
2179 # The first thing received is a key.
2180 MAX_LENGTH = _MAX_KEY_LENGTH
2181
2182 def proto_init(self, string):
2183 """
2184 String received in the 'init' state.
2185 """
2186 self._currentBox = AmpBox()
2187 return self.proto_key(string)
2188
2189 def proto_key(self, string):
2190 """
2191 String received in the 'key' state. If the key is empty, a complete
2192 box has been received.
2193 """
2194 if string:
2195 self._currentKey = string
2196 self.MAX_LENGTH = self._MAX_VALUE_LENGTH
2197 return 'value'
2198 else:
2199 self.boxReceiver.ampBoxReceived(self._currentBox)
2200 self._currentBox = None
2201 return 'init'
2202
2203 def proto_value(self, string):
2204 """
2205 String received in the 'value' state.
2206 """
2207 self._currentBox[self._currentKey] = string
2208 self._currentKey = None
2209 self.MAX_LENGTH = self._MAX_KEY_LENGTH
2210 return 'key'
2211
2212 def lengthLimitExceeded(self, length):
2213 """
2214 The key length limit was exceeded. Disconnect the transport and make
2215 sure a meaningful exception is reported.
2216 """
2217 self._keyLengthLimitExceeded = True
2218 self.transport.loseConnection()
2219
2220 def _lockForSwitch(self):
2221 """
2222 Lock this binary protocol so that no further boxes may be sent. This
2223 is used when sending a request to switch underlying protocols. You
2224 probably want to subclass ProtocolSwitchCommand rather than calling
2225 this directly.
2226 """
2227 self._locked = True
2228
2229 def _unlockFromSwitch(self):
2230 """
2231 Unlock this locked binary protocol so that further boxes may be sent
2232 again. This is used after an attempt to switch protocols has failed
2233 for some reason.
2234 """
2235 if self.innerProtocol is not None:
2236 raise ProtocolSwitched(
2237 "Protocol already switched. Cannot unlock.")
2238 self._locked = False
2239
2240 def _prepareTLS(self, certificate, verifyAuthorities):
2241 """
2242 Used by StartTLSCommand to put us into the state where we don't
2243 actually send things that get sent, instead we buffer them. see
2244 L{_sendBox}.
2245 """
2246 self._startingTLSBuffer = []
2247 if self.hostCertificate is not None:
2248 raise OnlyOneTLS(
2249 "Previously authenticated connection between %s and %s "
2250 "is trying to re-establish as %s" % (
2251 self.hostCertificate,
2252 self.peerCertificate,
2253 (certificate, verifyAuthorities)))
2254
2255 def _startTLS(self, certificate, verifyAuthorities):
2256 """
2257 Used by TLSBox to initiate the SSL handshake.
2258
2259 @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for
2260 use locally.
2261
2262 @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances
2263 representing certificate authorities which will verify our peer.
2264 """
2265 self.hostCertificate = certificate
2266 self._justStartedTLS = True
2267 if verifyAuthorities is None:
2268 verifyAuthorities = ()
2269 self.transport.startTLS(certificate.options(*verifyAuthorities))
2270 stlsb = self._startingTLSBuffer
2271 if stlsb is not None:
2272 self._startingTLSBuffer = None
2273 for box in stlsb:
2274 self.sendBox(box)
2275
2276 def _getPeerCertificate(self):
2277 if self.noPeerCertificate:
2278 return None
2279 return Certificate.peerFromTransport(self.transport)
2280 peerCertificate = property(_getPeerCertificate)
2281
2282 def unhandledError(self, failure):
2283 """
2284 The buck stops here. This error was completely unhandled, time to
2285 terminate the connection.
2286 """
2287 log.err(
2288 failure,
2289 "Amp server or network failure unhandled by client application. "
2290 "Dropping connection! To avoid, add errbacks to ALL remote "
2291 "commands!")
2292 if self.transport is not None:
2293 self.transport.loseConnection()
2294
2295 def _defaultStartTLSResponder(self):
2296 """
2297 The default TLS responder doesn't specify any certificate or anything.
2298
2299 From a security perspective, it's little better than a plain-text
2300 connection - but it is still a *bit* better, so it's included for
2301 convenience.
2302
2303 You probably want to override this by providing your own
2304 C{StartTLS.responder}.
2305 """
2306 return {}
2307 StartTLS.responder(_defaultStartTLSResponder)
2308
2309
2310class AMP(BinaryBoxProtocol, BoxDispatcher,
2311 CommandLocator, SimpleStringLocator):
2312 """
2313 This protocol is an AMP connection. See the module docstring for protocol
2314 details.
2315 """
2316
2317 _ampInitialized = False
2318
2319 def __init__(self, boxReceiver=None, locator=None):
2320 # For backwards compatibility. When AMP did not separate parsing logic
2321 # (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and
2322 # command routing (L{CommandLocator}), it did not have a constructor.
2323 # Now it does, so old subclasses might have defined their own that did
2324 # not upcall. If this flag isn't set, we'll call the constructor in
2325 # makeConnection before anything actually happens.
2326 self._ampInitialized = True
2327 if boxReceiver is None:
2328 boxReceiver = self
2329 if locator is None:
2330 locator = self
2331 BoxDispatcher.__init__(self, locator)
2332 BinaryBoxProtocol.__init__(self, boxReceiver)
2333
2334 def locateResponder(self, name):
2335 """
2336 Unify the implementations of L{CommandLocator} and
2337 L{SimpleStringLocator} to perform both kinds of dispatch, preferring
2338 L{CommandLocator}.
2339 """
2340 firstResponder = CommandLocator.locateResponder(self, name)
2341 if firstResponder is not None:
2342 return firstResponder
2343 secondResponder = SimpleStringLocator.locateResponder(self, name)
2344 return secondResponder
2345
2346 def __repr__(self):
2347 """
2348 A verbose string representation which gives us information about this
2349 AMP connection.
2350 """
2351 if self.innerProtocol is not None:
2352 innerRepr = ' inner %r' % (self.innerProtocol,)
2353 else:
2354 innerRepr = ''
2355 return '<%s%s at 0x%x>' % (
2356 self.__class__.__name__, innerRepr, id(self))
2357
2358 def makeConnection(self, transport):
2359 """
2360 Emit a helpful log message when the connection is made.
2361 """
2362 if not self._ampInitialized:
2363 # See comment in the constructor re: backward compatibility. I
2364 # should probably emit a deprecation warning here.
2365 AMP.__init__(self)
2366 # Save these so we can emit a similar log message in L{connectionLost}.
2367 self._transportPeer = transport.getPeer()
2368 self._transportHost = transport.getHost()
2369 log.msg("%s connection established (HOST:%s PEER:%s)" % (
2370 self.__class__.__name__,
2371 self._transportHost,
2372 self._transportPeer))
2373 BinaryBoxProtocol.makeConnection(self, transport)
2374
2375 def connectionLost(self, reason):
2376 """
2377 Emit a helpful log message when the connection is lost.
2378 """
2379 log.msg("%s connection lost (HOST:%s PEER:%s)" %
2380 (self.__class__.__name__,
2381 self._transportHost,
2382 self._transportPeer))
2383 BinaryBoxProtocol.connectionLost(self, reason)
2384 self.transport = None
2385
2386
2387class _ParserHelper:
2388 """
2389 A box receiver which records all boxes received.
2390 """
2391
2392 def __init__(self):
2393 self.boxes = []
2394
2395 def getPeer(self):
2396 return 'string'
2397
2398 def getHost(self):
2399 return 'string'
2400
2401 disconnecting = False
2402
2403 def startReceivingBoxes(self, sender):
2404 """
2405 No initialization is required.
2406 """
2407
2408 def ampBoxReceived(self, box):
2409 self.boxes.append(box)
2410
2411 # Synchronous helpers
2412 @classmethod
2413 def parse(cls, fileObj):
2414 """
2415 Parse some amp data stored in a file.
2416
2417 @param fileObj: a file-like object.
2418
2419 @return: a list of AmpBoxes encoded in the given file.
2420 """
2421 parserHelper = cls()
2422 bbp = BinaryBoxProtocol(boxReceiver=parserHelper)
2423 bbp.makeConnection(parserHelper)
2424 bbp.dataReceived(fileObj.read())
2425 return parserHelper.boxes
2426
2427 @classmethod
2428 def parseString(cls, data):
2429 """
2430 Parse some amp data stored in a string.
2431
2432 @param data: a bytes holding some amp-encoded data.
2433
2434 @return: a list of AmpBoxes encoded in the given string.
2435 """
2436 return cls.parse(BytesIO(data))
2437
2438
2439parse = _ParserHelper.parse
2440parseString = _ParserHelper.parseString
2441
2442
2443def _stringsToObjects(strings, arglist, proto):
2444 """
2445 Convert an AmpBox to a dictionary of python objects, converting through a
2446 given arglist.
2447
2448 @param strings: an AmpBox (or dict of strings)
2449
2450 @param arglist: a list of 2-tuples of strings and Argument objects, as
2451 described in L{Command.arguments}.
2452
2453 @param proto: an L{AMP} instance.
2454
2455 @return: the converted dictionary mapping names to argument objects.
2456 """
2457 objects = {}
2458 myStrings = strings.copy()
2459 for argname, argparser in arglist:
2460 argparser.fromBox(argname, myStrings, objects, proto)
2461 return objects
2462
2463
2464def _objectsToStrings(objects, arglist, strings, proto):
2465 """
2466 Convert a dictionary of python objects to an AmpBox, converting through a
2467 given arglist.
2468
2469 @param objects: a dict mapping names to python objects
2470
2471 @param arglist: a list of 2-tuples of strings and Argument objects, as
2472 described in L{Command.arguments}.
2473
2474 @param strings: [OUT PARAMETER] An object providing the L{dict}
2475 interface which will be populated with serialized data.
2476
2477 @param proto: an L{AMP} instance.
2478
2479 @return: The converted dictionary mapping names to encoded argument
2480 strings (identical to C{strings}).
2481 """
2482 myObjects = objects.copy()
2483 for argname, argparser in arglist:
2484 argparser.toBox(argname, strings, myObjects, proto)
2485 return strings
2486
2487
2488class _FixedOffsetTZInfo(datetime.tzinfo):
2489 """
2490 Represents a fixed timezone offset (without daylight saving time).
2491
2492 @ivar name: A C{bytes} giving the name of this timezone; the name just
2493 includes how much time this offset represents.
2494
2495 @ivar offset: A C{datetime.timedelta} giving the amount of time this
2496 timezone is offset.
2497 """
2498
2499 def __init__(self, sign, hours, minutes):
2500 self.name = '%s%02i:%02i' % (sign, hours, minutes)
2501 if sign == '-':
2502 hours = -hours
2503 minutes = -minutes
2504 elif sign != '+':
2505 raise ValueError('invalid sign for timezone %r' % (sign,))
2506 self.offset = datetime.timedelta(hours=hours, minutes=minutes)
2507
2508 def utcoffset(self, dt):
2509 """
2510 Return this timezone's offset from UTC.
2511 """
2512 return self.offset
2513
2514 def dst(self, dt):
2515 """
2516 Return a zero C{datetime.timedelta} for the daylight saving time
2517 offset, since there is never one.
2518 """
2519 return datetime.timedelta(0)
2520
2521 def tzname(self, dt):
2522 """
2523 Return a string describing this timezone.
2524 """
2525 return self.name
2526
2527
2528utc = _FixedOffsetTZInfo('+', 0, 0)
2529
2530
2531class Decimal(Argument):
2532 """
2533 Encodes C{decimal.Decimal} instances.
2534
2535 There are several ways in which a decimal value might be encoded.
2536
2537 Special values are encoded as special strings::
2538
2539 - Positive infinity is encoded as C{"Infinity"}
2540 - Negative infinity is encoded as C{"-Infinity"}
2541 - Quiet not-a-number is encoded as either C{"NaN"} or C{"-NaN"}
2542 - Signalling not-a-number is encoded as either C{"sNaN"} or C{"-sNaN"}
2543
2544 Normal values are encoded using the base ten string representation, using
2545 engineering notation to indicate magnitude without precision, and "normal"
2546 digits to indicate precision. For example::
2547
2548 - C{"1"} represents the value I{1} with precision to one place.
2549 - C{"-1"} represents the value I{-1} with precision to one place.
2550 - C{"1.0"} represents the value I{1} with precision to two places.
2551 - C{"10"} represents the value I{10} with precision to two places.
2552 - C{"1E+2"} represents the value I{10} with precision to one place.
2553 - C{"1E-1"} represents the value I{0.1} with precision to one place.
2554 - C{"1.5E+2"} represents the value I{15} with precision to two places.
2555
2556 U{http://speleotrove.com/decimal/} should be considered the authoritative
2557 specification for the format.
2558 """
2559
2560 fromString = decimal.Decimal
2561
2562 def toString(self, inObject):
2563 """
2564 Serialize a C{decimal.Decimal} instance to the specified wire format.
2565 """
2566 if isinstance(inObject, decimal.Decimal):
2567 # Hopefully decimal.Decimal.__bytes__ actually does what we want.
2568 return bytes(inObject)
2569 raise ValueError(
2570 "amp32.Decimal can only encode instances of decimal.Decimal")
2571
2572
2573class DateTime(Argument):
2574 """
2575 Encodes C{datetime.datetime} instances.
2576
2577 Wire format: '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i'. Fields in
2578 order are: year, month, day, hour, minute, second, microsecond, timezone
2579 direction (+ or -), timezone hour, timezone minute. Encoded string is
2580 always exactly 32 characters long. This format is compatible with ISO 8601,
2581 but that does not mean all ISO 8601 dates can be accepted.
2582
2583 Also, note that the datetime module's notion of a "timezone" can be
2584 complex, but the wire format includes only a fixed offset, so the
2585 conversion is not lossless. A lossless transmission of a C{datetime}
2586 instance is not feasible since the receiving end would require a Python
2587 interpreter.
2588
2589 @ivar _positions: A sequence of slices giving the positions of various
2590 interesting parts of the wire format.
2591 """
2592
2593 _positions = [
2594 slice(0, 4), slice(5, 7), slice(8, 10), # year, month, day
2595 slice(11, 13), slice(14, 16), slice(17, 19), # hour, minute, second
2596 slice(20, 26), # microsecond
2597 # intentionally skip timezone direction, as it is not an integer
2598 slice(27, 29), slice(30, 32) # timezone hour, timezone minute
2599 ]
2600
2601 def fromString(self, s):
2602 """
2603 Parse a string containing a date and time in the wire format into a
2604 C{datetime.datetime} instance.
2605 """
2606 if len(s) != 32:
2607 raise ValueError('invalid date format %r' % (s,))
2608
2609 values = [int(s[p]) for p in self._positions]
2610 sign = s[26]
2611 timezone = _FixedOffsetTZInfo(sign, *values[7:])
2612 values[7:] = [timezone]
2613 return datetime.datetime(*values)
2614
2615 def toString(self, i):
2616 """
2617 Serialize a C{datetime.datetime} instance to a string in the specified
2618 wire format.
2619 """
2620 offset = i.utcoffset()
2621 if offset is None:
2622 raise ValueError(
2623 'amp32.DateTime cannot serialize naive datetime instances. '
2624 'You may find amp32.utc useful.')
2625
2626 minutesOffset = (offset.days * 86400 + offset.seconds) // 60
2627
2628 if minutesOffset > 0:
2629 sign = '+'
2630 else:
2631 sign = '-'
2632
2633 # strftime has no way to format the microseconds, or put a ':' in the
2634 # timezone. Suprise!
2635
2636 return '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i' % (
2637 i.year,
2638 i.month,
2639 i.day,
2640 i.hour,
2641 i.minute,
2642 i.second,
2643 i.microsecond,
2644 sign,
2645 abs(minutesOffset) // 60,
2646 abs(minutesOffset) % 60)
02647
=== modified file 'src/provisioningserver/rpc/arguments.py'
--- src/provisioningserver/rpc/arguments.py 2014-10-30 11:22:41 +0000
+++ src/provisioningserver/rpc/arguments.py 2014-11-12 12:20:32 +0000
@@ -25,10 +25,10 @@
25import zlib25import zlib
2626
27from apiclient.utils import ascii_url27from apiclient.utils import ascii_url
28from twisted.protocols import amp28from provisioningserver.rpc import amp32
2929
3030
31class Bytes(amp.Argument):31class Bytes(amp32.Argument):
32 """Encode a structure on the wire as bytes.32 """Encode a structure on the wire as bytes.
3333
34 In truth, this does nothing more than assert that the inputs are34 In truth, this does nothing more than assert that the inputs are
@@ -41,11 +41,11 @@
41 return inObject41 return inObject
4242
43 def fromString(self, inString):43 def fromString(self, inString):
44 # inString is always a byte string, as defined by amp.Argument.44 # inString is always a byte string, as defined by amp32.Argument.
45 return inString45 return inString
4646
4747
48class Choice(amp.Argument):48class Choice(amp32.Argument):
49 """Encode a choice to a predefined bytestring on the wire."""49 """Encode a choice to a predefined bytestring on the wire."""
5050
51 def __init__(self, choices, optional=False):51 def __init__(self, choices, optional=False):
@@ -76,7 +76,7 @@
76 return self._decode[inString]76 return self._decode[inString]
7777
7878
79class ParsedURL(amp.Argument):79class ParsedURL(amp32.Argument):
80 """Encode a URL on the wire.80 """Encode a URL on the wire.
8181
82 The URL should be an instance of :py:class:`~urlparse.ParseResult`82 The URL should be an instance of :py:class:`~urlparse.ParseResult`
@@ -105,11 +105,11 @@
105 return urlparse.urlparse(inString)105 return urlparse.urlparse(inString)
106106
107107
108class StructureAsJSON(amp.Argument):108class StructureAsJSON(amp32.Argument):
109 """Encode a structure on the wire as JSON, compressed with zlib.109 """Encode a structure on the wire as JSON, compressed with zlib.
110110
111 The compressed size of the structure should not exceed111 The compressed size of the structure should not exceed
112 :py:data:`~twisted.protocols.amp.MAX_VALUE_LENGTH`, or ``0xffff``112 :py:data:`~provisioningserver.rpc.amp32.MAX_VALUE_LENGTH`, or ``0xffff``
113 bytes. This is pretty hard to be sure of ahead of time, so only use113 bytes. This is pretty hard to be sure of ahead of time, so only use
114 this for small structures that won't go near the limit.114 this for small structures that won't go near the limit.
115 """115 """
@@ -121,8 +121,8 @@
121 return json.loads(zlib.decompress(inString))121 return json.loads(zlib.decompress(inString))
122122
123123
124class CompressedAmpList(amp.AmpList):124class CompressedAmpList(amp32.AmpList):
125 """An :py:class:`amp.AmpList` that's compressed on the wire.125 """An :py:class:`amp32.AmpList` that's compressed on the wire.
126126
127 The serialised form is transparently compressed and decompressed with127 The serialised form is transparently compressed and decompressed with
128 zlib. This can be useful when there's a lot of repetition in the list128 zlib. This can be useful when there's a lot of repetition in the list
129129
=== modified file 'src/provisioningserver/rpc/cluster.py'
--- src/provisioningserver/rpc/cluster.py 2014-10-23 18:21:41 +0000
+++ src/provisioningserver/rpc/cluster.py 2014-11-12 12:20:32 +0000
@@ -37,7 +37,10 @@
37 PowerActionFail,37 PowerActionFail,
38 UnknownPowerType,38 UnknownPowerType,
39 )39 )
40from provisioningserver.rpc import exceptions40from provisioningserver.rpc import (
41 amp32,
42 exceptions,
43 )
41from provisioningserver.rpc.arguments import (44from provisioningserver.rpc.arguments import (
42 Bytes,45 Bytes,
43 ParsedURL,46 ParsedURL,
@@ -47,10 +50,9 @@
47 Authenticate,50 Authenticate,
48 Identify,51 Identify,
49 )52 )
50from twisted.protocols import amp53
5154
5255class ListBootImages(amp32.Command):
53class ListBootImages(amp.Command):
54 """List the boot images available on this cluster controller.56 """List the boot images available on this cluster controller.
5557
56 :since: 1.558 :since: 1.5
@@ -58,20 +60,20 @@
5860
59 arguments = []61 arguments = []
60 response = [62 response = [
61 (b"images", amp.AmpList(63 (b"images", amp32.AmpList(
62 [(b"osystem", amp.Unicode()),64 [(b"osystem", amp32.Unicode()),
63 (b"architecture", amp.Unicode()),65 (b"architecture", amp32.Unicode()),
64 (b"subarchitecture", amp.Unicode()),66 (b"subarchitecture", amp32.Unicode()),
65 (b"release", amp.Unicode()),67 (b"release", amp32.Unicode()),
66 (b"label", amp.Unicode()),68 (b"label", amp32.Unicode()),
67 (b"purpose", amp.Unicode()),69 (b"purpose", amp32.Unicode()),
68 (b"xinstall_type", amp.Unicode()),70 (b"xinstall_type", amp32.Unicode()),
69 (b"xinstall_path", amp.Unicode())]))71 (b"xinstall_path", amp32.Unicode())]))
70 ]72 ]
71 errors = []73 errors = []
7274
7375
74class DescribePowerTypes(amp.Command):76class DescribePowerTypes(amp32.Command):
75 """Get a JSON Schema describing this cluster's power types.77 """Get a JSON Schema describing this cluster's power types.
7678
77 :since: 1.579 :since: 1.5
@@ -84,7 +86,7 @@
84 errors = []86 errors = []
8587
8688
87class ListSupportedArchitectures(amp.Command):89class ListSupportedArchitectures(amp32.Command):
88 """Report the cluster's supported architectures.90 """Report the cluster's supported architectures.
8991
90 :since: 1.592 :since: 1.5
@@ -92,15 +94,15 @@
9294
93 arguments = []95 arguments = []
94 response = [96 response = [
95 (b"architectures", amp.AmpList([97 (b"architectures", amp32.AmpList([
96 (b"name", amp.Unicode()),98 (b"name", amp32.Unicode()),
97 (b"description", amp.Unicode()),99 (b"description", amp32.Unicode()),
98 ])),100 ])),
99 ]101 ]
100 errors = []102 errors = []
101103
102104
103class ListOperatingSystems(amp.Command):105class ListOperatingSystems(amp32.Command):
104 """Report the cluster's supported operating systems.106 """Report the cluster's supported operating systems.
105107
106 :since: 1.7108 :since: 1.7
@@ -108,34 +110,34 @@
108110
109 arguments = []111 arguments = []
110 response = [112 response = [
111 (b"osystems", amp.AmpList([113 (b"osystems", amp32.AmpList([
112 (b"name", amp.Unicode()),114 (b"name", amp32.Unicode()),
113 (b"title", amp.Unicode()),115 (b"title", amp32.Unicode()),
114 (b"releases", amp.AmpList([116 (b"releases", amp32.AmpList([
115 (b"name", amp.Unicode()),117 (b"name", amp32.Unicode()),
116 (b"title", amp.Unicode()),118 (b"title", amp32.Unicode()),
117 (b"requires_license_key", amp.Boolean()),119 (b"requires_license_key", amp32.Boolean()),
118 (b"can_commission", amp.Boolean()),120 (b"can_commission", amp32.Boolean()),
119 ])),121 ])),
120 (b"default_release", amp.Unicode(optional=True)),122 (b"default_release", amp32.Unicode(optional=True)),
121 (b"default_commissioning_release", amp.Unicode(optional=True)),123 (b"default_commissioning_release", amp32.Unicode(optional=True)),
122 ])),124 ])),
123 ]125 ]
124 errors = []126 errors = []
125127
126128
127class GetOSReleaseTitle(amp.Command):129class GetOSReleaseTitle(amp32.Command):
128 """Get the title for the operating systems release.130 """Get the title for the operating systems release.
129131
130 :since: 1.7132 :since: 1.7
131 """133 """
132134
133 arguments = [135 arguments = [
134 (b"osystem", amp.Unicode()),136 (b"osystem", amp32.Unicode()),
135 (b"release", amp.Unicode()),137 (b"release", amp32.Unicode()),
136 ]138 ]
137 response = [139 response = [
138 (b"title", amp.Unicode()),140 (b"title", amp32.Unicode()),
139 ]141 ]
140 errors = {142 errors = {
141 exceptions.NoSuchOperatingSystem: (143 exceptions.NoSuchOperatingSystem: (
@@ -143,19 +145,19 @@
143 }145 }
144146
145147
146class ValidateLicenseKey(amp.Command):148class ValidateLicenseKey(amp32.Command):
147 """Validate an OS license key.149 """Validate an OS license key.
148150
149 :since: 1.7151 :since: 1.7
150 """152 """
151153
152 arguments = [154 arguments = [
153 (b"osystem", amp.Unicode()),155 (b"osystem", amp32.Unicode()),
154 (b"release", amp.Unicode()),156 (b"release", amp32.Unicode()),
155 (b"key", amp.Unicode()),157 (b"key", amp32.Unicode()),
156 ]158 ]
157 response = [159 response = [
158 (b"is_valid", amp.Boolean()),160 (b"is_valid", amp32.Boolean()),
159 ]161 ]
160 errors = {162 errors = {
161 exceptions.NoSuchOperatingSystem: (163 exceptions.NoSuchOperatingSystem: (
@@ -163,20 +165,20 @@
163 }165 }
164166
165167
166class GetPreseedData(amp.Command):168class GetPreseedData(amp32.Command):
167 """Get OS-specific preseed data.169 """Get OS-specific preseed data.
168170
169 :since: 1.7171 :since: 1.7
170 """172 """
171173
172 arguments = [174 arguments = [
173 (b"osystem", amp.Unicode()),175 (b"osystem", amp32.Unicode()),
174 (b"preseed_type", amp.Unicode()),176 (b"preseed_type", amp32.Unicode()),
175 (b"node_system_id", amp.Unicode()),177 (b"node_system_id", amp32.Unicode()),
176 (b"node_hostname", amp.Unicode()),178 (b"node_hostname", amp32.Unicode()),
177 (b"consumer_key", amp.Unicode()),179 (b"consumer_key", amp32.Unicode()),
178 (b"token_key", amp.Unicode()),180 (b"token_key", amp32.Unicode()),
179 (b"token_secret", amp.Unicode()),181 (b"token_secret", amp32.Unicode()),
180 (b"metadata_url", ParsedURL()),182 (b"metadata_url", ParsedURL()),
181 ]183 ]
182 response = [184 response = [
@@ -190,16 +192,16 @@
190 }192 }
191193
192194
193class ComposeCurtinNetworkPreseed(amp.Command):195class ComposeCurtinNetworkPreseed(amp32.Command):
194 """Generate Curtin network preseed for a node.196 """Generate Curtin network preseed for a node.
195197
196 :since: 1.7198 :since: 1.7
197 """199 """
198200
199 arguments = [201 arguments = [
200 (b"osystem", amp.Unicode()),202 (b"osystem", amp32.Unicode()),
201 (b"config", StructureAsJSON()),203 (b"config", StructureAsJSON()),
202 (b"disable_ipv4", amp.Boolean()),204 (b"disable_ipv4", amp32.Boolean()),
203 ]205 ]
204 response = [206 response = [
205 (b"data", StructureAsJSON()),207 (b"data", StructureAsJSON()),
@@ -209,16 +211,16 @@
209 }211 }
210212
211213
212class _Power(amp.Command):214class _Power(amp32.Command):
213 """Base class for power control commands.215 """Base class for power control commands.
214216
215 :since: 1.7217 :since: 1.7
216 """218 """
217219
218 arguments = [220 arguments = [
219 (b"system_id", amp.Unicode()),221 (b"system_id", amp32.Unicode()),
220 (b"hostname", amp.Unicode()),222 (b"hostname", amp32.Unicode()),
221 (b"power_type", amp.Unicode()),223 (b"power_type", amp32.Unicode()),
222 # We can't define a tighter schema here because this is a highly224 # We can't define a tighter schema here because this is a highly
223 # variable bag of arguments from a variety of sources.225 # variable bag of arguments from a variety of sources.
224 (b"context", StructureAsJSON()),226 (b"context", StructureAsJSON()),
@@ -256,29 +258,29 @@
256 :since: 1.7258 :since: 1.7
257 """259 """
258 response = [260 response = [
259 (b"state", amp.Unicode()),261 (b"state", amp32.Unicode()),
260 ]262 ]
261263
262264
263class _ConfigureDHCP(amp.Command):265class _ConfigureDHCP(amp32.Command):
264 """Configure a DHCP server.266 """Configure a DHCP server.
265267
266 :since: 1.7268 :since: 1.7
267 """269 """
268 arguments = [270 arguments = [
269 (b"omapi_key", amp.Unicode()),271 (b"omapi_key", amp32.Unicode()),
270 (b"subnet_configs", amp.AmpList([272 (b"subnet_configs", amp32.AmpList([
271 (b"subnet", amp.Unicode()),273 (b"subnet", amp32.Unicode()),
272 (b"subnet_mask", amp.Unicode()),274 (b"subnet_mask", amp32.Unicode()),
273 (b"subnet_cidr", amp.Unicode()),275 (b"subnet_cidr", amp32.Unicode()),
274 (b"broadcast_ip", amp.Unicode()),276 (b"broadcast_ip", amp32.Unicode()),
275 (b"interface", amp.Unicode()),277 (b"interface", amp32.Unicode()),
276 (b"router_ip", amp.Unicode()),278 (b"router_ip", amp32.Unicode()),
277 (b"dns_servers", amp.Unicode()),279 (b"dns_servers", amp32.Unicode()),
278 (b"ntp_server", amp.Unicode()),280 (b"ntp_server", amp32.Unicode()),
279 (b"domain_name", amp.Unicode()),281 (b"domain_name", amp32.Unicode()),
280 (b"ip_range_low", amp.Unicode()),282 (b"ip_range_low", amp32.Unicode()),
281 (b"ip_range_high", amp.Unicode()),283 (b"ip_range_high", amp32.Unicode()),
282 ])),284 ])),
283 ]285 ]
284 response = []286 response = []
@@ -299,18 +301,18 @@
299 """301 """
300302
301303
302class CreateHostMaps(amp.Command):304class CreateHostMaps(amp32.Command):
303 """Create host maps in the DHCP server's configuration.305 """Create host maps in the DHCP server's configuration.
304306
305 :since: 1.7307 :since: 1.7
306 """308 """
307309
308 arguments = [310 arguments = [
309 (b"mappings", amp.AmpList([311 (b"mappings", amp32.AmpList([
310 (b"ip_address", amp.Unicode()),312 (b"ip_address", amp32.Unicode()),
311 (b"mac_address", amp.Unicode()),313 (b"mac_address", amp32.Unicode()),
312 ])),314 ])),
313 (b"shared_key", amp.Unicode()),315 (b"shared_key", amp32.Unicode()),
314 ]316 ]
315 response = []317 response = []
316 errors = {318 errors = {
@@ -319,15 +321,15 @@
319 }321 }
320322
321323
322class RemoveHostMaps(amp.Command):324class RemoveHostMaps(amp32.Command):
323 """Remove host maps from the DHCP server's configuration.325 """Remove host maps from the DHCP server's configuration.
324326
325 :since: 1.7327 :since: 1.7
326 """328 """
327329
328 arguments = [330 arguments = [
329 (b"ip_addresses", amp.ListOf(amp.Unicode())),331 (b"ip_addresses", amp32.ListOf(amp32.Unicode())),
330 (b"shared_key", amp.Unicode()),332 (b"shared_key", amp32.Unicode()),
331 ]333 ]
332 response = []334 response = []
333 errors = {335 errors = {
@@ -336,7 +338,7 @@
336 }338 }
337339
338340
339class ImportBootImages(amp.Command):341class ImportBootImages(amp32.Command):
340 """Import boot images and report the final342 """Import boot images and report the final
341 boot images that exist on the cluster.343 boot images that exist on the cluster.
342344
@@ -344,15 +346,15 @@
344 """346 """
345347
346 arguments = [348 arguments = [
347 (b"sources", amp.AmpList(349 (b"sources", amp32.AmpList(
348 [(b"url", amp.Unicode()),350 [(b"url", amp32.Unicode()),
349 (b"keyring_data", Bytes()),351 (b"keyring_data", Bytes()),
350 (b"selections", amp.AmpList(352 (b"selections", amp32.AmpList(
351 [(b"os", amp.Unicode()),353 [(b"os", amp32.Unicode()),
352 (b"release", amp.Unicode()),354 (b"release", amp32.Unicode()),
353 (b"arches", amp.ListOf(amp.Unicode())),355 (b"arches", amp32.ListOf(amp32.Unicode())),
354 (b"subarches", amp.ListOf(amp.Unicode())),356 (b"subarches", amp32.ListOf(amp32.Unicode())),
355 (b"labels", amp.ListOf(amp.Unicode()))]))])),357 (b"labels", amp32.ListOf(amp32.Unicode()))]))])),
356 (b"http_proxy", ParsedURL(optional=True)),358 (b"http_proxy", ParsedURL(optional=True)),
357 (b"https_proxy", ParsedURL(optional=True)),359 (b"https_proxy", ParsedURL(optional=True)),
358 ]360 ]
@@ -360,80 +362,80 @@
360 errors = []362 errors = []
361363
362364
363class StartMonitors(amp.Command):365class StartMonitors(amp32.Command):
364 """Starts monitors(s) on the cluster.366 """Starts monitors(s) on the cluster.
365367
366 :since: 1.7368 :since: 1.7
367 """369 """
368370
369 arguments = [371 arguments = [
370 (b"monitors", amp.AmpList(372 (b"monitors", amp32.AmpList(
371 [(b"deadline", amp.DateTime()),373 [(b"deadline", amp32.DateTime()),
372 (b"context", StructureAsJSON()),374 (b"context", StructureAsJSON()),
373 (b"id", amp.Unicode()),375 (b"id", amp32.Unicode()),
374 ]))376 ]))
375 ]377 ]
376 response = []378 response = []
377 errors = []379 errors = []
378380
379381
380class CancelMonitor(amp.Command):382class CancelMonitor(amp32.Command):
381 """Cancels an existing monitor on the cluster.383 """Cancels an existing monitor on the cluster.
382384
383 :since: 1.7385 :since: 1.7
384 """386 """
385387
386 arguments = [388 arguments = [
387 (b"id", amp.Unicode()),389 (b"id", amp32.Unicode()),
388 ]390 ]
389 response = []391 response = []
390 error = []392 error = []
391393
392394
393class EvaluateTag(amp.Command):395class EvaluateTag(amp32.Command):
394 """Evaluate a tag against all of the cluster's nodes.396 """Evaluate a tag against all of the cluster's nodes.
395397
396 :since: 1.7398 :since: 1.7
397 """399 """
398400
399 arguments = [401 arguments = [
400 (b"tag_name", amp.Unicode()),402 (b"tag_name", amp32.Unicode()),
401 (b"tag_definition", amp.Unicode()),403 (b"tag_definition", amp32.Unicode()),
402 (b"tag_nsmap", amp.AmpList([404 (b"tag_nsmap", amp32.AmpList([
403 (b"prefix", amp.Unicode()),405 (b"prefix", amp32.Unicode()),
404 (b"uri", amp.Unicode()),406 (b"uri", amp32.Unicode()),
405 ])),407 ])),
406 # A 3-part credential string for the web API.408 # A 3-part credential string for the web API.
407 (b"credentials", amp.Unicode()),409 (b"credentials", amp32.Unicode()),
408 ]410 ]
409 response = []411 response = []
410 errors = []412 errors = []
411413
412414
413class AddVirsh(amp.Command):415class AddVirsh(amp32.Command):
414 """Probe for and enlist virsh VMs attached to the cluster.416 """Probe for and enlist virsh VMs attached to the cluster.
415417
416 :since: 1.7418 :since: 1.7
417 """419 """
418420
419 arguments = [421 arguments = [
420 (b"poweraddr", amp.Unicode()),422 (b"poweraddr", amp32.Unicode()),
421 (b"password", amp.Unicode(optional=True)),423 (b"password", amp32.Unicode(optional=True)),
422 ]424 ]
423 response = []425 response = []
424 errors = []426 errors = []
425427
426428
427class AddSeaMicro15k(amp.Command):429class AddSeaMicro15k(amp32.Command):
428 """Probe for and enlist seamicro15k machines attached to the cluster.430 """Probe for and enlist seamicro15k machines attached to the cluster.
429431
430 :since: 1.7432 :since: 1.7
431 """433 """
432 arguments = [434 arguments = [
433 (b"mac", amp.Unicode()),435 (b"mac", amp32.Unicode()),
434 (b"username", amp.Unicode()),436 (b"username", amp32.Unicode()),
435 (b"password", amp.Unicode()),437 (b"password", amp32.Unicode()),
436 (b"power_control", amp.Unicode(optional=True)),438 (b"power_control", amp32.Unicode(optional=True)),
437 ]439 ]
438 response = []440 response = []
439 errors = {441 errors = {
@@ -441,41 +443,41 @@
441 }443 }
442444
443445
444class EnlistNodesFromMSCM(amp.Command):446class EnlistNodesFromMSCM(amp32.Command):
445 """Probe for and enlist mscm machines attached to the cluster.447 """Probe for and enlist mscm machines attached to the cluster.
446448
447 :since: 1.7449 :since: 1.7
448 """450 """
449 arguments = [451 arguments = [
450 (b"host", amp.Unicode()),452 (b"host", amp32.Unicode()),
451 (b"username", amp.Unicode()),453 (b"username", amp32.Unicode()),
452 (b"password", amp.Unicode()),454 (b"password", amp32.Unicode()),
453 ]455 ]
454 response = []456 response = []
455 errors = {}457 errors = {}
456458
457459
458class EnlistNodesFromUCSM(amp.Command):460class EnlistNodesFromUCSM(amp32.Command):
459 """Probe for and enlist ucsm machines attached to the cluster.461 """Probe for and enlist ucsm machines attached to the cluster.
460462
461 :since: 1.7463 :since: 1.7
462 """464 """
463 arguments = [465 arguments = [
464 (b"url", amp.Unicode()),466 (b"url", amp32.Unicode()),
465 (b"username", amp.Unicode()),467 (b"username", amp32.Unicode()),
466 (b"password", amp.Unicode()),468 (b"password", amp32.Unicode()),
467 ]469 ]
468 response = []470 response = []
469 errors = {}471 errors = {}
470472
471473
472class IsImportBootImagesRunning(amp.Command):474class IsImportBootImagesRunning(amp32.Command):
473 """Check if the import boot images task is running on the cluster.475 """Check if the import boot images task is running on the cluster.
474476
475 :since: 1.7477 :since: 1.7
476 """478 """
477 arguments = []479 arguments = []
478 response = [480 response = [
479 (b"running", amp.Boolean()),481 (b"running", amp32.Boolean()),
480 ]482 ]
481 errors = {}483 errors = {}
482484
=== modified file 'src/provisioningserver/rpc/clusterservice.py'
--- src/provisioningserver/rpc/clusterservice.py 2014-11-10 15:11:58 +0000
+++ src/provisioningserver/rpc/clusterservice.py 2014-11-12 12:20:32 +0000
@@ -44,6 +44,7 @@
44from provisioningserver.logger.utils import log_call44from provisioningserver.logger.utils import log_call
45from provisioningserver.network import discover_networks45from provisioningserver.network import discover_networks
46from provisioningserver.rpc import (46from provisioningserver.rpc import (
47 amp32,
47 cluster,48 cluster,
48 common,49 common,
49 dhcp,50 dhcp,
@@ -97,7 +98,6 @@
97 ConnectionClosed,98 ConnectionClosed,
98 )99 )
99from twisted.internet.threads import deferToThread100from twisted.internet.threads import deferToThread
100from twisted.protocols import amp
101from twisted.python import log101from twisted.python import log
102from twisted.web import http102from twisted.web import http
103import twisted.web.client103import twisted.web.client
@@ -312,11 +312,11 @@
312 cancel_monitor(id)312 cancel_monitor(id)
313 return {}313 return {}
314314
315 @amp.StartTLS.responder315 @amp32.StartTLS.responder
316 def get_tls_parameters(self):316 def get_tls_parameters(self):
317 """get_tls_parameters()317 """get_tls_parameters()
318318
319 Implementation of :py:class:`~twisted.protocols.amp.StartTLS`.319 Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`.
320 """320 """
321 try:321 try:
322 from provisioningserver.rpc.testing import tls322 from provisioningserver.rpc.testing import tls
@@ -551,7 +551,7 @@
551551
552 @inlineCallbacks552 @inlineCallbacks
553 def secureConnection(self):553 def secureConnection(self):
554 yield self.callRemote(amp.StartTLS, **self.get_tls_parameters())554 yield self.callRemote(amp32.StartTLS, **self.get_tls_parameters())
555555
556 # For some weird reason (it's mentioned in Twisted's source),556 # For some weird reason (it's mentioned in Twisted's source),
557 # TLS negotiation does not complete until we do something with557 # TLS negotiation does not complete until we do something with
558558
=== modified file 'src/provisioningserver/rpc/common.py'
--- src/provisioningserver/rpc/common.py 2014-10-03 12:52:35 +0000
+++ src/provisioningserver/rpc/common.py 2014-11-12 12:20:32 +0000
@@ -19,22 +19,22 @@
19 "RPCProtocol",19 "RPCProtocol",
20]20]
2121
22from provisioningserver.rpc import amp32
22from provisioningserver.rpc.interfaces import IConnection23from provisioningserver.rpc.interfaces import IConnection
23from provisioningserver.utils.twisted import asynchronous24from provisioningserver.utils.twisted import asynchronous
24from twisted.internet.defer import Deferred25from twisted.internet.defer import Deferred
25from twisted.protocols import amp26
2627
2728class Identify(amp32.Command):
28class Identify(amp.Command):
29 """Request the identity of the remote side, e.g. its UUID.29 """Request the identity of the remote side, e.g. its UUID.
3030
31 :since: 1.531 :since: 1.5
32 """32 """
3333
34 response = [(b"ident", amp.Unicode())]34 response = [(b"ident", amp32.Unicode())]
3535
3636
37class Authenticate(amp.Command):37class Authenticate(amp32.Command):
38 """Authenticate the remote side.38 """Authenticate the remote side.
3939
40 The procedure is as follows:40 The procedure is as follows:
@@ -63,19 +63,19 @@
63 """63 """
6464
65 arguments = [65 arguments = [
66 (b"message", amp.String()),66 (b"message", amp32.String()),
67 ]67 ]
68 response = [68 response = [
69 (b"digest", amp.String()),69 (b"digest", amp32.String()),
70 (b"salt", amp.String()), # Is 'salt' the right term here?70 (b"salt", amp32.String()), # Is 'salt' the right term here?
71 ]71 ]
72 errors = []72 errors = []
7373
7474
75class Client:75class Client:
76 """Wrapper around an :class:`amp.AMP` instance.76 """Wrapper around an :class:`amp32.AMP` instance.
7777
78 Limits the API to a subset of the behaviour of :class:`amp.AMP`'s,78 Limits the API to a subset of the behaviour of :class:`amp32.AMP`'s,
79 with alterations to make it suitable for use from a thread outside79 with alterations to make it suitable for use from a thread outside
80 of the reactor.80 of the reactor.
81 """81 """
@@ -107,7 +107,7 @@
107 different stack from the caller's, e.g. when calling into the107 different stack from the caller's, e.g. when calling into the
108 Twisted reactor from a thread.108 Twisted reactor from a thread.
109109
110 :param cmd: The `amp.Command` child class representing the remote110 :param cmd: The `amp32.Command` child class representing the remote
111 method to be invoked.111 method to be invoked.
112 :param kwargs: Any parameters to the remote method. Only keyword112 :param kwargs: Any parameters to the remote method. Only keyword
113 arguments are accepted.113 arguments are accepted.
@@ -143,10 +143,10 @@
143 return hash(self._conn)143 return hash(self._conn)
144144
145145
146class RPCProtocol(amp.AMP, object):146class RPCProtocol(amp32.AMP, object):
147 """A specialisation of `amp.AMP`.147 """A specialisation of `amp32.AMP`.
148148
149 It's hard to track exactly when an `amp.AMP` protocol is connected to its149 It's hard to track exactly when an `amp32.AMP` protocol is connected to its
150 transport, or disconnected, from the "outside". It's necessary to subclass150 transport, or disconnected, from the "outside". It's necessary to subclass
151 and override `connectionMade` and `connectionLost` and signal from there,151 and override `connectionMade` and `connectionLost` and signal from there,
152 which is what this class does.152 which is what this class does.
153153
=== modified file 'src/provisioningserver/rpc/monitors.py'
--- src/provisioningserver/rpc/monitors.py 2014-10-16 11:12:48 +0000
+++ src/provisioningserver/rpc/monitors.py 2014-11-12 12:20:32 +0000
@@ -20,11 +20,13 @@
20from datetime import datetime20from datetime import datetime
2121
22from provisioningserver.logger import get_maas_logger22from provisioningserver.logger import get_maas_logger
23from provisioningserver.rpc import getRegionClient23from provisioningserver.rpc import (
24 amp32,
25 getRegionClient,
26 )
24from provisioningserver.rpc.exceptions import NoConnectionsAvailable27from provisioningserver.rpc.exceptions import NoConnectionsAvailable
25from provisioningserver.rpc.region import MonitorExpired28from provisioningserver.rpc.region import MonitorExpired
26from twisted.internet import reactor29from twisted.internet import reactor
27from twisted.protocols import amp
2830
2931
30maaslog = get_maas_logger("monitors")32maaslog = get_maas_logger("monitors")
@@ -47,7 +49,7 @@
47 monitor ID.49 monitor ID.
48 """50 """
49 for monitor in monitors:51 for monitor in monitors:
50 delay = monitor["deadline"] - datetime.now(amp.utc)52 delay = monitor["deadline"] - datetime.now(amp32.utc)
51 monitor_id = monitor["id"]53 monitor_id = monitor["id"]
52 if monitor_id in running_monitors:54 if monitor_id in running_monitors:
53 dc, _ = running_monitors.pop(monitor_id)55 dc, _ = running_monitors.pop(monitor_id)
5456
=== modified file 'src/provisioningserver/rpc/region.py'
--- src/provisioningserver/rpc/region.py 2014-10-30 11:29:22 +0000
+++ src/provisioningserver/rpc/region.py 2014-11-12 12:20:32 +0000
@@ -40,6 +40,7 @@
40 "UpdateNodePowerState",40 "UpdateNodePowerState",
41]41]
4242
43from provisioningserver.rpc import amp32
43from provisioningserver.rpc.arguments import (44from provisioningserver.rpc.arguments import (
44 Bytes,45 Bytes,
45 Choice,46 Choice,
@@ -59,10 +60,9 @@
59 NoSuchEventType,60 NoSuchEventType,
60 NoSuchNode,61 NoSuchNode,
61 )62 )
62from twisted.protocols import amp63
6364
6465class Register(amp32.Command):
65class Register(amp.Command):
66 """Register a cluster with the region controller.66 """Register a cluster with the region controller.
6767
68 This is the last part of the Authenticate and Register two-step. See68 This is the last part of the Authenticate and Register two-step. See
@@ -72,11 +72,11 @@
72 """72 """
7373
74 arguments = [74 arguments = [
75 (b"uuid", amp.Unicode()),75 (b"uuid", amp32.Unicode()),
76 (b"networks", amp.AmpList([76 (b"networks", amp32.AmpList([
77 (b"interface", amp.Unicode()),77 (b"interface", amp32.Unicode()),
78 (b"ip", amp.Unicode()),78 (b"ip", amp32.Unicode()),
79 (b"subnet_mask", amp.Unicode()),79 (b"subnet_mask", amp32.Unicode()),
80 ], optional=True)),80 ], optional=True)),
81 # The URL for the region as seen by the cluster.81 # The URL for the region as seen by the cluster.
82 (b"url", ParsedURL(optional=True)),82 (b"url", ParsedURL(optional=True)),
@@ -87,7 +87,7 @@
87 }87 }
8888
8989
90class ReportBootImages(amp.Command):90class ReportBootImages(amp32.Command):
91 """Report boot images available on the invoking cluster controller.91 """Report boot images available on the invoking cluster controller.
9292
93 :since: 1.593 :since: 1.5
@@ -95,18 +95,18 @@
9595
96 arguments = [96 arguments = [
97 # The cluster UUID.97 # The cluster UUID.
98 (b"uuid", amp.Unicode()),98 (b"uuid", amp32.Unicode()),
99 (b"images", amp.AmpList(99 (b"images", amp32.AmpList(
100 [(b"architecture", amp.Unicode()),100 [(b"architecture", amp32.Unicode()),
101 (b"subarchitecture", amp.Unicode()),101 (b"subarchitecture", amp32.Unicode()),
102 (b"release", amp.Unicode()),102 (b"release", amp32.Unicode()),
103 (b"purpose", amp.Unicode())])),103 (b"purpose", amp32.Unicode())])),
104 ]104 ]
105 response = []105 response = []
106 errors = []106 errors = []
107107
108108
109class GetBootSources(amp.Command):109class GetBootSources(amp32.Command):
110 """Report boot sources and selections for the given cluster.110 """Report boot sources and selections for the given cluster.
111111
112 :since: 1.6112 :since: 1.6
@@ -115,22 +115,22 @@
115115
116 arguments = [116 arguments = [
117 # The cluster UUID.117 # The cluster UUID.
118 (b"uuid", amp.Unicode()),118 (b"uuid", amp32.Unicode()),
119 ]119 ]
120 response = [120 response = [
121 (b"sources", amp.AmpList(121 (b"sources", amp32.AmpList(
122 [(b"url", amp.Unicode()),122 [(b"url", amp32.Unicode()),
123 (b"keyring_data", Bytes()),123 (b"keyring_data", Bytes()),
124 (b"selections", amp.AmpList(124 (b"selections", amp32.AmpList(
125 [(b"release", amp.Unicode()),125 [(b"release", amp32.Unicode()),
126 (b"arches", amp.ListOf(amp.Unicode())),126 (b"arches", amp32.ListOf(amp32.Unicode())),
127 (b"subarches", amp.ListOf(amp.Unicode())),127 (b"subarches", amp32.ListOf(amp32.Unicode())),
128 (b"labels", amp.ListOf(amp.Unicode()))]))])),128 (b"labels", amp32.ListOf(amp32.Unicode()))]))])),
129 ]129 ]
130 errors = []130 errors = []
131131
132132
133class GetBootSourcesV2(amp.Command):133class GetBootSourcesV2(amp32.Command):
134 """Report boot sources and selections for the given cluster.134 """Report boot sources and selections for the given cluster.
135135
136 Includes the new os field for the selections.136 Includes the new os field for the selections.
@@ -140,33 +140,33 @@
140140
141 arguments = [141 arguments = [
142 # The cluster UUID.142 # The cluster UUID.
143 (b"uuid", amp.Unicode()),143 (b"uuid", amp32.Unicode()),
144 ]144 ]
145 response = [145 response = [
146 (b"sources", amp.AmpList(146 (b"sources", amp32.AmpList(
147 [(b"url", amp.Unicode()),147 [(b"url", amp32.Unicode()),
148 (b"keyring_data", Bytes()),148 (b"keyring_data", Bytes()),
149 (b"selections", amp.AmpList(149 (b"selections", amp32.AmpList(
150 [(b"os", amp.Unicode()),150 [(b"os", amp32.Unicode()),
151 (b"release", amp.Unicode()),151 (b"release", amp32.Unicode()),
152 (b"arches", amp.ListOf(amp.Unicode())),152 (b"arches", amp32.ListOf(amp32.Unicode())),
153 (b"subarches", amp.ListOf(amp.Unicode())),153 (b"subarches", amp32.ListOf(amp32.Unicode())),
154 (b"labels", amp.ListOf(amp.Unicode()))]))])),154 (b"labels", amp32.ListOf(amp32.Unicode()))]))])),
155 ]155 ]
156 errors = []156 errors = []
157157
158158
159class UpdateLeases(amp.Command):159class UpdateLeases(amp32.Command):
160 """Report DHCP leases on the invoking cluster controller.160 """Report DHCP leases on the invoking cluster controller.
161161
162 :since: 1.7162 :since: 1.7
163 """163 """
164 arguments = [164 arguments = [
165 # The cluster UUID.165 # The cluster UUID.
166 (b"uuid", amp.Unicode()),166 (b"uuid", amp32.Unicode()),
167 (b"mappings", CompressedAmpList(167 (b"mappings", CompressedAmpList(
168 [(b"ip", amp.Unicode()),168 [(b"ip", amp32.Unicode()),
169 (b"mac", amp.Unicode())]))169 (b"mac", amp32.Unicode())]))
170 ]170 ]
171 response = []171 response = []
172 errors = {172 errors = {
@@ -174,7 +174,7 @@
174 }174 }
175175
176176
177class GetArchiveMirrors(amp.Command):177class GetArchiveMirrors(amp32.Command):
178 """Return the Main and Port mirrors to use.178 """Return the Main and Port mirrors to use.
179179
180 :since: 1.7180 :since: 1.7
@@ -187,7 +187,7 @@
187 errors = []187 errors = []
188188
189189
190class GetProxies(amp.Command):190class GetProxies(amp32.Command):
191 """Return the HTTP and HTTPS proxies to use.191 """Return the HTTP and HTTPS proxies to use.
192192
193 :since: 1.6193 :since: 1.6
@@ -201,7 +201,7 @@
201 errors = []201 errors = []
202202
203203
204class GetClusterStatus(amp.Command):204class GetClusterStatus(amp32.Command):
205 """Return the status of the given cluster.205 """Return the status of the given cluster.
206206
207 :since: 1.7207 :since: 1.7
@@ -209,7 +209,7 @@
209209
210 arguments = [210 arguments = [
211 # The cluster UUID.211 # The cluster UUID.
212 (b"uuid", amp.Unicode()),212 (b"uuid", amp32.Unicode()),
213 ]213 ]
214 _response_status_choices = {214 _response_status_choices = {
215 0: b"PENDING", # NODEGROUP_STATUS.PENDING215 0: b"PENDING", # NODEGROUP_STATUS.PENDING
@@ -224,7 +224,7 @@
224 }224 }
225225
226226
227class MarkNodeFailed(amp.Command):227class MarkNodeFailed(amp32.Command):
228 """Mark a node as 'broken'.228 """Mark a node as 'broken'.
229229
230 :since: 1.7230 :since: 1.7
@@ -232,9 +232,9 @@
232232
233 arguments = [233 arguments = [
234 # The node's system_id.234 # The node's system_id.
235 (b"system_id", amp.Unicode()),235 (b"system_id", amp32.Unicode()),
236 # The error description.236 # The error description.
237 (b"error_description", amp.Unicode()),237 (b"error_description", amp32.Unicode()),
238 ]238 ]
239 response = []239 response = []
240 errors = {240 errors = {
@@ -243,7 +243,7 @@
243 }243 }
244244
245245
246class ListNodePowerParameters(amp.Command):246class ListNodePowerParameters(amp32.Command):
247 """Return the list of power parameters for nodes247 """Return the list of power parameters for nodes
248 that this cluster controls.248 that this cluster controls.
249249
@@ -255,14 +255,14 @@
255255
256 arguments = [256 arguments = [
257 # The cluster UUID.257 # The cluster UUID.
258 (b"uuid", amp.Unicode()),258 (b"uuid", amp32.Unicode()),
259 ]259 ]
260 response = [260 response = [
261 (b"nodes", amp.AmpList(261 (b"nodes", amp32.AmpList(
262 [(b"system_id", amp.Unicode()),262 [(b"system_id", amp32.Unicode()),
263 (b"hostname", amp.Unicode()),263 (b"hostname", amp32.Unicode()),
264 (b"power_state", amp.Unicode()),264 (b"power_state", amp32.Unicode()),
265 (b"power_type", amp.Unicode()),265 (b"power_type", amp32.Unicode()),
266 # We can't define a tighter schema here because this is a highly266 # We can't define a tighter schema here because this is a highly
267 # variable bag of arguments from a variety of sources.267 # variable bag of arguments from a variety of sources.
268 (b"context", StructureAsJSON())])),268 (b"context", StructureAsJSON())])),
@@ -272,7 +272,7 @@
272 }272 }
273273
274274
275class UpdateNodePowerState(amp.Command):275class UpdateNodePowerState(amp32.Command):
276 """Update Node Power State.276 """Update Node Power State.
277277
278 :since: 1.7278 :since: 1.7
@@ -280,120 +280,120 @@
280280
281 arguments = [281 arguments = [
282 # The node's system_id.282 # The node's system_id.
283 (b"system_id", amp.Unicode()),283 (b"system_id", amp32.Unicode()),
284 # The node's power_state.284 # The node's power_state.
285 (b"power_state", amp.Unicode()),285 (b"power_state", amp32.Unicode()),
286 ]286 ]
287 response = []287 response = []
288 errors = {NoSuchNode: b"NoSuchNode"}288 errors = {NoSuchNode: b"NoSuchNode"}
289289
290290
291class RegisterEventType(amp.Command):291class RegisterEventType(amp32.Command):
292 """Register an event type.292 """Register an event type.
293293
294 :since: 1.7294 :since: 1.7
295 """295 """
296296
297 arguments = [297 arguments = [
298 (b"name", amp.Unicode()),298 (b"name", amp32.Unicode()),
299 (b"description", amp.Unicode()),299 (b"description", amp32.Unicode()),
300 (b"level", amp.Integer()),300 (b"level", amp32.Integer()),
301 ]301 ]
302 response = []302 response = []
303 errors = []303 errors = []
304304
305305
306class SendEvent(amp.Command):306class SendEvent(amp32.Command):
307 """Send an event.307 """Send an event.
308308
309 :since: 1.7309 :since: 1.7
310 """310 """
311311
312 arguments = [312 arguments = [
313 (b"system_id", amp.Unicode()),313 (b"system_id", amp32.Unicode()),
314 (b"type_name", amp.Unicode()),314 (b"type_name", amp32.Unicode()),
315 (b"description", amp.Unicode()),315 (b"description", amp32.Unicode()),
316 ]316 ]
317 response = []317 response = []
318 errors = {318 errors = {
319 NoSuchNode: b"NoSuchNode",319 NoSuchNode: b"NoSuchNode",
320 NoSuchEventType: b"NoSuchEventType"320 NoSuchEventType: b"NoSuchEventType"
321 }321 }
322322
323323
324class SendEventMACAddress(amp.Command):324class SendEventMACAddress(amp32.Command):
325 """Send an event.325 """Send an event.
326326
327 :since: 1.7327 :since: 1.7
328 """328 """
329329
330 arguments = [330 arguments = [
331 (b"mac_address", amp.Unicode()),331 (b"mac_address", amp32.Unicode()),
332 (b"type_name", amp.Unicode()),332 (b"type_name", amp32.Unicode()),
333 (b"description", amp.Unicode()),333 (b"description", amp32.Unicode()),
334 ]334 ]
335 response = []335 response = []
336 errors = {336 errors = {
337 NoSuchNode: b"NoSuchNode",337 NoSuchNode: b"NoSuchNode",
338 NoSuchEventType: b"NoSuchEventType"338 NoSuchEventType: b"NoSuchEventType"
339 }339 }
340340
341341
342class ReportForeignDHCPServer(amp.Command):342class ReportForeignDHCPServer(amp32.Command):
343 """Report a foreign DHCP server on the cluster's network.343 """Report a foreign DHCP server on the cluster's network.
344344
345 :since: 1.7345 :since: 1.7
346 """346 """
347347
348 arguments = [348 arguments = [
349 (b"cluster_uuid", amp.Unicode()),349 (b"cluster_uuid", amp32.Unicode()),
350 (b"interface_name", amp.Unicode()),350 (b"interface_name", amp32.Unicode()),
351 (b"foreign_dhcp_ip", amp.Unicode(optional=True)),351 (b"foreign_dhcp_ip", amp32.Unicode(optional=True)),
352 ]352 ]
353 response = []353 response = []
354 errors = []354 errors = []
355355
356356
357class GetClusterInterfaces(amp.Command):357class GetClusterInterfaces(amp32.Command):
358 """Fetch the known interfaces for a cluster from the region.358 """Fetch the known interfaces for a cluster from the region.
359359
360 :since: 1.7360 :since: 1.7
361 """361 """
362362
363 arguments = [363 arguments = [
364 (b"cluster_uuid", amp.Unicode()),364 (b"cluster_uuid", amp32.Unicode()),
365 ]365 ]
366 response = [366 response = [
367 (b"interfaces", amp.AmpList(367 (b"interfaces", amp32.AmpList(
368 [(b"name", amp.Unicode()),368 [(b"name", amp32.Unicode()),
369 (b"interface", amp.Unicode()),369 (b"interface", amp32.Unicode()),
370 (b"ip", amp.Unicode())]))370 (b"ip", amp32.Unicode())]))
371 ]371 ]
372 errors = []372 errors = []
373373
374374
375class CreateNode(amp.Command):375class CreateNode(amp32.Command):
376 """Create a node on a given cluster.376 """Create a node on a given cluster.
377377
378 :since: 1.7378 :since: 1.7
379 """379 """
380380
381 arguments = [381 arguments = [
382 (b'cluster_uuid', amp.Unicode()),382 (b'cluster_uuid', amp32.Unicode()),
383 (b'architecture', amp.Unicode()),383 (b'architecture', amp32.Unicode()),
384 (b'power_type', amp.Unicode()),384 (b'power_type', amp32.Unicode()),
385 (b'power_parameters', amp.Unicode()),385 (b'power_parameters', amp32.Unicode()),
386 (b'mac_addresses', amp.ListOf(amp.Unicode())),386 (b'mac_addresses', amp32.ListOf(amp32.Unicode())),
387 ]387 ]
388 response = [388 response = [
389 (b'system_id', amp.Unicode()),389 (b'system_id', amp32.Unicode()),
390 ]390 ]
391 errors = {391 errors = {
392 NodeAlreadyExists: b"NodeAlreadyExists",392 NodeAlreadyExists: b"NodeAlreadyExists",
393 }393 }
394394
395395
396class MonitorExpired(amp.Command):396class MonitorExpired(amp32.Command):
397 """Called by a cluster when a running monitor hits its deadline.397 """Called by a cluster when a running monitor hits its deadline.
398398
399 The original context parameter from the StartMonitors call is returned.399 The original context parameter from the StartMonitors call is returned.
@@ -402,14 +402,14 @@
402 """402 """
403403
404 arguments = [404 arguments = [
405 (b"id", amp.Unicode()),405 (b"id", amp32.Unicode()),
406 (b"context", StructureAsJSON()),406 (b"context", StructureAsJSON()),
407 ]407 ]
408 response = []408 response = []
409 errors = []409 errors = []
410410
411411
412class ReloadCluster(amp.Command):412class ReloadCluster(amp32.Command):
413 """Called by a cluster when it wants to reload its state.413 """Called by a cluster when it wants to reload its state.
414414
415 The region may respond with many different calls to the cluster415 The region may respond with many different calls to the cluster
@@ -424,30 +424,30 @@
424 """424 """
425425
426 arguments = [426 arguments = [
427 (b"cluster_uuid", amp.Unicode()),427 (b"cluster_uuid", amp32.Unicode()),
428 ]428 ]
429 response = []429 response = []
430 errors = []430 errors = []
431431
432432
433class RequestNodeInfoByMACAddress(amp.Command):433class RequestNodeInfoByMACAddress(amp32.Command):
434 """Request Node information by mac address.434 """Request Node information by mac address.
435435
436 :since: 1.7436 :since: 1.7
437 """437 """
438438
439 arguments = [439 arguments = [
440 (b"mac_address", amp.Unicode()),440 (b"mac_address", amp32.Unicode()),
441 ]441 ]
442 response = [442 response = [
443 (b"system_id", amp.Unicode()),443 (b"system_id", amp32.Unicode()),
444 (b"hostname", amp.Unicode()),444 (b"hostname", amp32.Unicode()),
445 (b"status", amp.Integer()),445 (b"status", amp32.Integer()),
446 (b"boot_type", amp.Unicode()),446 (b"boot_type", amp32.Unicode()),
447 (b"osystem", amp.Unicode()),447 (b"osystem", amp32.Unicode()),
448 (b"distro_series", amp.Unicode()),448 (b"distro_series", amp32.Unicode()),
449 (b"architecture", amp.Unicode()),449 (b"architecture", amp32.Unicode()),
450 (b"purpose", amp.Unicode()),450 (b"purpose", amp32.Unicode()),
451 ]451 ]
452 errors = {452 errors = {
453 NoSuchNode: b"NoSuchNode",453 NoSuchNode: b"NoSuchNode",
454454
=== modified file 'src/provisioningserver/rpc/testing/__init__.py'
--- src/provisioningserver/rpc/testing/__init__.py 2014-10-10 16:44:15 +0000
+++ src/provisioningserver/rpc/testing/__init__.py 2014-11-12 12:20:32 +0000
@@ -45,7 +45,10 @@
45 sentinel,45 sentinel,
46 )46 )
47import provisioningserver47import provisioningserver
48from provisioningserver.rpc import region48from provisioningserver.rpc import (
49 amp32,
50 region,
51 )
49from provisioningserver.rpc.clusterservice import (52from provisioningserver.rpc.clusterservice import (
50 Cluster,53 Cluster,
51 ClusterClient,54 ClusterClient,
@@ -80,7 +83,6 @@
80 )83 )
81from twisted.internet.protocol import Factory84from twisted.internet.protocol import Factory
82from twisted.internet.task import Clock85from twisted.internet.task import Clock
83from twisted.protocols import amp
84from twisted.python import (86from twisted.python import (
85 log,87 log,
86 reflect,88 reflect,
@@ -100,11 +102,11 @@
100 d.addCallback(command.parseResponse, protocol)102 d.addCallback(command.parseResponse, protocol)
101103
102 def eb_massage_error(error):104 def eb_massage_error(error):
103 if error.check(amp.RemoteAmpError):105 if error.check(amp32.RemoteAmpError):
104 # Convert remote errors back into local errors using the106 # Convert remote errors back into local errors using the
105 # command's error map if possible.107 # command's error map if possible.
106 error_type = command.reverseErrors.get(108 error_type = command.reverseErrors.get(
107 error.value.errorCode, amp.UnknownRemoteError)109 error.value.errorCode, amp32.UnknownRemoteError)
108 return Failure(error_type(error.value.description))110 return Failure(error_type(error.value.description))
109 else:111 else:
110 # Exceptions raised in responders that aren't declared in that112 # Exceptions raised in responders that aren't declared in that
@@ -112,7 +114,7 @@
112 # in RemoteAmpError. This is because call_responder() bypasses the114 # in RemoteAmpError. This is because call_responder() bypasses the
113 # network marshall/unmarshall steps, where these exceptions would115 # network marshall/unmarshall steps, where these exceptions would
114 # ordinarily get squashed.116 # ordinarily get squashed.
115 return Failure(amp.UnknownRemoteError("%s: %s" % (117 return Failure(amp32.UnknownRemoteError("%s: %s" % (
116 reflect.qual(error.type), reflect.safe_str(error.value))))118 reflect.qual(error.type), reflect.safe_str(error.value))))
117 d.addErrback(eb_massage_error)119 d.addErrback(eb_massage_error)
118120
@@ -261,7 +263,7 @@
261 def addEventLoop(self, protocol):263 def addEventLoop(self, protocol):
262 """Add a new stub event-loop using the given `protocol`.264 """Add a new stub event-loop using the given `protocol`.
263265
264 The `protocol` should be an instance of `amp.AMP`.266 The `protocol` should be an instance of `amp32.AMP`.
265267
266 :returns: py:class:`twisted.test.iosim.IOPump`268 :returns: py:class:`twisted.test.iosim.IOPump`
267 """269 """
@@ -281,8 +283,8 @@
281 commands = commands + (region.Authenticate,)283 commands = commands + (region.Authenticate,)
282 if region.Register not in commands:284 if region.Register not in commands:
283 commands = commands + (region.Register,)285 commands = commands + (region.Register,)
284 if amp.StartTLS not in commands:286 if amp32.StartTLS not in commands:
285 commands = commands + (amp.StartTLS,)287 commands = commands + (amp32.StartTLS,)
286 protocol_factory = make_amp_protocol_factory(*commands)288 protocol_factory = make_amp_protocol_factory(*commands)
287 protocol = protocol_factory()289 protocol = protocol_factory()
288 eventloop = self.getEventLoopName(protocol)290 eventloop = self.getEventLoopName(protocol)
289291
=== modified file 'src/provisioningserver/rpc/testing/tls.py'
--- src/provisioningserver/rpc/testing/tls.py 2014-09-03 12:23:11 +0000
+++ src/provisioningserver/rpc/testing/tls.py 2014-11-12 12:20:32 +0000
@@ -26,7 +26,7 @@
26def get_tls_parameters(private_cert_name, trust_cert_name):26def get_tls_parameters(private_cert_name, trust_cert_name):
27 """get_tls_parameters()27 """get_tls_parameters()
2828
29 Implementation of :py:class:`~twisted.protocols.amp.StartTLS`.29 Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`.
30 """30 """
31 testing = filepath.FilePath(__file__).parent()31 testing = filepath.FilePath(__file__).parent()
32 with testing.child(private_cert_name).open() as fin:32 with testing.child(private_cert_name).open() as fin:
3333
=== added file 'src/provisioningserver/rpc/tests/test_amp32.py'
--- src/provisioningserver/rpc/tests/test_amp32.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/rpc/tests/test_amp32.py 2014-11-12 12:20:32 +0000
@@ -0,0 +1,3270 @@
1# Copyright (c) 2005 Divmod, Inc.
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE.Twisted for details.
4
5"""
6Tests for L{provisioningserver.rpc.amp32}.
7"""
8
9from __future__ import (
10 absolute_import,
11 print_function,
12 # unicode_literals,
13 )
14
15str = None
16
17__metaclass__ = type
18__all__ = []
19
20import datetime
21import decimal
22from warnings import (
23 catch_warnings,
24 simplefilter,
25 )
26
27from maastesting.fixtures import TempDirectory
28from maastesting.testcase import (
29 MAASTestCase,
30 MAASTwistedRunTest,
31 )
32from provisioningserver.rpc import amp32
33from provisioningserver.rpc.testing import TwistedLoggerFixture
34from testtools.deferredruntest import assert_fails_with
35from testtools.matchers import (
36 Equals,
37 HasLength,
38 Is,
39 IsInstance,
40 StartsWith,
41 )
42from twisted.internet import (
43 defer,
44 error,
45 interfaces,
46 protocol,
47 reactor,
48 )
49from twisted.internet.address import UNIXAddress
50from twisted.internet.error import ConnectionLost
51from twisted.python import filepath
52from twisted.python.failure import Failure
53from twisted.test import iosim
54from twisted.test.proto_helpers import StringTransport
55from zope.interface import implements
56from zope.interface.verify import (
57 verifyClass,
58 verifyObject,
59 )
60
61
62try:
63 from twisted.internet import ssl
64except ImportError:
65 ssl = None
66else:
67 if not ssl.supported:
68 ssl = None
69
70if ssl is None:
71 skipSSL = "SSL not available"
72else:
73 skipSSL = None
74
75
76class AMP32TestCase(MAASTestCase):
77 """Common test class for testing AMP with 32-bit length prefixes.
78
79 All the tests in this module, as well as the implementation in `amp32`,
80 are derived from Twisted's `amp` implementation. Twisted uses Trial and
81 the AMP tests depend on it. Many of these dependencies have been recast
82 into uses of `testtools` or plain `unittest` and so on. This class remains
83 as the retirement home for the last few bits for which there are not yet
84 clean and uninvasive replacements.
85 """
86
87 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
88
89 def setUp(self):
90 super(AMP32TestCase, self).setUp()
91 # Capture all Twisted logs. Normally this isn't necessary, but these
92 # tests are very noisy without this. This is also required for
93 # getLoggedFailures().
94 self.logger = self.useFixture(TwistedLoggerFixture())
95
96 def getLoggedFailures(self, error_type):
97 """Replacement for Trial's `flushLoggedErrors`.
98
99 This only queries the log for errors, whereas `flushLoggedErrors` also
100 removes those errors from the log. The reason for this is that Twisted
101 will fail a test if any errors are logged.
102
103 In time it would make sense to adopt Trial's approach. That's too
104 invasive at present, so this remains here as a reminder.
105
106 See :py:func:`twisted.trial.testcase.TestCase.flushLoggedErrors`.
107 """
108 errors = (log for log in self.logger.logs if log["isError"])
109 failures = (log['failure'] for log in errors if 'failure' in log)
110 return list(f for f in failures if f.check(error_type))
111
112 def assertWarns(self, category, message, filename, f, *args, **kwargs):
113 """Replacement for Trial's `assertWarns`.
114
115 Trial's implementation is slightly more featureful, but this does much
116 the same. Could be recast as a :py:class:`testtools.matchers.Matcher`.
117
118 See :py:func:`twisted.trial.testcase.TestCase.assertWarns`.
119 """
120 with catch_warnings(record=True) as log:
121 simplefilter('always')
122 result = f(*args, **kwargs)
123
124 self.assertThat(log, HasLength(1))
125 [warning] = log
126 self.expectThat(category, Is(warning.category))
127 self.expectThat(warning.message, IsInstance(Warning))
128 self.expectThat(message, Equals(warning.message[0]))
129 self.expectThat(filename, StartsWith(warning.filename))
130
131 return result
132
133
134class TestProto(protocol.Protocol):
135 """
136 A trivial protocol for use in testing where a L{Protocol} is expected.
137
138 @ivar instanceId: the id of this instance
139 @ivar onConnLost: deferred that will fired when the connection is lost
140 @ivar dataToSend: data to send on the protocol
141 """
142
143 instanceCount = 0
144
145 def __init__(self, onConnLost, dataToSend):
146 self.onConnLost = onConnLost
147 self.dataToSend = dataToSend
148 self.instanceId = TestProto.instanceCount
149 TestProto.instanceCount = TestProto.instanceCount + 1
150
151 def connectionMade(self):
152 self.data = []
153 self.transport.write(self.dataToSend)
154
155 def dataReceived(self, bytes):
156 self.data.append(bytes)
157
158 def connectionLost(self, reason):
159 self.onConnLost.callback(self.data)
160
161 def __repr__(self):
162 """
163 Custom repr for testing to avoid coupling amp tests with repr from
164 L{Protocol}
165
166 Returns a string which contains a unique identifier that can be looked
167 up using the instanceId property::
168
169 <TestProto #3>
170 """
171 return "<TestProto #%d>" % (self.instanceId,)
172
173
174class SimpleSymmetricProtocol(amp32.AMP):
175
176 def sendHello(self, text):
177 return self.callRemoteString("hello", hello=text)
178
179 def amp_HELLO(self, box):
180 return amp32.Box(hello=box['hello'])
181
182 def amp_HOWDOYOUDO(self, box):
183 return amp32.QuitBox(howdoyoudo='world')
184
185
186class UnfriendlyGreeting(Exception):
187 """Greeting was insufficiently kind.
188 """
189
190
191class DeathThreat(Exception):
192 """Greeting was insufficiently kind.
193 """
194
195
196class UnknownProtocol(Exception):
197 """Asked to switch to the wrong protocol.
198 """
199
200
201class TransportPeer(amp32.Argument):
202
203 # this serves as some informal documentation for how to get variables from
204 # the protocol or your environment and pass them to methods as arguments.
205 def retrieve(self, d, name, proto):
206 return ''
207
208 def fromStringProto(self, notAString, proto):
209 return proto.transport.getPeer()
210
211 def toBox(self, name, strings, objects, proto):
212 return
213
214
215class Hello(amp32.Command):
216
217 commandName = 'hello'
218
219 arguments = [
220 ('hello', amp32.String()),
221 ('optional', amp32.Boolean(optional=True)),
222 ('print', amp32.Unicode(optional=True)),
223 ('from', TransportPeer(optional=True)),
224 ('mixedCase', amp32.String(optional=True)),
225 ('dash-arg', amp32.String(optional=True)),
226 ('underscore_arg', amp32.String(optional=True)),
227 ]
228
229 response = [
230 ('hello', amp32.String()),
231 ('print', amp32.Unicode(optional=True)),
232 ]
233
234 errors = {UnfriendlyGreeting: 'UNFRIENDLY'}
235
236 fatalErrors = {DeathThreat: 'DEAD'}
237
238
239class NoAnswerHello(Hello):
240
241 commandName = Hello.commandName
242 requiresAnswer = False
243
244
245class FutureHello(amp32.Command):
246
247 commandName = 'hello'
248 arguments = [
249 ('hello', amp32.String()),
250 ('optional', amp32.Boolean(optional=True)),
251 ('print', amp32.Unicode(optional=True)),
252 ('from', TransportPeer(optional=True)),
253 # addt'l arguments should generally be added at the end, and be
254 # optional...
255 ('bonus', amp32.String(optional=True)),
256 ]
257
258 response = [
259 ('hello', amp32.String()),
260 ('print', amp32.Unicode(optional=True)),
261 ]
262
263 errors = {UnfriendlyGreeting: 'UNFRIENDLY'}
264
265
266class WTF(amp32.Command):
267 """
268 An example of an invalid command.
269 """
270
271
272class BrokenReturn(amp32.Command):
273 """
274 An example of a perfectly good command, but the handler is going to return
275 None...
276 """
277
278 commandName = 'broken_return'
279
280
281class Goodbye(amp32.Command):
282
283 # commandName left blank on purpose: this tests implicit command names.
284 response = [('goodbye', amp32.String())]
285 responseType = amp32.QuitBox
286
287
288class Howdoyoudo(amp32.Command):
289
290 commandName = 'howdoyoudo'
291 # responseType = amp32.QuitBox
292
293
294class WaitForever(amp32.Command):
295
296 commandName = 'wait_forever'
297
298
299class GetList(amp32.Command):
300
301 commandName = 'getlist'
302 arguments = [('length', amp32.Integer())]
303 response = [('body', amp32.AmpList([('x', amp32.Integer())]))]
304
305
306class DontRejectMe(amp32.Command):
307
308 commandName = 'dontrejectme'
309 arguments = [
310 ('magicWord', amp32.Unicode()),
311 ('list', amp32.AmpList([('name', amp32.Unicode())], optional=True)),
312 ]
313 response = [('response', amp32.Unicode())]
314
315
316class SecuredPing(amp32.Command):
317
318 # XXX TODO: actually make this refuse to send over an insecure connection
319 response = [('pinged', amp32.Boolean())]
320
321
322class TestSwitchProto(amp32.ProtocolSwitchCommand):
323
324 commandName = 'Switch-Proto'
325 arguments = [
326 ('name', amp32.String()),
327 ]
328 errors = {UnknownProtocol: 'UNKNOWN'}
329
330
331class SingleUseFactory(protocol.ClientFactory):
332
333 def __init__(self, proto):
334 self.proto = proto
335 self.proto.factory = self
336
337 def buildProtocol(self, addr):
338 p, self.proto = self.proto, None
339 return p
340
341 reasonFailed = None
342
343 def clientConnectionFailed(self, connector, reason):
344 self.reasonFailed = reason
345 return
346
347
348THING_I_DONT_UNDERSTAND = 'gwebol nargo'
349
350
351class ThingIDontUnderstandError(Exception):
352 pass
353
354
355class FactoryNotifier(amp32.AMP):
356
357 factory = None
358
359 def connectionMade(self):
360 if self.factory is not None:
361 self.factory.theProto = self
362 if hasattr(self.factory, 'onMade'):
363 self.factory.onMade.callback(None)
364
365 @SecuredPing.responder
366 def emitpong(self):
367 from twisted.internet.interfaces import ISSLTransport
368 if not ISSLTransport.providedBy(self.transport):
369 raise DeathThreat("only send secure pings over secure channels")
370 return {'pinged': True}
371
372
373class SimpleSymmetricCommandProtocol(FactoryNotifier):
374
375 maybeLater = None
376
377 def __init__(self, onConnLost=None):
378 amp32.AMP.__init__(self)
379 self.onConnLost = onConnLost
380
381 def sendHello(self, text):
382 return self.callRemote(Hello, hello=text)
383
384 def sendUnicodeHello(self, text, translation):
385 return self.callRemote(Hello, hello=text, Print=translation)
386
387 greeted = False
388
389 @Hello.responder
390 def cmdHello(self, hello, From, optional=None, Print=None,
391 mixedCase=None, dash_arg=None, underscore_arg=None):
392 assert From == self.transport.getPeer()
393 if hello == THING_I_DONT_UNDERSTAND:
394 raise ThingIDontUnderstandError()
395 if hello.startswith('fuck'):
396 raise UnfriendlyGreeting("Don't be a dick.")
397 if hello == 'die':
398 raise DeathThreat("aieeeeeeeee")
399 result = dict(hello=hello)
400 if Print is not None:
401 result.update(dict(Print=Print))
402 self.greeted = True
403 return result
404
405 @GetList.responder
406 def cmdGetlist(self, length):
407 return {'body': [dict(x=1)] * length}
408
409 @DontRejectMe.responder
410 def okiwont(self, magicWord, list=None):
411 if list is None:
412 response = u'list omitted'
413 else:
414 response = u'%s accepted' % (list[0]['name'])
415 return dict(response=response)
416
417 @WaitForever.responder
418 def waitforit(self):
419 self.waiting = defer.Deferred()
420 return self.waiting
421
422 @Howdoyoudo.responder
423 def howdo(self):
424 return dict(howdoyoudo='world')
425
426 @Goodbye.responder
427 def saybye(self):
428 return dict(goodbye="everyone")
429
430 def switchToTestProtocol(self, fail=False):
431 if fail:
432 name = 'no-proto'
433 else:
434 name = 'test-proto'
435 p = TestProto(self.onConnLost, SWITCH_CLIENT_DATA)
436 return self.callRemote(
437 TestSwitchProto,
438 SingleUseFactory(p), name=name).addCallback(lambda ign: p)
439
440 @TestSwitchProto.responder
441 def switchit(self, name):
442 if name == 'test-proto':
443 return TestProto(self.onConnLost, SWITCH_SERVER_DATA)
444 raise UnknownProtocol(name)
445
446 @BrokenReturn.responder
447 def donothing(self):
448 return None
449
450
451class DeferredSymmetricCommandProtocol(SimpleSymmetricCommandProtocol):
452
453 @TestSwitchProto.responder
454 def switchit(self, name):
455 if name == 'test-proto':
456 self.maybeLaterProto = TestProto(
457 self.onConnLost, SWITCH_SERVER_DATA)
458 self.maybeLater = defer.Deferred()
459 return self.maybeLater
460 raise UnknownProtocol(name)
461
462
463class BadNoAnswerCommandProtocol(SimpleSymmetricCommandProtocol):
464
465 @NoAnswerHello.responder
466 def badResponder(
467 self, hello, From, optional=None, Print=None, mixedCase=None,
468 dash_arg=None, underscore_arg=None):
469 """
470 This responder does nothing and forgets to return a dictionary.
471 """
472
473
474class NoAnswerCommandProtocol(SimpleSymmetricCommandProtocol):
475
476 @NoAnswerHello.responder
477 def goodNoAnswerResponder(
478 self, hello, From, optional=None, Print=None,
479 mixedCase=None, dash_arg=None, underscore_arg=None):
480 return dict(hello=hello + "-noanswer")
481
482
483def connectedServerAndClient(
484 ServerClass=SimpleSymmetricProtocol,
485 ClientClass=SimpleSymmetricProtocol,
486 *a, **kw):
487 """Returns a 3-tuple: (client, server, pump)
488 """
489 return iosim.connectedServerAndClient(
490 ServerClass, ClientClass,
491 *a, **kw)
492
493
494class TotallyDumbProtocol(protocol.Protocol):
495
496 buf = ''
497
498 def dataReceived(self, data):
499 self.buf += data
500
501
502class LiteralAmp(amp32.AMP):
503
504 def __init__(self):
505 self.boxes = []
506
507 def ampBoxReceived(self, box):
508 self.boxes.append(box)
509 return
510
511
512class AmpBoxTests(AMP32TestCase):
513 """
514 Test a few essential properties of AMP boxes, mostly with respect to
515 serialization correctness.
516 """
517
518 def test_serializeStr(self):
519 """
520 Make sure that strs serialize to strs.
521 """
522 a = amp32.AmpBox(key='value')
523 self.assertEqual(type(a.serialize()), bytes)
524
525 def test_serializeUnicodeKeyRaises(self):
526 """
527 Verify that TypeError is raised when trying to serialize Unicode keys.
528 """
529 a = amp32.AmpBox(**{u'key': 'value'})
530 self.assertRaises(TypeError, a.serialize)
531
532 def test_serializeUnicodeValueRaises(self):
533 """
534 Verify that TypeError is raised when trying to serialize Unicode
535 values.
536 """
537 a = amp32.AmpBox(key=u'value')
538 self.assertRaises(TypeError, a.serialize)
539
540
541class ParsingTest(AMP32TestCase):
542
543 def test_booleanValues(self):
544 """
545 Verify that the Boolean parser parses 'True' and 'False', but nothing
546 else.
547 """
548 b = amp32.Boolean()
549 self.assertEqual(b.fromString("True"), True)
550 self.assertEqual(b.fromString("False"), False)
551 self.assertRaises(TypeError, b.fromString, "ninja")
552 self.assertRaises(TypeError, b.fromString, "true")
553 self.assertRaises(TypeError, b.fromString, "TRUE")
554 self.assertEqual(b.toString(True), 'True')
555 self.assertEqual(b.toString(False), 'False')
556
557 def test_pathValueRoundTrip(self):
558 """
559 Verify the 'Path' argument can parse and emit a file path.
560 """
561 with TempDirectory() as tempdir:
562 fp = filepath.FilePath(tempdir.path)
563 p = amp32.Path()
564 s = p.toString(fp)
565 v = p.fromString(s)
566 self.assertIsNot(fp, v) # sanity check
567 self.assertEqual(fp, v)
568
569 def test_sillyEmptyThing(self):
570 """
571 Test that empty boxes raise an error; they aren't supposed to be sent
572 on purpose.
573 """
574 a = amp32.AMP()
575 return self.assertRaises(
576 amp32.NoEmptyBoxes, a.ampBoxReceived, amp32.Box())
577
578 def test_ParsingRoundTrip(self):
579 """
580 Verify that various kinds of data make it through the encode/parse
581 round-trip unharmed.
582 """
583 c, s, p = connectedServerAndClient(
584 ClientClass=LiteralAmp, ServerClass=LiteralAmp)
585
586 SIMPLE = ('simple', 'test')
587 CE = ('ceq', ': ')
588 CR = ('crtest', 'test\r')
589 LF = ('lftest', 'hello\n')
590 NEWLINE = ('newline', 'test\r\none\r\ntwo')
591 NEWLINE2 = ('newline2', 'test\r\none\r\n two')
592 BODYTEST = ('body', 'blah\r\n\r\ntesttest')
593
594 testData = [
595 [SIMPLE],
596 [SIMPLE, BODYTEST],
597 [SIMPLE, CE],
598 [SIMPLE, CR],
599 [SIMPLE, CE, CR, LF],
600 [CE, CR, LF],
601 [SIMPLE, NEWLINE, CE, NEWLINE2],
602 [BODYTEST, SIMPLE, NEWLINE]
603 ]
604
605 for test in testData:
606 jb = amp32.Box()
607 jb.update(dict(test))
608 jb._sendTo(c)
609 p.flush()
610 self.assertEqual(s.boxes[-1], jb)
611
612
613class FakeLocator(object):
614 """
615 This is a fake implementation of the interface implied by
616 L{CommandLocator}.
617 """
618
619 def __init__(self):
620 """
621 Remember the given keyword arguments as a set of responders.
622 """
623 self.commands = {}
624
625 def locateResponder(self, commandName):
626 """
627 Look up and return a function passed as a keyword argument of the given
628 name to the constructor.
629 """
630 return self.commands[commandName]
631
632
633class FakeSender:
634 """
635 This is a fake implementation of the 'box sender' interface implied by
636 L{AMP}.
637 """
638
639 def __init__(self):
640 """
641 Create a fake sender and initialize the list of received boxes and
642 unhandled errors.
643 """
644 self.sentBoxes = []
645 self.unhandledErrors = []
646 self.expectedErrors = 0
647
648 def expectError(self):
649 """
650 Expect one error, so that the test doesn't fail.
651 """
652 self.expectedErrors += 1
653
654 def sendBox(self, box):
655 """
656 Accept a box, but don't do anything.
657 """
658 self.sentBoxes.append(box)
659
660 def unhandledError(self, failure):
661 """
662 Deal with failures by instantly re-raising them for easier debugging.
663 """
664 self.expectedErrors -= 1
665 if self.expectedErrors < 0:
666 failure.raiseException()
667 else:
668 self.unhandledErrors.append(failure)
669
670
671class CommandDispatchTests(AMP32TestCase):
672 """
673 The AMP CommandDispatcher class dispatches converts AMP boxes into commands
674 and responses using Command.responder decorator.
675
The diff has been truncated for viewing.