Merge lp:~allenap/maas/more-better-things--bug-1389007 into lp:~maas-committers/maas/trunk
- more-better-things--bug-1389007
- Merge into trunk
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 |
Related bugs: |
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-
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?
Christian Reis (kiko) wrote : | # |
AMT on my mind, I obviously menat AMP.
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.
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.
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.
- 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.
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.
> 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.
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!
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.
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.
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://
Canonical VP Hyperscale | [+55 16] 9 9112 6430 | http://
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://
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://
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.
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 :)
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:/
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
1 | === added file 'LICENSE.Twisted' | |||
2 | --- LICENSE.Twisted 1970-01-01 00:00:00 +0000 | |||
3 | +++ LICENSE.Twisted 2014-11-12 12:20:32 +0000 | |||
4 | @@ -0,0 +1,65 @@ | |||
5 | 1 | Copyright (c) 2001-2014 | ||
6 | 2 | Allen Short | ||
7 | 3 | Andy Gayton | ||
8 | 4 | Andrew Bennetts | ||
9 | 5 | Antoine Pitrou | ||
10 | 6 | Apple Computer, Inc. | ||
11 | 7 | Ashwini Oruganti | ||
12 | 8 | Benjamin Bruheim | ||
13 | 9 | Bob Ippolito | ||
14 | 10 | Canonical Limited | ||
15 | 11 | Christopher Armstrong | ||
16 | 12 | David Reid | ||
17 | 13 | Donovan Preston | ||
18 | 14 | Eric Mangold | ||
19 | 15 | Eyal Lotem | ||
20 | 16 | Google Inc. | ||
21 | 17 | Hybrid Logic Ltd. | ||
22 | 18 | Hynek Schlawack | ||
23 | 19 | Itamar Turner-Trauring | ||
24 | 20 | James Knight | ||
25 | 21 | Jason A. Mobarak | ||
26 | 22 | Jean-Paul Calderone | ||
27 | 23 | Jessica McKellar | ||
28 | 24 | Jonathan Jacobs | ||
29 | 25 | Jonathan Lange | ||
30 | 26 | Jonathan D. Simms | ||
31 | 27 | Jürgen Hermann | ||
32 | 28 | Julian Berman | ||
33 | 29 | Kevin Horn | ||
34 | 30 | Kevin Turner | ||
35 | 31 | Laurens Van Houtven | ||
36 | 32 | Mary Gardiner | ||
37 | 33 | Matthew Lefkowitz | ||
38 | 34 | Massachusetts Institute of Technology | ||
39 | 35 | Moshe Zadka | ||
40 | 36 | Paul Swartz | ||
41 | 37 | Pavel Pergamenshchik | ||
42 | 38 | Ralph Meijer | ||
43 | 39 | Richard Wall | ||
44 | 40 | Sean Riley | ||
45 | 41 | Software Freedom Conservancy | ||
46 | 42 | Travis B. Hartwell | ||
47 | 43 | Thijs Triemstra | ||
48 | 44 | Thomas Herve | ||
49 | 45 | Timothy Allen | ||
50 | 46 | Tom Prince | ||
51 | 47 | |||
52 | 48 | Permission is hereby granted, free of charge, to any person obtaining | ||
53 | 49 | a copy of this software and associated documentation files (the | ||
54 | 50 | "Software"), to deal in the Software without restriction, including | ||
55 | 51 | without limitation the rights to use, copy, modify, merge, publish, | ||
56 | 52 | distribute, sublicense, and/or sell copies of the Software, and to | ||
57 | 53 | permit persons to whom the Software is furnished to do so, subject to | ||
58 | 54 | the following conditions: | ||
59 | 55 | |||
60 | 56 | The above copyright notice and this permission notice shall be | ||
61 | 57 | included in all copies or substantial portions of the Software. | ||
62 | 58 | |||
63 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
64 | 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
65 | 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
66 | 62 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
67 | 63 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
68 | 64 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
69 | 65 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
70 | 0 | 66 | ||
71 | === modified file 'docs/development/rpc.rst' | |||
72 | --- docs/development/rpc.rst 2014-08-21 20:10:47 +0000 | |||
73 | +++ docs/development/rpc.rst 2014-11-12 12:20:32 +0000 | |||
74 | @@ -5,7 +5,11 @@ | |||
75 | 5 | 5 | ||
76 | 6 | MAAS contains an RPC mechanism such that every process in the region is | 6 | MAAS contains an RPC mechanism such that every process in the region is |
77 | 7 | connected to every process in the cluster (strictly, every pserv | 7 | connected to every process in the cluster (strictly, every pserv |
79 | 8 | process). It's based on AMP_, specifically `Twisted's implementation`_. | 8 | process). It's based on AMP_, specifically `Twisted's implementation`_, |
80 | 9 | but MAAS incorporates an extended version that can transmit much larger | ||
81 | 10 | messages by using 32-bit length prefixes in place of 16-bit. This makes | ||
82 | 11 | it on-the-wire incompatible with standard AMP_ implementations, but its | ||
83 | 12 | essential behaviour is identical. | ||
84 | 9 | 13 | ||
85 | 10 | .. _AMP: | 14 | .. _AMP: |
86 | 11 | http://amp-protocol.net/ | 15 | http://amp-protocol.net/ |
87 | @@ -17,22 +21,22 @@ | |||
88 | 17 | Where do I start? | 21 | Where do I start? |
89 | 18 | ----------------- | 22 | ----------------- |
90 | 19 | 23 | ||
93 | 20 | Start in the :py:mod:`provisioningserver.rpc` package. The first two files to | 24 | Start in the :py:mod:`provisioningserver.rpc` package. The first two |
94 | 21 | look at are ``cluster.py`` and ``region.py``. This contain the | 25 | files to look at are ``cluster.py`` and ``region.py``. This contains the |
95 | 22 | declarations of what commands are available on clusters and regions | 26 | declarations of what commands are available on clusters and regions |
96 | 23 | respectively. | 27 | respectively. |
97 | 24 | 28 | ||
98 | 25 | A new command could be declared like so:: | 29 | A new command could be declared like so:: |
99 | 26 | 30 | ||
101 | 27 | from twisted.protocols import amp | 31 | from provisioningserver.rpc import amp32 |
102 | 28 | 32 | ||
104 | 29 | class EatCheez(amp.Command): | 33 | class EatCheez(amp32.Command): |
105 | 30 | arguments = [ | 34 | arguments = [ |
108 | 31 | (b"name", amp.Unicode()), | 35 | (b"name", amp32.Unicode()), |
109 | 32 | (b"origin", amp.Unicode()), | 36 | (b"origin", amp32.Unicode()), |
110 | 33 | ] | 37 | ] |
111 | 34 | response = [ | 38 | response = [ |
113 | 35 | (b"rating", amp.Integer()), | 39 | (b"rating", amp32.Integer()), |
114 | 36 | ] | 40 | ] |
115 | 37 | 41 | ||
116 | 38 | It's also possible to map exceptions across the wire using an ``errors`` | 42 | It's also possible to map exceptions across the wire using an ``errors`` |
117 | 39 | 43 | ||
118 | === modified file 'scripts/ampclient.py' | |||
119 | --- scripts/ampclient.py 2014-01-31 16:15:00 +0000 | |||
120 | +++ scripts/ampclient.py 2014-11-12 12:20:32 +0000 | |||
121 | @@ -16,6 +16,7 @@ | |||
122 | 16 | 16 | ||
123 | 17 | import sys | 17 | import sys |
124 | 18 | 18 | ||
125 | 19 | from provisioningserver.rpc.amp32 import AMP | ||
126 | 19 | from provisioningserver.rpc.cluster import ListBootImages | 20 | from provisioningserver.rpc.cluster import ListBootImages |
127 | 20 | from provisioningserver.rpc.region import ReportBootImages | 21 | from provisioningserver.rpc.region import ReportBootImages |
128 | 21 | from twisted.internet import reactor | 22 | from twisted.internet import reactor |
129 | @@ -23,7 +24,6 @@ | |||
130 | 23 | connectProtocol, | 24 | connectProtocol, |
131 | 24 | TCP4ClientEndpoint, | 25 | TCP4ClientEndpoint, |
132 | 25 | ) | 26 | ) |
133 | 26 | from twisted.protocols.amp import AMP | ||
134 | 27 | 27 | ||
135 | 28 | 28 | ||
136 | 29 | def callRemote(command, port, **kwargs): | 29 | def callRemote(command, port, **kwargs): |
137 | 30 | 30 | ||
138 | === modified file 'src/maasserver/clusterrpc/power.py' | |||
139 | --- src/maasserver/clusterrpc/power.py 2014-09-16 21:00:33 +0000 | |||
140 | +++ src/maasserver/clusterrpc/power.py 2014-11-12 12:20:32 +0000 | |||
141 | @@ -38,7 +38,7 @@ | |||
142 | 38 | Nodes can be in any cluster; the power calls will be directed to their | 38 | Nodes can be in any cluster; the power calls will be directed to their |
143 | 39 | owning cluster. | 39 | owning cluster. |
144 | 40 | 40 | ||
146 | 41 | :param command: The `amp.Command` to call. | 41 | :param command: The `amp32.Command` to call. |
147 | 42 | :param nodes: A sequence of ``(system-id, hostname, cluster-uuid, | 42 | :param nodes: A sequence of ``(system-id, hostname, cluster-uuid, |
148 | 43 | power-info)`` tuples. | 43 | power-info)`` tuples. |
149 | 44 | :returns: A mapping of each node's system ID to a | 44 | :returns: A mapping of each node's system ID to a |
150 | 45 | 45 | ||
151 | === modified file 'src/maasserver/clusterrpc/utils.py' | |||
152 | --- src/maasserver/clusterrpc/utils.py 2014-10-08 09:43:51 +0000 | |||
153 | +++ src/maasserver/clusterrpc/utils.py 2014-11-12 12:20:32 +0000 | |||
154 | @@ -38,7 +38,7 @@ | |||
155 | 38 | 38 | ||
156 | 39 | :param nodegroups: The :class:`NodeGroup`s on which to make the RPC | 39 | :param nodegroups: The :class:`NodeGroup`s on which to make the RPC |
157 | 40 | call. If None, defaults to all :class:`NodeGroup`s. | 40 | call. If None, defaults to all :class:`NodeGroup`s. |
159 | 41 | :param command: An :class:`amp.Command` to call on the clusters. | 41 | :param command: An :class:`amp32.Command` to call on the clusters. |
160 | 42 | :param ignore_errors: If True, errors encountered whilst calling | 42 | :param ignore_errors: If True, errors encountered whilst calling |
161 | 43 | `command` on the clusters won't raise an exception. | 43 | `command` on the clusters won't raise an exception. |
162 | 44 | :return: A generator of results, i.e. the dicts returned by the RPC | 44 | :return: A generator of results, i.e. the dicts returned by the RPC |
163 | 45 | 45 | ||
164 | === modified file 'src/maasserver/models/node.py' | |||
165 | --- src/maasserver/models/node.py 2014-11-10 03:22:59 +0000 | |||
166 | +++ src/maasserver/models/node.py 2014-11-12 12:20:32 +0000 | |||
167 | @@ -111,6 +111,7 @@ | |||
168 | 111 | from piston.models import Token | 111 | from piston.models import Token |
169 | 112 | from provisioningserver.logger import get_maas_logger | 112 | from provisioningserver.logger import get_maas_logger |
170 | 113 | from provisioningserver.power.poweraction import UnknownPowerType | 113 | from provisioningserver.power.poweraction import UnknownPowerType |
171 | 114 | from provisioningserver.rpc import amp32 | ||
172 | 114 | from provisioningserver.rpc.cluster import ( | 115 | from provisioningserver.rpc.cluster import ( |
173 | 115 | CancelMonitor, | 116 | CancelMonitor, |
174 | 116 | StartMonitors, | 117 | StartMonitors, |
175 | @@ -118,7 +119,6 @@ | |||
176 | 118 | from provisioningserver.rpc.power import QUERY_POWER_TYPES | 119 | from provisioningserver.rpc.power import QUERY_POWER_TYPES |
177 | 119 | from provisioningserver.utils.enum import map_enum_reverse | 120 | from provisioningserver.utils.enum import map_enum_reverse |
178 | 120 | from provisioningserver.utils.twisted import asynchronous | 121 | from provisioningserver.utils.twisted import asynchronous |
179 | 121 | from twisted.protocols import amp | ||
180 | 122 | 122 | ||
181 | 123 | 123 | ||
182 | 124 | maaslog = get_maas_logger("node") | 124 | maaslog = get_maas_logger("node") |
183 | @@ -584,7 +584,7 @@ | |||
184 | 584 | 'node_status': self.status, | 584 | 'node_status': self.status, |
185 | 585 | 'timeout': timeout, | 585 | 'timeout': timeout, |
186 | 586 | } | 586 | } |
188 | 587 | deadline = datetime.now(tz=amp.utc) + timedelta(seconds=timeout) | 587 | deadline = datetime.now(tz=amp32.utc) + timedelta(seconds=timeout) |
189 | 588 | monitors = [{ | 588 | monitors = [{ |
190 | 589 | 'deadline': deadline, | 589 | 'deadline': deadline, |
191 | 590 | 'id': self.system_id, | 590 | 'id': self.system_id, |
192 | 591 | 591 | ||
193 | === modified file 'src/maasserver/models/tests/test_node.py' | |||
194 | --- src/maasserver/models/tests/test_node.py 2014-11-10 03:22:53 +0000 | |||
195 | +++ src/maasserver/models/tests/test_node.py 2014-11-12 12:20:32 +0000 | |||
196 | @@ -101,7 +101,10 @@ | |||
197 | 101 | ) | 101 | ) |
198 | 102 | from provisioningserver.power.poweraction import UnknownPowerType | 102 | from provisioningserver.power.poweraction import UnknownPowerType |
199 | 103 | from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS | 103 | from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS |
201 | 104 | from provisioningserver.rpc import cluster as cluster_module | 104 | from provisioningserver.rpc import ( |
202 | 105 | amp32, | ||
203 | 106 | cluster as cluster_module, | ||
204 | 107 | ) | ||
205 | 105 | from provisioningserver.rpc.cluster import StartMonitors | 108 | from provisioningserver.rpc.cluster import StartMonitors |
206 | 106 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable | 109 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
207 | 107 | from provisioningserver.rpc.power import QUERY_POWER_TYPES | 110 | from provisioningserver.rpc.power import QUERY_POWER_TYPES |
208 | @@ -114,7 +117,6 @@ | |||
209 | 114 | ) | 117 | ) |
210 | 115 | from twisted.internet import defer | 118 | from twisted.internet import defer |
211 | 116 | from twisted.internet.defer import Deferred | 119 | from twisted.internet.defer import Deferred |
212 | 117 | from twisted.protocols import amp | ||
213 | 118 | from twisted.python.failure import Failure | 120 | from twisted.python.failure import Failure |
214 | 119 | 121 | ||
215 | 120 | 122 | ||
216 | @@ -2123,7 +2125,7 @@ | |||
217 | 2123 | 2125 | ||
218 | 2124 | def test__start_transition_monitor_starts_monitor(self): | 2126 | def test__start_transition_monitor_starts_monitor(self): |
219 | 2125 | rpc_fixture = self.prepare_rpc() | 2127 | rpc_fixture = self.prepare_rpc() |
221 | 2126 | now = datetime.now(tz=amp.utc) | 2128 | now = datetime.now(tz=amp32.utc) |
222 | 2127 | self.patch_datetime_now(now) | 2129 | self.patch_datetime_now(now) |
223 | 2128 | node = factory.make_Node() | 2130 | node = factory.make_Node() |
224 | 2129 | cluster = rpc_fixture.makeCluster(node.nodegroup, StartMonitors) | 2131 | cluster = rpc_fixture.makeCluster(node.nodegroup, StartMonitors) |
225 | @@ -2142,7 +2144,7 @@ | |||
226 | 2142 | ) | 2144 | ) |
227 | 2143 | 2145 | ||
228 | 2144 | def test__start_transition_monitor_copes_with_timeouterror(self): | 2146 | def test__start_transition_monitor_copes_with_timeouterror(self): |
230 | 2145 | now = datetime.now(tz=amp.utc) | 2147 | now = datetime.now(tz=amp32.utc) |
231 | 2146 | self.patch_datetime_now(now) | 2148 | self.patch_datetime_now(now) |
232 | 2147 | node = factory.make_Node() | 2149 | node = factory.make_Node() |
233 | 2148 | mock_client = Mock() | 2150 | mock_client = Mock() |
234 | 2149 | 2151 | ||
235 | === modified file 'src/maasserver/rpc/regionservice.py' | |||
236 | --- src/maasserver/rpc/regionservice.py 2014-11-10 15:11:58 +0000 | |||
237 | +++ src/maasserver/rpc/regionservice.py 2014-11-12 12:20:32 +0000 | |||
238 | @@ -57,6 +57,7 @@ | |||
239 | 57 | from maasserver.utils.async import transactional | 57 | from maasserver.utils.async import transactional |
240 | 58 | from netaddr import IPAddress | 58 | from netaddr import IPAddress |
241 | 59 | from provisioningserver.rpc import ( | 59 | from provisioningserver.rpc import ( |
242 | 60 | amp32, | ||
243 | 60 | cluster, | 61 | cluster, |
244 | 61 | common, | 62 | common, |
245 | 62 | exceptions, | 63 | exceptions, |
246 | @@ -86,7 +87,6 @@ | |||
247 | 86 | from twisted.internet.error import ConnectionClosed | 87 | from twisted.internet.error import ConnectionClosed |
248 | 87 | from twisted.internet.protocol import Factory | 88 | from twisted.internet.protocol import Factory |
249 | 88 | from twisted.internet.threads import deferToThread | 89 | from twisted.internet.threads import deferToThread |
250 | 89 | from twisted.protocols import amp | ||
251 | 90 | from twisted.python import log | 90 | from twisted.python import log |
252 | 91 | from zope.interface import implementer | 91 | from zope.interface import implementer |
253 | 92 | 92 | ||
254 | @@ -151,11 +151,11 @@ | |||
255 | 151 | """ | 151 | """ |
256 | 152 | return deferToThread(leases.update_leases, uuid, mappings) | 152 | return deferToThread(leases.update_leases, uuid, mappings) |
257 | 153 | 153 | ||
259 | 154 | @amp.StartTLS.responder | 154 | @amp32.StartTLS.responder |
260 | 155 | def get_tls_parameters(self): | 155 | def get_tls_parameters(self): |
261 | 156 | """get_tls_parameters() | 156 | """get_tls_parameters() |
262 | 157 | 157 | ||
264 | 158 | Implementation of :py:class:`~twisted.protocols.amp.StartTLS`. | 158 | Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`. |
265 | 159 | """ | 159 | """ |
266 | 160 | try: | 160 | try: |
267 | 161 | from provisioningserver.rpc.testing import tls | 161 | from provisioningserver.rpc.testing import tls |
268 | 162 | 162 | ||
269 | === modified file 'src/maasserver/rpc/testing/fixtures.py' | |||
270 | --- src/maasserver/rpc/testing/fixtures.py 2014-10-08 21:59:23 +0000 | |||
271 | +++ src/maasserver/rpc/testing/fixtures.py 2014-11-12 12:20:32 +0000 | |||
272 | @@ -191,7 +191,7 @@ | |||
273 | 191 | def addCluster(self, protocol): | 191 | def addCluster(self, protocol): |
274 | 192 | """Add a new stub cluster using the given `protocol`. | 192 | """Add a new stub cluster using the given `protocol`. |
275 | 193 | 193 | ||
277 | 194 | The `protocol` should be an instance of `amp.AMP`. | 194 | The `protocol` should be an instance of `amp32.AMP`. |
278 | 195 | 195 | ||
279 | 196 | :returns: py:class:`twisted.test.iosim.IOPump` | 196 | :returns: py:class:`twisted.test.iosim.IOPump` |
280 | 197 | """ | 197 | """ |
281 | @@ -315,7 +315,7 @@ | |||
282 | 315 | def addCluster(self, protocol): | 315 | def addCluster(self, protocol): |
283 | 316 | """Add a new stub cluster using the given `protocol`. | 316 | """Add a new stub cluster using the given `protocol`. |
284 | 317 | 317 | ||
286 | 318 | The `protocol` should be an instance of `amp.AMP`. | 318 | The `protocol` should be an instance of `amp32.AMP`. |
287 | 319 | 319 | ||
288 | 320 | :returns: A `Deferred` that fires with the connected protocol | 320 | :returns: A `Deferred` that fires with the connected protocol |
289 | 321 | instance. | 321 | instance. |
290 | 322 | 322 | ||
291 | === modified file 'src/maasserver/rpc/tests/test_regionservice.py' | |||
292 | --- src/maasserver/rpc/tests/test_regionservice.py 2014-11-10 15:11:58 +0000 | |||
293 | +++ src/maasserver/rpc/tests/test_regionservice.py 2014-11-12 12:20:32 +0000 | |||
294 | @@ -87,6 +87,7 @@ | |||
295 | 87 | import netaddr | 87 | import netaddr |
296 | 88 | from provisioningserver.network import discover_networks | 88 | from provisioningserver.network import discover_networks |
297 | 89 | from provisioningserver.rpc import ( | 89 | from provisioningserver.rpc import ( |
298 | 90 | amp32, | ||
299 | 90 | cluster, | 91 | cluster, |
300 | 91 | common, | 92 | common, |
301 | 92 | exceptions, | 93 | exceptions, |
302 | @@ -163,7 +164,6 @@ | |||
303 | 163 | from twisted.internet.interfaces import IStreamServerEndpoint | 164 | from twisted.internet.interfaces import IStreamServerEndpoint |
304 | 164 | from twisted.internet.protocol import Factory | 165 | from twisted.internet.protocol import Factory |
305 | 165 | from twisted.internet.threads import deferToThread | 166 | from twisted.internet.threads import deferToThread |
306 | 166 | from twisted.protocols import amp | ||
307 | 167 | from twisted.python import log | 167 | from twisted.python import log |
308 | 168 | from twisted.python.failure import Failure | 168 | from twisted.python.failure import Failure |
309 | 169 | from zope.interface.verify import verifyObject | 169 | from zope.interface.verify import verifyObject |
310 | @@ -284,7 +284,7 @@ | |||
311 | 284 | 284 | ||
312 | 285 | def test_StartTLS_is_registered(self): | 285 | def test_StartTLS_is_registered(self): |
313 | 286 | protocol = Region() | 286 | protocol = Region() |
315 | 287 | responder = protocol.locateResponder(amp.StartTLS.commandName) | 287 | responder = protocol.locateResponder(amp32.StartTLS.commandName) |
316 | 288 | self.assertIsNotNone(responder) | 288 | self.assertIsNotNone(responder) |
317 | 289 | 289 | ||
318 | 290 | def test_get_tls_parameters_returns_parameters(self): | 290 | def test_get_tls_parameters_returns_parameters(self): |
319 | @@ -292,7 +292,7 @@ | |||
320 | 292 | # However, locateResponder() returns a closure, so we have to | 292 | # However, locateResponder() returns a closure, so we have to |
321 | 293 | # side-step it. | 293 | # side-step it. |
322 | 294 | protocol = Region() | 294 | protocol = Region() |
324 | 295 | cls, func = protocol._commandDispatch[amp.StartTLS.commandName] | 295 | cls, func = protocol._commandDispatch[amp32.StartTLS.commandName] |
325 | 296 | self.assertThat(func(protocol), are_valid_tls_parameters) | 296 | self.assertThat(func(protocol), are_valid_tls_parameters) |
326 | 297 | 297 | ||
327 | 298 | @wait_for_reactor | 298 | @wait_for_reactor |
328 | @@ -303,7 +303,7 @@ | |||
329 | 303 | # travelling over the wire as part of an AMP message. However, | 303 | # travelling over the wire as part of an AMP message. However, |
330 | 304 | # the responder is not aware of this, and is called just like | 304 | # the responder is not aware of this, and is called just like |
331 | 305 | # any other. | 305 | # any other. |
333 | 306 | d = call_responder(Region(), amp.StartTLS, {}) | 306 | d = call_responder(Region(), amp32.StartTLS, {}) |
334 | 307 | 307 | ||
335 | 308 | def check(response): | 308 | def check(response): |
336 | 309 | self.assertEqual({}, response) | 309 | self.assertEqual({}, response) |
337 | @@ -1119,7 +1119,7 @@ | |||
338 | 1119 | service.factory.protocol = HandshakingRegionServer | 1119 | service.factory.protocol = HandshakingRegionServer |
339 | 1120 | protocol = service.factory.buildProtocol(addr=None) # addr is unused. | 1120 | protocol = service.factory.buildProtocol(addr=None) # addr is unused. |
340 | 1121 | protocol.connectionMade() | 1121 | protocol.connectionMade() |
342 | 1122 | connectionLost_up_call = self.patch(amp.AMP, "connectionLost") | 1122 | connectionLost_up_call = self.patch(amp32.AMP, "connectionLost") |
343 | 1123 | self.assertDictEqual( | 1123 | self.assertDictEqual( |
344 | 1124 | {protocol.ident: {protocol}}, | 1124 | {protocol.ident: {protocol}}, |
345 | 1125 | service.connections) | 1125 | service.connections) |
346 | 1126 | 1126 | ||
347 | === modified file 'src/provisioningserver/pserv_services/dhcp_probe_service.py' | |||
348 | --- src/provisioningserver/pserv_services/dhcp_probe_service.py 2014-10-02 11:53:59 +0000 | |||
349 | +++ src/provisioningserver/pserv_services/dhcp_probe_service.py 2014-11-12 12:20:32 +0000 | |||
350 | @@ -22,6 +22,7 @@ | |||
351 | 22 | 22 | ||
352 | 23 | from provisioningserver.dhcp.detect import probe_interface | 23 | from provisioningserver.dhcp.detect import probe_interface |
353 | 24 | from provisioningserver.logger.log import get_maas_logger | 24 | from provisioningserver.logger.log import get_maas_logger |
354 | 25 | from provisioningserver.rpc.amp32 import UnhandledCommand | ||
355 | 25 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable | 26 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
356 | 26 | from provisioningserver.rpc.region import ( | 27 | from provisioningserver.rpc.region import ( |
357 | 27 | GetClusterInterfaces, | 28 | GetClusterInterfaces, |
358 | @@ -37,7 +38,6 @@ | |||
359 | 37 | returnValue, | 38 | returnValue, |
360 | 38 | ) | 39 | ) |
361 | 39 | from twisted.internet.threads import deferToThread | 40 | from twisted.internet.threads import deferToThread |
362 | 40 | from twisted.protocols.amp import UnhandledCommand | ||
363 | 41 | 41 | ||
364 | 42 | 42 | ||
365 | 43 | maaslog = get_maas_logger("dhcp.probe") | 43 | maaslog = get_maas_logger("dhcp.probe") |
366 | 44 | 44 | ||
367 | === added file 'src/provisioningserver/rpc/amp32.py' | |||
368 | --- src/provisioningserver/rpc/amp32.py 1970-01-01 00:00:00 +0000 | |||
369 | +++ src/provisioningserver/rpc/amp32.py 2014-11-12 12:20:32 +0000 | |||
370 | @@ -0,0 +1,2646 @@ | |||
371 | 1 | # Copyright (c) 2005 Divmod, Inc. | ||
372 | 2 | # Copyright (c) Twisted Matrix Laboratories. | ||
373 | 3 | # See LICENSE.Twisted for details. | ||
374 | 4 | |||
375 | 5 | """ | ||
376 | 6 | This module implements a modified version of AMP, the Asynchronous Messaging | ||
377 | 7 | Protocol, which is referred to as AMP32 from now on where it's necessary to | ||
378 | 8 | differentiate between the two. | ||
379 | 9 | |||
380 | 10 | The original AMP uses 16-bit length prefixes when sending messages, which | ||
381 | 11 | limits payloads to 64k. This version uses 32-bit prefixes so that messages can | ||
382 | 12 | be much larger, though in practice they're limit them to something much | ||
383 | 13 | smaller that the maximum. Unfortunately this makes AMP32 incompatible with | ||
384 | 14 | AMP, though their essential behaviour is identical. | ||
385 | 15 | |||
386 | 16 | AMP is a protocol for sending multiple asynchronous request/response pairs over | ||
387 | 17 | the same connection. Requests and responses are both collections of key/value | ||
388 | 18 | pairs. | ||
389 | 19 | |||
390 | 20 | AMP is a very simple protocol which is not an application. This module is a | ||
391 | 21 | "protocol construction kit" of sorts; it attempts to be the simplest wire-level | ||
392 | 22 | implementation of Deferreds. AMP provides the following base-level features: | ||
393 | 23 | |||
394 | 24 | - Asynchronous request/response handling (hence the name) | ||
395 | 25 | |||
396 | 26 | - Requests and responses are both key/value pairs | ||
397 | 27 | |||
398 | 28 | - Binary transfer of all data: all data is length-prefixed. Your | ||
399 | 29 | application will never need to worry about quoting. | ||
400 | 30 | |||
401 | 31 | - Command dispatching (like HTTP Verbs): the protocol is extensible, and | ||
402 | 32 | multiple AMP sub-protocols can be grouped together easily. | ||
403 | 33 | |||
404 | 34 | The protocol implementation also provides a few additional features which are | ||
405 | 35 | not part of the core wire protocol, but are nevertheless very useful: | ||
406 | 36 | |||
407 | 37 | - Tight TLS integration, with an included StartTLS command. | ||
408 | 38 | |||
409 | 39 | - Handshaking to other protocols: because AMP has well-defined message | ||
410 | 40 | boundaries and maintains all incoming and outgoing requests for you, you | ||
411 | 41 | can start a connection over AMP and then switch to another protocol. | ||
412 | 42 | This makes it ideal for firewall-traversal applications where you may | ||
413 | 43 | have only one forwarded port but multiple applications that want to use | ||
414 | 44 | it. | ||
415 | 45 | |||
416 | 46 | Using AMP with Twisted is simple. Each message is a command, with a response. | ||
417 | 47 | You begin by defining a command type. Commands specify their input and output | ||
418 | 48 | in terms of the types that they expect to see in the request and response | ||
419 | 49 | key-value pairs. Here's an example of a command that adds two integers, 'a' | ||
420 | 50 | and 'b':: | ||
421 | 51 | |||
422 | 52 | class Sum(amp32.Command): | ||
423 | 53 | arguments = [('a', amp32.Integer()), | ||
424 | 54 | ('b', amp32.Integer())] | ||
425 | 55 | response = [('total', amp32.Integer())] | ||
426 | 56 | |||
427 | 57 | Once you have specified a command, you need to make it part of a protocol, and | ||
428 | 58 | define a responder for it. Here's a 'JustSum' protocol that includes a | ||
429 | 59 | responder for our 'Sum' command:: | ||
430 | 60 | |||
431 | 61 | class JustSum(amp32.AMP): | ||
432 | 62 | def sum(self, a, b): | ||
433 | 63 | total = a + b | ||
434 | 64 | print 'Did a sum: %d + %d = %d' % (a, b, total) | ||
435 | 65 | return {'total': total} | ||
436 | 66 | Sum.responder(sum) | ||
437 | 67 | |||
438 | 68 | Later, when you want to actually do a sum, the following expression will return | ||
439 | 69 | a L{Deferred} which will fire with the result:: | ||
440 | 70 | |||
441 | 71 | ClientCreator(reactor, amp32.AMP).connectTCP(...).addCallback( | ||
442 | 72 | lambda p: p.callRemote(Sum, a=13, b=81)).addCallback( | ||
443 | 73 | lambda result: result['total']) | ||
444 | 74 | |||
445 | 75 | Command responders may also return Deferreds, causing the response to be | ||
446 | 76 | sent only once the Deferred fires:: | ||
447 | 77 | |||
448 | 78 | class DelayedSum(amp32.AMP): | ||
449 | 79 | def slowSum(self, a, b): | ||
450 | 80 | total = a + b | ||
451 | 81 | result = defer.Deferred() | ||
452 | 82 | reactor.callLater(3, result.callback, {'total': total}) | ||
453 | 83 | return result | ||
454 | 84 | Sum.responder(slowSum) | ||
455 | 85 | |||
456 | 86 | This is transparent to the caller. | ||
457 | 87 | |||
458 | 88 | You can also define the propagation of specific errors in AMP. For example, | ||
459 | 89 | for the slightly more complicated case of division, we might have to deal with | ||
460 | 90 | division by zero:: | ||
461 | 91 | |||
462 | 92 | class Divide(amp32.Command): | ||
463 | 93 | arguments = [('numerator', amp32.Integer()), | ||
464 | 94 | ('denominator', amp32.Integer())] | ||
465 | 95 | response = [('result', amp32.Float())] | ||
466 | 96 | errors = {ZeroDivisionError: 'ZERO_DIVISION'} | ||
467 | 97 | |||
468 | 98 | The 'errors' mapping here tells AMP that if a responder to Divide emits a | ||
469 | 99 | L{ZeroDivisionError}, then the other side should be informed that an error of | ||
470 | 100 | the type 'ZERO_DIVISION' has occurred. Writing a responder which takes | ||
471 | 101 | advantage of this is very simple - just raise your exception normally:: | ||
472 | 102 | |||
473 | 103 | class JustDivide(amp32.AMP): | ||
474 | 104 | def divide(self, numerator, denominator): | ||
475 | 105 | result = numerator / denominator | ||
476 | 106 | print 'Divided: %d / %d = %d' % (numerator, denominator, total) | ||
477 | 107 | return {'result': result} | ||
478 | 108 | Divide.responder(divide) | ||
479 | 109 | |||
480 | 110 | On the client side, the errors mapping will be used to determine what the | ||
481 | 111 | 'ZERO_DIVISION' error means, and translated into an asynchronous exception, | ||
482 | 112 | which can be handled normally as any L{Deferred} would be:: | ||
483 | 113 | |||
484 | 114 | def trapZero(result): | ||
485 | 115 | result.trap(ZeroDivisionError) | ||
486 | 116 | print "Divided by zero: returning INF" | ||
487 | 117 | return 1e1000 | ||
488 | 118 | ClientCreator(reactor, amp32.AMP).connectTCP(...).addCallback( | ||
489 | 119 | lambda p: p.callRemote(Divide, numerator=1234, | ||
490 | 120 | denominator=0) | ||
491 | 121 | ).addErrback(trapZero) | ||
492 | 122 | |||
493 | 123 | For a complete, runnable example of both of these commands, see the files in | ||
494 | 124 | the Twisted repository:: | ||
495 | 125 | |||
496 | 126 | doc/core/examples/ampserver.py | ||
497 | 127 | doc/core/examples/ampclient.py | ||
498 | 128 | |||
499 | 129 | On the wire, AMP is a protocol which uses 2-byte lengths to prefix keys and | ||
500 | 130 | values, and empty keys to separate messages:: | ||
501 | 131 | |||
502 | 132 | <2-byte length><key><2-byte length><value> | ||
503 | 133 | <2-byte length><key><2-byte length><value> | ||
504 | 134 | ... | ||
505 | 135 | <2-byte length><key><2-byte length><value> | ||
506 | 136 | <NUL><NUL> # Empty Key == End of Message | ||
507 | 137 | |||
508 | 138 | And so on. Because it's tedious to refer to lengths and NULs constantly, the | ||
509 | 139 | documentation will refer to packets as if they were newline delimited, like | ||
510 | 140 | so:: | ||
511 | 141 | |||
512 | 142 | C: _command: sum | ||
513 | 143 | C: _ask: ef639e5c892ccb54 | ||
514 | 144 | C: a: 13 | ||
515 | 145 | C: b: 81 | ||
516 | 146 | |||
517 | 147 | S: _answer: ef639e5c892ccb54 | ||
518 | 148 | S: total: 94 | ||
519 | 149 | |||
520 | 150 | Notes: | ||
521 | 151 | |||
522 | 152 | In general, the order of keys is arbitrary. Specific uses of AMP may impose an | ||
523 | 153 | ordering requirement, but unless this is specified explicitly, any ordering may | ||
524 | 154 | be generated and any ordering must be accepted. This applies to the | ||
525 | 155 | command-related keys I{_command} and I{_ask} as well as any other keys. | ||
526 | 156 | |||
527 | 157 | Values are limited to the maximum encodable size in a 32-bit length. | ||
528 | 158 | |||
529 | 159 | Keys are limited to the maximum encodable size in a 8-bit length, 255 bytes. | ||
530 | 160 | Note that we still use 2-byte lengths to encode keys. This small redundancy | ||
531 | 161 | has several features: | ||
532 | 162 | |||
533 | 163 | - If an implementation becomes confused and starts emitting corrupt data, | ||
534 | 164 | or gets keys confused with values, many common errors will be signalled | ||
535 | 165 | immediately instead of delivering obviously corrupt packets. | ||
536 | 166 | |||
537 | 167 | - A single NUL will separate every key, and a double NUL separates | ||
538 | 168 | messages. This provides some redundancy when debugging traffic dumps. | ||
539 | 169 | |||
540 | 170 | - NULs will be present at regular intervals along the protocol, providing | ||
541 | 171 | some padding for otherwise braindead C implementations of the protocol, | ||
542 | 172 | so that <stdio.h> string functions will see the NUL and stop. | ||
543 | 173 | |||
544 | 174 | - This makes it possible to run an AMP server on a port also used by a | ||
545 | 175 | plain-text protocol, and easily distinguish between non-AMP clients (like | ||
546 | 176 | web browsers) which issue non-NUL as the first byte, and AMP clients, | ||
547 | 177 | which always issue NUL as the first byte. | ||
548 | 178 | |||
549 | 179 | """ | ||
550 | 180 | |||
551 | 181 | from __future__ import ( | ||
552 | 182 | absolute_import, | ||
553 | 183 | print_function, | ||
554 | 184 | # unicode_literals, | ||
555 | 185 | ) | ||
556 | 186 | |||
557 | 187 | str = None | ||
558 | 188 | |||
559 | 189 | __metaclass__ = type | ||
560 | 190 | |||
561 | 191 | import datetime | ||
562 | 192 | import decimal | ||
563 | 193 | from io import BytesIO | ||
564 | 194 | from itertools import count | ||
565 | 195 | from struct import pack | ||
566 | 196 | import types | ||
567 | 197 | import warnings | ||
568 | 198 | |||
569 | 199 | from twisted.internet.defer import ( | ||
570 | 200 | Deferred, | ||
571 | 201 | fail, | ||
572 | 202 | maybeDeferred, | ||
573 | 203 | ) | ||
574 | 204 | from twisted.internet.error import ( | ||
575 | 205 | ConnectionClosed, | ||
576 | 206 | ConnectionLost, | ||
577 | 207 | PeerVerifyError, | ||
578 | 208 | ) | ||
579 | 209 | from twisted.internet.interfaces import IFileDescriptorReceiver | ||
580 | 210 | from twisted.internet.main import CONNECTION_LOST | ||
581 | 211 | from twisted.protocols.basic import ( | ||
582 | 212 | Int32StringReceiver, | ||
583 | 213 | StatefulStringProtocol, | ||
584 | 214 | ) | ||
585 | 215 | from twisted.python import ( | ||
586 | 216 | filepath, | ||
587 | 217 | log, | ||
588 | 218 | ) | ||
589 | 219 | from twisted.python.failure import Failure | ||
590 | 220 | from twisted.python.reflect import accumulateClassDict | ||
591 | 221 | from zope.interface import ( | ||
592 | 222 | implements, | ||
593 | 223 | Interface, | ||
594 | 224 | ) | ||
595 | 225 | |||
596 | 226 | |||
597 | 227 | try: | ||
598 | 228 | from twisted.internet import ssl | ||
599 | 229 | except ImportError: | ||
600 | 230 | ssl = None | ||
601 | 231 | else: | ||
602 | 232 | if ssl.supported: | ||
603 | 233 | from twisted.internet.ssl import ( | ||
604 | 234 | CertificateOptions, | ||
605 | 235 | Certificate, | ||
606 | 236 | DN, | ||
607 | 237 | KeyPair, | ||
608 | 238 | ) | ||
609 | 239 | else: | ||
610 | 240 | ssl = None | ||
611 | 241 | |||
612 | 242 | |||
613 | 243 | ASK = '_ask' | ||
614 | 244 | ANSWER = '_answer' | ||
615 | 245 | COMMAND = '_command' | ||
616 | 246 | ERROR = '_error' | ||
617 | 247 | ERROR_CODE = '_error_code' | ||
618 | 248 | ERROR_DESCRIPTION = '_error_description' | ||
619 | 249 | UNKNOWN_ERROR_CODE = 'UNKNOWN' | ||
620 | 250 | UNHANDLED_ERROR_CODE = 'UNHANDLED' | ||
621 | 251 | |||
622 | 252 | MAX_KEY_LENGTH = 0xff | ||
623 | 253 | MAX_VALUE_LENGTH = 2 * 1024 * 1024 # 2MiB | ||
624 | 254 | |||
625 | 255 | |||
626 | 256 | class IArgumentType(Interface): | ||
627 | 257 | """ | ||
628 | 258 | An L{IArgumentType} can serialize a Python object into an AMP box and | ||
629 | 259 | deserialize information from an AMP box back into a Python object. | ||
630 | 260 | |||
631 | 261 | @since: 9.0 | ||
632 | 262 | """ | ||
633 | 263 | |||
634 | 264 | def fromBox(name, strings, objects, proto): | ||
635 | 265 | """ | ||
636 | 266 | Given an argument name and an AMP box containing serialized values, | ||
637 | 267 | extract one or more Python objects and add them to the C{objects} | ||
638 | 268 | dictionary. | ||
639 | 269 | |||
640 | 270 | @param name: The name associated with this argument. Most commonly, | ||
641 | 271 | this is the key which can be used to find a serialized value in | ||
642 | 272 | C{strings} and which should be used as the key in C{objects} to | ||
643 | 273 | associate with a structured Python object. | ||
644 | 274 | @type name: C{bytes} | ||
645 | 275 | |||
646 | 276 | @param strings: The AMP box from which to extract one or more | ||
647 | 277 | values. | ||
648 | 278 | @type strings: C{dict} | ||
649 | 279 | |||
650 | 280 | @param objects: The output dictionary to populate with the value for | ||
651 | 281 | this argument. | ||
652 | 282 | @type objects: C{dict} | ||
653 | 283 | |||
654 | 284 | @param proto: The protocol instance which received the AMP box being | ||
655 | 285 | interpreted. Most likely this is an instance of L{AMP}, but | ||
656 | 286 | this is not guaranteed. | ||
657 | 287 | |||
658 | 288 | @return: C{None} | ||
659 | 289 | """ | ||
660 | 290 | |||
661 | 291 | def toBox(name, strings, objects, proto): | ||
662 | 292 | """ | ||
663 | 293 | Given an argument name and a dictionary containing structured Python | ||
664 | 294 | objects, serialize values into one or more strings and add them to | ||
665 | 295 | the C{strings} dictionary. | ||
666 | 296 | |||
667 | 297 | @param name: The name associated with this argument. Most commonly, | ||
668 | 298 | this is the key which can be used to find an object in | ||
669 | 299 | C{objects} and which should be used as the key in C{strings} to | ||
670 | 300 | associate with a C{bytes} giving the serialized form of that | ||
671 | 301 | object. | ||
672 | 302 | @type name: C{bytes} | ||
673 | 303 | |||
674 | 304 | @param strings: The AMP box into which to insert one or more | ||
675 | 305 | strings. | ||
676 | 306 | @type strings: C{dict} | ||
677 | 307 | |||
678 | 308 | @param objects: The input dictionary from which to extract Python | ||
679 | 309 | objects to serialize. | ||
680 | 310 | @type objects: C{dict} | ||
681 | 311 | |||
682 | 312 | @param proto: The protocol instance which will send the AMP box once | ||
683 | 313 | it is fully populated. Most likely this is an instance of | ||
684 | 314 | L{AMP}, but this is not guaranteed. | ||
685 | 315 | |||
686 | 316 | @return: C{None} | ||
687 | 317 | """ | ||
688 | 318 | |||
689 | 319 | |||
690 | 320 | class IBoxSender(Interface): | ||
691 | 321 | """ | ||
692 | 322 | A transport which can send L{AmpBox} objects. | ||
693 | 323 | """ | ||
694 | 324 | |||
695 | 325 | def sendBox(box): | ||
696 | 326 | """ | ||
697 | 327 | Send an L{AmpBox}. | ||
698 | 328 | |||
699 | 329 | @raise ProtocolSwitched: if the underlying protocol has been | ||
700 | 330 | switched. | ||
701 | 331 | |||
702 | 332 | @raise ConnectionLost: if the underlying connection has already been | ||
703 | 333 | lost. | ||
704 | 334 | """ | ||
705 | 335 | |||
706 | 336 | def unhandledError(failure): | ||
707 | 337 | """ | ||
708 | 338 | An unhandled error occurred in response to a box. Log it | ||
709 | 339 | appropriately. | ||
710 | 340 | |||
711 | 341 | @param failure: a L{Failure} describing the error that occurred. | ||
712 | 342 | """ | ||
713 | 343 | |||
714 | 344 | |||
715 | 345 | class IBoxReceiver(Interface): | ||
716 | 346 | """ | ||
717 | 347 | An application object which can receive L{AmpBox} objects and dispatch them | ||
718 | 348 | appropriately. | ||
719 | 349 | """ | ||
720 | 350 | |||
721 | 351 | def startReceivingBoxes(boxSender): | ||
722 | 352 | """ | ||
723 | 353 | The L{ampBoxReceived} method will start being called; boxes may be | ||
724 | 354 | responded to by responding to the given L{IBoxSender}. | ||
725 | 355 | |||
726 | 356 | @param boxSender: an L{IBoxSender} provider. | ||
727 | 357 | """ | ||
728 | 358 | |||
729 | 359 | def ampBoxReceived(box): | ||
730 | 360 | """ | ||
731 | 361 | A box was received from the transport; dispatch it appropriately. | ||
732 | 362 | """ | ||
733 | 363 | |||
734 | 364 | def stopReceivingBoxes(reason): | ||
735 | 365 | """ | ||
736 | 366 | No further boxes will be received on this connection. | ||
737 | 367 | |||
738 | 368 | @type reason: L{Failure} | ||
739 | 369 | """ | ||
740 | 370 | |||
741 | 371 | |||
742 | 372 | class IResponderLocator(Interface): | ||
743 | 373 | """ | ||
744 | 374 | An application object which can look up appropriate responder methods for | ||
745 | 375 | AMP commands. | ||
746 | 376 | """ | ||
747 | 377 | |||
748 | 378 | def locateResponder(name): | ||
749 | 379 | """ | ||
750 | 380 | Locate a responder method appropriate for the named command. | ||
751 | 381 | |||
752 | 382 | @param name: the wire-level name (commandName) of the AMP command to be | ||
753 | 383 | responded to. | ||
754 | 384 | |||
755 | 385 | @return: a 1-argument callable that takes an L{AmpBox} with argument | ||
756 | 386 | values for the given command, and returns an L{AmpBox} containing | ||
757 | 387 | argument values for the named command, or a L{Deferred} that fires the | ||
758 | 388 | same. | ||
759 | 389 | """ | ||
760 | 390 | |||
761 | 391 | |||
762 | 392 | class AmpError(Exception): | ||
763 | 393 | """ | ||
764 | 394 | Base class of all Amp-related exceptions. | ||
765 | 395 | """ | ||
766 | 396 | |||
767 | 397 | |||
768 | 398 | class ProtocolSwitched(Exception): | ||
769 | 399 | """ | ||
770 | 400 | Connections which have been switched to other protocols can no longer | ||
771 | 401 | accept traffic at the AMP level. This is raised when you try to send it. | ||
772 | 402 | """ | ||
773 | 403 | |||
774 | 404 | |||
775 | 405 | class OnlyOneTLS(AmpError): | ||
776 | 406 | """ | ||
777 | 407 | This is an implementation limitation; TLS may only be started once per | ||
778 | 408 | connection. | ||
779 | 409 | """ | ||
780 | 410 | |||
781 | 411 | |||
782 | 412 | class NoEmptyBoxes(AmpError): | ||
783 | 413 | """ | ||
784 | 414 | You can't have empty boxes on the connection. This is raised when you | ||
785 | 415 | receive or attempt to send one. | ||
786 | 416 | """ | ||
787 | 417 | |||
788 | 418 | |||
789 | 419 | class InvalidSignature(AmpError): | ||
790 | 420 | """ | ||
791 | 421 | You didn't pass all the required arguments. | ||
792 | 422 | """ | ||
793 | 423 | |||
794 | 424 | |||
795 | 425 | class TooLong(AmpError): | ||
796 | 426 | """ | ||
797 | 427 | One of the protocol's length limitations was violated. | ||
798 | 428 | |||
799 | 429 | @ivar isKey: true if the string being encoded in a key position, false if | ||
800 | 430 | it was in a value position. | ||
801 | 431 | |||
802 | 432 | @ivar isLocal: Was the string encoded locally, or received too long from | ||
803 | 433 | the network? (It's only physically possible to encode "too long" values on | ||
804 | 434 | the network for keys.) | ||
805 | 435 | |||
806 | 436 | @ivar value: The string that was too long. | ||
807 | 437 | |||
808 | 438 | @ivar keyName: If the string being encoded was in a value position, what | ||
809 | 439 | key was it being encoded for? | ||
810 | 440 | """ | ||
811 | 441 | |||
812 | 442 | def __init__(self, isKey, isLocal, value, keyName=None): | ||
813 | 443 | AmpError.__init__(self) | ||
814 | 444 | self.isKey = isKey | ||
815 | 445 | self.isLocal = isLocal | ||
816 | 446 | self.value = value | ||
817 | 447 | self.keyName = keyName | ||
818 | 448 | |||
819 | 449 | def __repr__(self): | ||
820 | 450 | hdr = self.isKey and "key" or "value" | ||
821 | 451 | if not self.isKey: | ||
822 | 452 | hdr += ' ' + repr(self.keyName) | ||
823 | 453 | lcl = self.isLocal and "local" or "remote" | ||
824 | 454 | return "%s %s too long: %d" % (lcl, hdr, len(self.value)) | ||
825 | 455 | |||
826 | 456 | |||
827 | 457 | class BadLocalReturn(AmpError): | ||
828 | 458 | """ | ||
829 | 459 | A bad value was returned from a local command; we were unable to coerce it. | ||
830 | 460 | """ | ||
831 | 461 | |||
832 | 462 | def __init__(self, message, enclosed): | ||
833 | 463 | AmpError.__init__(self) | ||
834 | 464 | self.message = message | ||
835 | 465 | self.enclosed = enclosed | ||
836 | 466 | |||
837 | 467 | def __repr__(self): | ||
838 | 468 | return self.message + " " + self.enclosed.getBriefTraceback() | ||
839 | 469 | |||
840 | 470 | __bytes__ = __repr__ | ||
841 | 471 | |||
842 | 472 | |||
843 | 473 | class RemoteAmpError(AmpError): | ||
844 | 474 | """ | ||
845 | 475 | This error indicates that something went wrong on the remote end of the | ||
846 | 476 | connection, and the error was serialized and transmitted to you. | ||
847 | 477 | """ | ||
848 | 478 | |||
849 | 479 | def __init__(self, errorCode, description, fatal=False, local=None): | ||
850 | 480 | """Create a remote error with an error code and description. | ||
851 | 481 | |||
852 | 482 | @param errorCode: the AMP error code of this error. | ||
853 | 483 | |||
854 | 484 | @param description: some text to show to the user. | ||
855 | 485 | |||
856 | 486 | @param fatal: a boolean, true if this error should terminate the | ||
857 | 487 | connection. | ||
858 | 488 | |||
859 | 489 | @param local: a local Failure, if one exists. | ||
860 | 490 | """ | ||
861 | 491 | if local: | ||
862 | 492 | localwhat = ' (local)' | ||
863 | 493 | othertb = local.getBriefTraceback() | ||
864 | 494 | else: | ||
865 | 495 | localwhat = '' | ||
866 | 496 | othertb = '' | ||
867 | 497 | Exception.__init__( | ||
868 | 498 | self, "Code<%s>%s: %s%s" % ( | ||
869 | 499 | errorCode, localwhat, | ||
870 | 500 | description, othertb)) | ||
871 | 501 | self.local = local | ||
872 | 502 | self.errorCode = errorCode | ||
873 | 503 | self.description = description | ||
874 | 504 | self.fatal = fatal | ||
875 | 505 | |||
876 | 506 | |||
877 | 507 | class UnknownRemoteError(RemoteAmpError): | ||
878 | 508 | """ | ||
879 | 509 | This means that an error whose type we can't identify was raised from the | ||
880 | 510 | other side. | ||
881 | 511 | """ | ||
882 | 512 | |||
883 | 513 | def __init__(self, description): | ||
884 | 514 | errorCode = UNKNOWN_ERROR_CODE | ||
885 | 515 | RemoteAmpError.__init__(self, errorCode, description) | ||
886 | 516 | |||
887 | 517 | |||
888 | 518 | class MalformedAmpBox(AmpError): | ||
889 | 519 | """ | ||
890 | 520 | This error indicates that the wire-level protocol was malformed. | ||
891 | 521 | """ | ||
892 | 522 | |||
893 | 523 | |||
894 | 524 | class UnhandledCommand(AmpError): | ||
895 | 525 | """ | ||
896 | 526 | A command received via amp could not be dispatched. | ||
897 | 527 | """ | ||
898 | 528 | |||
899 | 529 | |||
900 | 530 | class IncompatibleVersions(AmpError): | ||
901 | 531 | """ | ||
902 | 532 | It was impossible to negotiate a compatible version of the protocol with | ||
903 | 533 | the other end of the connection. | ||
904 | 534 | """ | ||
905 | 535 | |||
906 | 536 | |||
907 | 537 | PROTOCOL_ERRORS = {UNHANDLED_ERROR_CODE: UnhandledCommand} | ||
908 | 538 | |||
909 | 539 | |||
910 | 540 | class AmpBox(dict): | ||
911 | 541 | """ | ||
912 | 542 | I am a packet in the AMP protocol, much like a regular bytes:bytes | ||
913 | 543 | dictionary. | ||
914 | 544 | """ | ||
915 | 545 | |||
916 | 546 | # be like a regular dictionary, don't magically acquire a __dict__... | ||
917 | 547 | __slots__ = [] | ||
918 | 548 | |||
919 | 549 | def copy(self): | ||
920 | 550 | """ | ||
921 | 551 | Return another AmpBox just like me. | ||
922 | 552 | """ | ||
923 | 553 | newBox = self.__class__() | ||
924 | 554 | newBox.update(self) | ||
925 | 555 | return newBox | ||
926 | 556 | |||
927 | 557 | def serialize(self): | ||
928 | 558 | """ | ||
929 | 559 | Convert me into a wire-encoded string. | ||
930 | 560 | |||
931 | 561 | @return: a bytes encoded according to the rules described in the module | ||
932 | 562 | docstring. | ||
933 | 563 | """ | ||
934 | 564 | i = sorted(self.viewitems()) | ||
935 | 565 | L = [] | ||
936 | 566 | w = L.append | ||
937 | 567 | for k, v in i: | ||
938 | 568 | if type(k) == unicode: | ||
939 | 569 | raise TypeError("Unicode key not allowed: %r" % k) | ||
940 | 570 | if type(v) == unicode: | ||
941 | 571 | raise TypeError( | ||
942 | 572 | "Unicode value for key %r not allowed: %r" % (k, v)) | ||
943 | 573 | if len(k) > MAX_KEY_LENGTH: | ||
944 | 574 | raise TooLong(True, True, k, None) | ||
945 | 575 | if len(v) > MAX_VALUE_LENGTH: | ||
946 | 576 | raise TooLong(False, True, v, k) | ||
947 | 577 | for kv in k, v: | ||
948 | 578 | w(pack("!I", len(kv))) | ||
949 | 579 | w(kv) | ||
950 | 580 | w(pack("!I", 0)) | ||
951 | 581 | return ''.join(L) | ||
952 | 582 | |||
953 | 583 | def _sendTo(self, proto): | ||
954 | 584 | """ | ||
955 | 585 | Serialize and send this box to a Amp instance. By the time it is being | ||
956 | 586 | sent, several keys are required. I must have exactly ONE of:: | ||
957 | 587 | |||
958 | 588 | _ask | ||
959 | 589 | _answer | ||
960 | 590 | _error | ||
961 | 591 | |||
962 | 592 | If the '_ask' key is set, then the '_command' key must also be | ||
963 | 593 | set. | ||
964 | 594 | |||
965 | 595 | @param proto: an AMP instance. | ||
966 | 596 | """ | ||
967 | 597 | proto.sendBox(self) | ||
968 | 598 | |||
969 | 599 | def __repr__(self): | ||
970 | 600 | return 'AmpBox(%s)' % (dict.__repr__(self),) | ||
971 | 601 | |||
972 | 602 | |||
973 | 603 | # amp32.Box => AmpBox | ||
974 | 604 | Box = AmpBox | ||
975 | 605 | |||
976 | 606 | |||
977 | 607 | class QuitBox(AmpBox): | ||
978 | 608 | """ | ||
979 | 609 | I am an AmpBox that, upon being sent, terminates the connection. | ||
980 | 610 | """ | ||
981 | 611 | |||
982 | 612 | __slots__ = [] | ||
983 | 613 | |||
984 | 614 | def __repr__(self): | ||
985 | 615 | return 'QuitBox(**%s)' % (super(QuitBox, self).__repr__(),) | ||
986 | 616 | |||
987 | 617 | def _sendTo(self, proto): | ||
988 | 618 | """ | ||
989 | 619 | Immediately call loseConnection after sending. | ||
990 | 620 | """ | ||
991 | 621 | super(QuitBox, self)._sendTo(proto) | ||
992 | 622 | proto.transport.loseConnection() | ||
993 | 623 | |||
994 | 624 | |||
995 | 625 | class _SwitchBox(AmpBox): | ||
996 | 626 | """ | ||
997 | 627 | Implementation detail of ProtocolSwitchCommand: I am a AmpBox which sets | ||
998 | 628 | up state for the protocol to switch. | ||
999 | 629 | """ | ||
1000 | 630 | |||
1001 | 631 | # DON'T set __slots__ here; we do have an attribute. | ||
1002 | 632 | |||
1003 | 633 | def __init__(self, innerProto, **kw): | ||
1004 | 634 | """ | ||
1005 | 635 | Create a _SwitchBox with the protocol to switch to after being sent. | ||
1006 | 636 | |||
1007 | 637 | @param innerProto: the protocol instance to switch to. | ||
1008 | 638 | @type innerProto: an IProtocol provider. | ||
1009 | 639 | """ | ||
1010 | 640 | super(_SwitchBox, self).__init__(**kw) | ||
1011 | 641 | self.innerProto = innerProto | ||
1012 | 642 | |||
1013 | 643 | def __repr__(self): | ||
1014 | 644 | return '_SwitchBox(%r, **%s)' % (self.innerProto, | ||
1015 | 645 | dict.__repr__(self),) | ||
1016 | 646 | |||
1017 | 647 | def _sendTo(self, proto): | ||
1018 | 648 | """ | ||
1019 | 649 | Send me; I am the last box on the connection. All further traffic will | ||
1020 | 650 | be over the new protocol. | ||
1021 | 651 | """ | ||
1022 | 652 | super(_SwitchBox, self)._sendTo(proto) | ||
1023 | 653 | proto._lockForSwitch() | ||
1024 | 654 | proto._switchTo(self.innerProto) | ||
1025 | 655 | |||
1026 | 656 | |||
1027 | 657 | class BoxDispatcher: | ||
1028 | 658 | """ | ||
1029 | 659 | A L{BoxDispatcher} dispatches '_ask', '_answer', and '_error' L{AmpBox}es, | ||
1030 | 660 | both incoming and outgoing, to their appropriate destinations. | ||
1031 | 661 | |||
1032 | 662 | Outgoing commands are converted into L{Deferred}s and outgoing boxes, and | ||
1033 | 663 | associated tracking state to fire those L{Deferred} when '_answer' boxes | ||
1034 | 664 | come back. Incoming '_answer' and '_error' boxes are converted into | ||
1035 | 665 | callbacks and errbacks on those L{Deferred}s, respectively. | ||
1036 | 666 | |||
1037 | 667 | Incoming '_ask' boxes are converted into method calls on a supplied method | ||
1038 | 668 | locator. | ||
1039 | 669 | |||
1040 | 670 | @ivar _outstandingRequests: a dictionary mapping request IDs to | ||
1041 | 671 | L{Deferred}s which were returned for those requests. | ||
1042 | 672 | |||
1043 | 673 | @ivar locator: an object with a L{locateResponder} method that locates a | ||
1044 | 674 | responder function that takes a Box and returns a result (either a Box or a | ||
1045 | 675 | Deferred which fires one). | ||
1046 | 676 | |||
1047 | 677 | @ivar boxSender: an object which can send boxes, via the L{_sendBox} | ||
1048 | 678 | method, such as an L{AMP} instance. | ||
1049 | 679 | @type boxSender: L{IBoxSender} | ||
1050 | 680 | """ | ||
1051 | 681 | |||
1052 | 682 | implements(IBoxReceiver) | ||
1053 | 683 | |||
1054 | 684 | _failAllReason = None | ||
1055 | 685 | _outstandingRequests = None | ||
1056 | 686 | _counter = 0 | ||
1057 | 687 | boxSender = None | ||
1058 | 688 | |||
1059 | 689 | def __init__(self, locator): | ||
1060 | 690 | self._outstandingRequests = {} | ||
1061 | 691 | self.locator = locator | ||
1062 | 692 | |||
1063 | 693 | def startReceivingBoxes(self, boxSender): | ||
1064 | 694 | """ | ||
1065 | 695 | The given boxSender is going to start calling boxReceived on this | ||
1066 | 696 | L{BoxDispatcher}. | ||
1067 | 697 | |||
1068 | 698 | @param boxSender: The L{IBoxSender} to send command responses to. | ||
1069 | 699 | """ | ||
1070 | 700 | self.boxSender = boxSender | ||
1071 | 701 | |||
1072 | 702 | def stopReceivingBoxes(self, reason): | ||
1073 | 703 | """ | ||
1074 | 704 | No further boxes will be received here. Terminate all currently | ||
1075 | 705 | oustanding command deferreds with the given reason. | ||
1076 | 706 | """ | ||
1077 | 707 | self.failAllOutgoing(reason) | ||
1078 | 708 | |||
1079 | 709 | def failAllOutgoing(self, reason): | ||
1080 | 710 | """ | ||
1081 | 711 | Call the errback on all outstanding requests awaiting responses. | ||
1082 | 712 | |||
1083 | 713 | @param reason: the Failure instance to pass to those errbacks. | ||
1084 | 714 | """ | ||
1085 | 715 | self._failAllReason = reason | ||
1086 | 716 | OR = list(self._outstandingRequests.viewitems()) | ||
1087 | 717 | self._outstandingRequests = None # we can never send another request | ||
1088 | 718 | for key, value in OR: | ||
1089 | 719 | value.errback(reason) | ||
1090 | 720 | |||
1091 | 721 | def _nextTag(self): | ||
1092 | 722 | """ | ||
1093 | 723 | Generate protocol-local serial numbers for _ask keys. | ||
1094 | 724 | |||
1095 | 725 | @return: a string that has not yet been used on this connection. | ||
1096 | 726 | """ | ||
1097 | 727 | self._counter += 1 | ||
1098 | 728 | return '%x' % (self._counter,) | ||
1099 | 729 | |||
1100 | 730 | def _sendBoxCommand(self, command, box, requiresAnswer=True): | ||
1101 | 731 | """ | ||
1102 | 732 | Send a command across the wire with the given C{amp32.Box}. | ||
1103 | 733 | |||
1104 | 734 | Mutate the given box to give it any additional keys (_command, _ask) | ||
1105 | 735 | required for the command and request/response machinery, then send it. | ||
1106 | 736 | |||
1107 | 737 | If requiresAnswer is True, returns a C{Deferred} which fires when a | ||
1108 | 738 | response is received. The C{Deferred} is fired with an C{amp32.Box} on | ||
1109 | 739 | success, or with an C{amp32.RemoteAmpError} if an error is received. | ||
1110 | 740 | |||
1111 | 741 | If the Deferred fails and the error is not handled by the caller of | ||
1112 | 742 | this method, the failure will be logged and the connection dropped. | ||
1113 | 743 | |||
1114 | 744 | @param command: a bytes, the name of the command to issue. | ||
1115 | 745 | |||
1116 | 746 | @param box: an AmpBox with the arguments for the command. | ||
1117 | 747 | |||
1118 | 748 | @param requiresAnswer: a boolean. Defaults to True. If True, return a | ||
1119 | 749 | Deferred which will fire when the other side responds to this command. | ||
1120 | 750 | If False, return None and do not ask the other side for | ||
1121 | 751 | acknowledgement. | ||
1122 | 752 | |||
1123 | 753 | @return: a Deferred which fires the AmpBox that holds the response to | ||
1124 | 754 | this command, or None, as specified by requiresAnswer. | ||
1125 | 755 | |||
1126 | 756 | @raise ProtocolSwitched: if the protocol has been switched. | ||
1127 | 757 | """ | ||
1128 | 758 | if self._failAllReason is not None: | ||
1129 | 759 | return fail(self._failAllReason) | ||
1130 | 760 | box[COMMAND] = command | ||
1131 | 761 | tag = self._nextTag() | ||
1132 | 762 | if requiresAnswer: | ||
1133 | 763 | box[ASK] = tag | ||
1134 | 764 | box._sendTo(self.boxSender) | ||
1135 | 765 | if requiresAnswer: | ||
1136 | 766 | result = self._outstandingRequests[tag] = Deferred() | ||
1137 | 767 | else: | ||
1138 | 768 | result = None | ||
1139 | 769 | return result | ||
1140 | 770 | |||
1141 | 771 | def callRemoteString(self, command, requiresAnswer=True, **kw): | ||
1142 | 772 | """ | ||
1143 | 773 | This is a low-level API, designed only for optimizing simple messages | ||
1144 | 774 | for which the overhead of parsing is too great. | ||
1145 | 775 | |||
1146 | 776 | @param command: a bytes naming the command. | ||
1147 | 777 | |||
1148 | 778 | @param kw: arguments to the amp box. | ||
1149 | 779 | |||
1150 | 780 | @param requiresAnswer: a boolean. Defaults to True. If True, return a | ||
1151 | 781 | Deferred which will fire when the other side responds to this command. | ||
1152 | 782 | If False, return None and do not ask the other side for | ||
1153 | 783 | acknowledgement. | ||
1154 | 784 | |||
1155 | 785 | @return: a Deferred which fires the AmpBox that holds the response to | ||
1156 | 786 | this command, or None, as specified by requiresAnswer. | ||
1157 | 787 | """ | ||
1158 | 788 | box = Box(kw) | ||
1159 | 789 | return self._sendBoxCommand(command, box, requiresAnswer) | ||
1160 | 790 | |||
1161 | 791 | def callRemote(self, commandType, *a, **kw): | ||
1162 | 792 | """ | ||
1163 | 793 | This is the primary high-level API for sending messages via AMP. | ||
1164 | 794 | Invoke it with a command and appropriate arguments to send a message | ||
1165 | 795 | to this connection's peer. | ||
1166 | 796 | |||
1167 | 797 | @param commandType: a subclass of Command. | ||
1168 | 798 | @type commandType: L{type} | ||
1169 | 799 | |||
1170 | 800 | @param a: Positional (special) parameters taken by the command. | ||
1171 | 801 | Positional parameters will typically not be sent over the wire. The | ||
1172 | 802 | only command included with AMP which uses positional parameters is | ||
1173 | 803 | L{ProtocolSwitchCommand}, which takes the protocol that will be | ||
1174 | 804 | switched to as its first argument. | ||
1175 | 805 | |||
1176 | 806 | @param kw: Keyword arguments taken by the command. These are the | ||
1177 | 807 | arguments declared in the command's 'arguments' attribute. They will | ||
1178 | 808 | be encoded and sent to the peer as arguments for the L{commandType}. | ||
1179 | 809 | |||
1180 | 810 | @return: If L{commandType} has a C{requiresAnswer} attribute set to | ||
1181 | 811 | L{False}, then return L{None}. Otherwise, return a L{Deferred} which | ||
1182 | 812 | fires with a dictionary of objects representing the result of this | ||
1183 | 813 | call. Additionally, this L{Deferred} may fail with an exception | ||
1184 | 814 | representing a connection failure, with L{UnknownRemoteError} if the | ||
1185 | 815 | other end of the connection fails for an unknown reason, or with any | ||
1186 | 816 | error specified as a key in L{commandType}'s C{errors} dictionary. | ||
1187 | 817 | """ | ||
1188 | 818 | |||
1189 | 819 | # XXX this takes command subclasses and not command objects on purpose. | ||
1190 | 820 | # There's really no reason to have all this back-and-forth between | ||
1191 | 821 | # command objects and the protocol, and the extra object being created | ||
1192 | 822 | # (the Command instance) is pointless. Command is kind of like | ||
1193 | 823 | # Interface, and should be more like it. | ||
1194 | 824 | |||
1195 | 825 | # In other words, the fact that commandType is instantiated here is an | ||
1196 | 826 | # implementation detail. Don't rely on it. | ||
1197 | 827 | |||
1198 | 828 | try: | ||
1199 | 829 | co = commandType(*a, **kw) | ||
1200 | 830 | except: | ||
1201 | 831 | return fail() | ||
1202 | 832 | return co._doCommand(self) | ||
1203 | 833 | |||
1204 | 834 | def unhandledError(self, failure): | ||
1205 | 835 | """ | ||
1206 | 836 | This is a terminal callback called after application code has had a | ||
1207 | 837 | chance to quash any errors. | ||
1208 | 838 | """ | ||
1209 | 839 | return self.boxSender.unhandledError(failure) | ||
1210 | 840 | |||
1211 | 841 | def _answerReceived(self, box): | ||
1212 | 842 | """ | ||
1213 | 843 | An AMP box was received that answered a command previously sent with | ||
1214 | 844 | L{callRemote}. | ||
1215 | 845 | |||
1216 | 846 | @param box: an AmpBox with a value for its L{ANSWER} key. | ||
1217 | 847 | """ | ||
1218 | 848 | question = self._outstandingRequests.pop(box[ANSWER]) | ||
1219 | 849 | question.addErrback(self.unhandledError) | ||
1220 | 850 | question.callback(box) | ||
1221 | 851 | |||
1222 | 852 | def _errorReceived(self, box): | ||
1223 | 853 | """ | ||
1224 | 854 | An AMP box was received that answered a command previously sent with | ||
1225 | 855 | L{callRemote}, with an error. | ||
1226 | 856 | |||
1227 | 857 | @param box: an L{AmpBox} with a value for its L{ERROR}, L{ERROR_CODE}, | ||
1228 | 858 | and L{ERROR_DESCRIPTION} keys. | ||
1229 | 859 | """ | ||
1230 | 860 | question = self._outstandingRequests.pop(box[ERROR]) | ||
1231 | 861 | question.addErrback(self.unhandledError) | ||
1232 | 862 | errorCode = box[ERROR_CODE] | ||
1233 | 863 | description = box[ERROR_DESCRIPTION] | ||
1234 | 864 | if errorCode in PROTOCOL_ERRORS: | ||
1235 | 865 | exc = PROTOCOL_ERRORS[errorCode](errorCode, description) | ||
1236 | 866 | else: | ||
1237 | 867 | exc = RemoteAmpError(errorCode, description) | ||
1238 | 868 | question.errback(Failure(exc)) | ||
1239 | 869 | |||
1240 | 870 | def _commandReceived(self, box): | ||
1241 | 871 | """ | ||
1242 | 872 | @param box: an L{AmpBox} with a value for its L{COMMAND} and L{ASK} | ||
1243 | 873 | keys. | ||
1244 | 874 | """ | ||
1245 | 875 | def formatAnswer(answerBox): | ||
1246 | 876 | answerBox[ANSWER] = box[ASK] | ||
1247 | 877 | return answerBox | ||
1248 | 878 | |||
1249 | 879 | def formatError(error): | ||
1250 | 880 | if error.check(RemoteAmpError): | ||
1251 | 881 | code = error.value.errorCode | ||
1252 | 882 | desc = error.value.description | ||
1253 | 883 | if error.value.fatal: | ||
1254 | 884 | errorBox = QuitBox() | ||
1255 | 885 | else: | ||
1256 | 886 | errorBox = AmpBox() | ||
1257 | 887 | else: | ||
1258 | 888 | errorBox = QuitBox() | ||
1259 | 889 | # here is where server-side logging happens if the error isn't | ||
1260 | 890 | # handled | ||
1261 | 891 | log.err(error) | ||
1262 | 892 | code = UNKNOWN_ERROR_CODE | ||
1263 | 893 | desc = "Unknown Error" | ||
1264 | 894 | errorBox[ERROR] = box[ASK] | ||
1265 | 895 | errorBox[ERROR_DESCRIPTION] = desc | ||
1266 | 896 | errorBox[ERROR_CODE] = code | ||
1267 | 897 | return errorBox | ||
1268 | 898 | |||
1269 | 899 | deferred = self.dispatchCommand(box) | ||
1270 | 900 | if ASK in box: | ||
1271 | 901 | deferred.addCallbacks(formatAnswer, formatError) | ||
1272 | 902 | deferred.addCallback(self._safeEmit) | ||
1273 | 903 | deferred.addErrback(self.unhandledError) | ||
1274 | 904 | |||
1275 | 905 | def ampBoxReceived(self, box): | ||
1276 | 906 | """ | ||
1277 | 907 | An AmpBox was received, representing a command, or an answer to a | ||
1278 | 908 | previously issued command (either successful or erroneous). Respond to | ||
1279 | 909 | it according to its contents. | ||
1280 | 910 | |||
1281 | 911 | @param box: an AmpBox | ||
1282 | 912 | |||
1283 | 913 | @raise NoEmptyBoxes: when a box is received that does not contain an | ||
1284 | 914 | '_answer', '_command' / '_ask', or '_error' key; i.e. one which does | ||
1285 | 915 | not fit into the command / response protocol defined by AMP. | ||
1286 | 916 | """ | ||
1287 | 917 | if ANSWER in box: | ||
1288 | 918 | self._answerReceived(box) | ||
1289 | 919 | elif ERROR in box: | ||
1290 | 920 | self._errorReceived(box) | ||
1291 | 921 | elif COMMAND in box: | ||
1292 | 922 | self._commandReceived(box) | ||
1293 | 923 | else: | ||
1294 | 924 | raise NoEmptyBoxes(box) | ||
1295 | 925 | |||
1296 | 926 | def _safeEmit(self, aBox): | ||
1297 | 927 | """ | ||
1298 | 928 | Emit a box, ignoring L{ProtocolSwitched} and L{ConnectionLost} errors | ||
1299 | 929 | which cannot be usefully handled. | ||
1300 | 930 | """ | ||
1301 | 931 | try: | ||
1302 | 932 | aBox._sendTo(self.boxSender) | ||
1303 | 933 | except (ProtocolSwitched, ConnectionLost): | ||
1304 | 934 | pass | ||
1305 | 935 | |||
1306 | 936 | def dispatchCommand(self, box): | ||
1307 | 937 | """ | ||
1308 | 938 | A box with a _command key was received. | ||
1309 | 939 | |||
1310 | 940 | Dispatch it to a local handler call it. | ||
1311 | 941 | |||
1312 | 942 | @param proto: an AMP instance. | ||
1313 | 943 | @param box: an AmpBox to be dispatched. | ||
1314 | 944 | """ | ||
1315 | 945 | cmd = box[COMMAND] | ||
1316 | 946 | responder = self.locator.locateResponder(cmd) | ||
1317 | 947 | if responder is None: | ||
1318 | 948 | return fail(RemoteAmpError( | ||
1319 | 949 | UNHANDLED_ERROR_CODE, "Unhandled Command: %r" % (cmd,), | ||
1320 | 950 | False, local=Failure(UnhandledCommand()))) | ||
1321 | 951 | return maybeDeferred(responder, box) | ||
1322 | 952 | |||
1323 | 953 | |||
1324 | 954 | class CommandLocator: | ||
1325 | 955 | """ | ||
1326 | 956 | A L{CommandLocator} is a collection of responders to AMP L{Command}s, with | ||
1327 | 957 | the help of the L{Command.responder} decorator. | ||
1328 | 958 | """ | ||
1329 | 959 | |||
1330 | 960 | class __metaclass__(type): | ||
1331 | 961 | """ | ||
1332 | 962 | This metaclass keeps track of all of the Command.responder-decorated | ||
1333 | 963 | methods defined since the last CommandLocator subclass was defined. It | ||
1334 | 964 | assumes (usually correctly, but unfortunately not necessarily so) that | ||
1335 | 965 | those commands responders were all declared as methods of the class | ||
1336 | 966 | being defined. Note that this list can be incorrect if users use the | ||
1337 | 967 | Command.responder decorator outside the context of a CommandLocator | ||
1338 | 968 | class declaration. | ||
1339 | 969 | |||
1340 | 970 | Command responders defined on subclasses are given precedence over | ||
1341 | 971 | those inherited from a base class. | ||
1342 | 972 | |||
1343 | 973 | The Command.responder decorator explicitly cooperates with this | ||
1344 | 974 | metaclass. | ||
1345 | 975 | """ | ||
1346 | 976 | |||
1347 | 977 | _currentClassCommands = [] | ||
1348 | 978 | |||
1349 | 979 | def __new__(cls, name, bases, attrs): | ||
1350 | 980 | commands = cls._currentClassCommands[:] | ||
1351 | 981 | cls._currentClassCommands[:] = [] | ||
1352 | 982 | cd = attrs['_commandDispatch'] = {} | ||
1353 | 983 | subcls = type.__new__(cls, name, bases, attrs) | ||
1354 | 984 | ancestors = list(subcls.__mro__[1:]) | ||
1355 | 985 | ancestors.reverse() | ||
1356 | 986 | for ancestor in ancestors: | ||
1357 | 987 | cd.update(getattr(ancestor, '_commandDispatch', {})) | ||
1358 | 988 | for commandClass, responderFunc in commands: | ||
1359 | 989 | cd[commandClass.commandName] = (commandClass, responderFunc) | ||
1360 | 990 | if (bases and ( | ||
1361 | 991 | subcls.lookupFunction != CommandLocator.lookupFunction)): | ||
1362 | 992 | def locateResponder(self, name): | ||
1363 | 993 | warnings.warn( | ||
1364 | 994 | "Override locateResponder, not lookupFunction.", | ||
1365 | 995 | category=PendingDeprecationWarning, | ||
1366 | 996 | stacklevel=2) | ||
1367 | 997 | return self.lookupFunction(name) | ||
1368 | 998 | subcls.locateResponder = locateResponder | ||
1369 | 999 | return subcls | ||
1370 | 1000 | |||
1371 | 1001 | implements(IResponderLocator) | ||
1372 | 1002 | |||
1373 | 1003 | def _wrapWithSerialization(self, aCallable, command): | ||
1374 | 1004 | """ | ||
1375 | 1005 | Wrap aCallable with its command's argument de-serialization | ||
1376 | 1006 | and result serialization logic. | ||
1377 | 1007 | |||
1378 | 1008 | @param aCallable: a callable with a 'command' attribute, designed to be | ||
1379 | 1009 | called with keyword arguments. | ||
1380 | 1010 | |||
1381 | 1011 | @param command: the command class whose serialization to use. | ||
1382 | 1012 | |||
1383 | 1013 | @return: a 1-arg callable which, when invoked with an AmpBox, will | ||
1384 | 1014 | deserialize the argument list and invoke appropriate user code for the | ||
1385 | 1015 | callable's command, returning a Deferred which fires with the result or | ||
1386 | 1016 | fails with an error. | ||
1387 | 1017 | """ | ||
1388 | 1018 | def doit(box): | ||
1389 | 1019 | kw = command.parseArguments(box, self) | ||
1390 | 1020 | |||
1391 | 1021 | def checkKnownErrors(error): | ||
1392 | 1022 | key = error.trap(*command.allErrors) | ||
1393 | 1023 | code = command.allErrors[key] | ||
1394 | 1024 | desc = bytes(error.value) | ||
1395 | 1025 | return Failure(RemoteAmpError( | ||
1396 | 1026 | code, desc, key in command.fatalErrors, local=error)) | ||
1397 | 1027 | |||
1398 | 1028 | def makeResponseFor(objects): | ||
1399 | 1029 | try: | ||
1400 | 1030 | return command.makeResponse(objects, self) | ||
1401 | 1031 | except: | ||
1402 | 1032 | # let's helpfully log this. | ||
1403 | 1033 | originalFailure = Failure() | ||
1404 | 1034 | raise BadLocalReturn( | ||
1405 | 1035 | "%r returned %r and %r could not serialize it" % ( | ||
1406 | 1036 | aCallable, | ||
1407 | 1037 | objects, | ||
1408 | 1038 | command), | ||
1409 | 1039 | originalFailure) | ||
1410 | 1040 | |||
1411 | 1041 | return maybeDeferred(aCallable, **kw).addCallback( | ||
1412 | 1042 | makeResponseFor).addErrback( | ||
1413 | 1043 | checkKnownErrors) | ||
1414 | 1044 | |||
1415 | 1045 | return doit | ||
1416 | 1046 | |||
1417 | 1047 | def lookupFunction(self, name): | ||
1418 | 1048 | """ | ||
1419 | 1049 | Deprecated synonym for L{locateResponder} | ||
1420 | 1050 | """ | ||
1421 | 1051 | if self.__class__.lookupFunction != CommandLocator.lookupFunction: | ||
1422 | 1052 | return CommandLocator.locateResponder(self, name) | ||
1423 | 1053 | else: | ||
1424 | 1054 | warnings.warn("Call locateResponder, not lookupFunction.", | ||
1425 | 1055 | category=PendingDeprecationWarning, | ||
1426 | 1056 | stacklevel=2) | ||
1427 | 1057 | return self.locateResponder(name) | ||
1428 | 1058 | |||
1429 | 1059 | def locateResponder(self, name): | ||
1430 | 1060 | """ | ||
1431 | 1061 | Locate a callable to invoke when executing the named command. | ||
1432 | 1062 | |||
1433 | 1063 | @param name: the normalized name (from the wire) of the command. | ||
1434 | 1064 | |||
1435 | 1065 | @return: a 1-argument function that takes a Box and returns a box or a | ||
1436 | 1066 | Deferred which fires a Box, for handling the command identified by the | ||
1437 | 1067 | given name, or None, if no appropriate responder can be found. | ||
1438 | 1068 | """ | ||
1439 | 1069 | # Try to find a high-level method to invoke, and if we can't find one, | ||
1440 | 1070 | # fall back to a low-level one. | ||
1441 | 1071 | cd = self._commandDispatch | ||
1442 | 1072 | if name in cd: | ||
1443 | 1073 | commandClass, responderFunc = cd[name] | ||
1444 | 1074 | responderMethod = types.MethodType( | ||
1445 | 1075 | responderFunc, self, self.__class__) | ||
1446 | 1076 | return self._wrapWithSerialization(responderMethod, commandClass) | ||
1447 | 1077 | |||
1448 | 1078 | |||
1449 | 1079 | class SimpleStringLocator(object): | ||
1450 | 1080 | """ | ||
1451 | 1081 | Implement the L{locateResponder} method to do simple, string-based | ||
1452 | 1082 | dispatch. | ||
1453 | 1083 | """ | ||
1454 | 1084 | |||
1455 | 1085 | implements(IResponderLocator) | ||
1456 | 1086 | |||
1457 | 1087 | baseDispatchPrefix = 'amp_' | ||
1458 | 1088 | |||
1459 | 1089 | def locateResponder(self, name): | ||
1460 | 1090 | """ | ||
1461 | 1091 | Locate a callable to invoke when executing the named command. | ||
1462 | 1092 | |||
1463 | 1093 | @return: a function with the name C{"amp_" + name} on L{self}, or None | ||
1464 | 1094 | if no such function exists. This function will then be called with the | ||
1465 | 1095 | L{AmpBox} itself as an argument. | ||
1466 | 1096 | |||
1467 | 1097 | @param name: the normalized name (from the wire) of the command. | ||
1468 | 1098 | """ | ||
1469 | 1099 | fName = self.baseDispatchPrefix + (name.upper()) | ||
1470 | 1100 | return getattr(self, fName, None) | ||
1471 | 1101 | |||
1472 | 1102 | |||
1473 | 1103 | PYTHON_KEYWORDS = [ | ||
1474 | 1104 | 'and', 'del', 'for', 'is', 'raise', 'assert', 'elif', 'from', 'lambda', | ||
1475 | 1105 | 'return', 'break', 'else', 'global', 'not', 'try', 'class', 'except', | ||
1476 | 1106 | 'if', 'or', 'while', 'continue', 'exec', 'import', 'pass', 'yield', | ||
1477 | 1107 | 'def', 'finally', 'in', 'print'] | ||
1478 | 1108 | |||
1479 | 1109 | |||
1480 | 1110 | def _wireNameToPythonIdentifier(key): | ||
1481 | 1111 | """ | ||
1482 | 1112 | (Private) Normalize an argument name from the wire for use with Python | ||
1483 | 1113 | code. If the return value is going to be a python keyword it will be | ||
1484 | 1114 | capitalized. If it contains any dashes they will be replaced with | ||
1485 | 1115 | underscores. | ||
1486 | 1116 | |||
1487 | 1117 | The rationale behind this method is that AMP should be an inherently | ||
1488 | 1118 | multi-language protocol, so message keys may contain all manner of bizarre | ||
1489 | 1119 | bytes. This is not a complete solution; there are still forms of arguments | ||
1490 | 1120 | that this implementation will be unable to parse. However, Python | ||
1491 | 1121 | identifiers share a huge raft of properties with identifiers from many | ||
1492 | 1122 | other languages, so this is a 'good enough' effort for now. We deal | ||
1493 | 1123 | explicitly with dashes because that is the most likely departure: Lisps | ||
1494 | 1124 | commonly use dashes to separate method names, so protocols initially | ||
1495 | 1125 | implemented in a lisp amp dialect may use dashes in argument or command | ||
1496 | 1126 | names. | ||
1497 | 1127 | |||
1498 | 1128 | @param key: a bytes, looking something like 'foo-bar-baz' or 'from' | ||
1499 | 1129 | |||
1500 | 1130 | @return: a bytes which is a valid python identifier, looking something like | ||
1501 | 1131 | 'foo_bar_baz' or 'From'. | ||
1502 | 1132 | """ | ||
1503 | 1133 | lkey = key.replace("-", "_") | ||
1504 | 1134 | if lkey in PYTHON_KEYWORDS: | ||
1505 | 1135 | return lkey.title() | ||
1506 | 1136 | return lkey | ||
1507 | 1137 | |||
1508 | 1138 | |||
1509 | 1139 | class Argument: | ||
1510 | 1140 | """ | ||
1511 | 1141 | Base-class of all objects that take values from Amp packets and convert | ||
1512 | 1142 | them into objects for Python functions. | ||
1513 | 1143 | |||
1514 | 1144 | This implementation of L{IArgumentType} provides several higher-level | ||
1515 | 1145 | hooks for subclasses to override. See L{toString} and L{fromString} | ||
1516 | 1146 | which will be used to define the behavior of L{IArgumentType.toBox} and | ||
1517 | 1147 | L{IArgumentType.fromBox}, respectively. | ||
1518 | 1148 | """ | ||
1519 | 1149 | |||
1520 | 1150 | implements(IArgumentType) | ||
1521 | 1151 | |||
1522 | 1152 | optional = False | ||
1523 | 1153 | |||
1524 | 1154 | def __init__(self, optional=False): | ||
1525 | 1155 | """ | ||
1526 | 1156 | Create an Argument. | ||
1527 | 1157 | |||
1528 | 1158 | @param optional: a boolean indicating whether this argument can be | ||
1529 | 1159 | omitted in the protocol. | ||
1530 | 1160 | """ | ||
1531 | 1161 | self.optional = optional | ||
1532 | 1162 | |||
1533 | 1163 | def retrieve(self, d, name, proto): | ||
1534 | 1164 | """ | ||
1535 | 1165 | Retrieve the given key from the given dictionary, removing it if found. | ||
1536 | 1166 | |||
1537 | 1167 | @param d: a dictionary. | ||
1538 | 1168 | |||
1539 | 1169 | @param name: a key in L{d}. | ||
1540 | 1170 | |||
1541 | 1171 | @param proto: an instance of an AMP. | ||
1542 | 1172 | |||
1543 | 1173 | @raise KeyError: if I am not optional and no value was found. | ||
1544 | 1174 | |||
1545 | 1175 | @return: d[name]. | ||
1546 | 1176 | """ | ||
1547 | 1177 | if self.optional: | ||
1548 | 1178 | value = d.get(name) | ||
1549 | 1179 | if value is not None: | ||
1550 | 1180 | del d[name] | ||
1551 | 1181 | else: | ||
1552 | 1182 | value = d.pop(name) | ||
1553 | 1183 | return value | ||
1554 | 1184 | |||
1555 | 1185 | def fromBox(self, name, strings, objects, proto): | ||
1556 | 1186 | """ | ||
1557 | 1187 | Populate an 'out' dictionary with mapping names to Python values | ||
1558 | 1188 | decoded from an 'in' AmpBox mapping strings to string values. | ||
1559 | 1189 | |||
1560 | 1190 | @param name: the argument name to retrieve | ||
1561 | 1191 | @type name: bytes | ||
1562 | 1192 | |||
1563 | 1193 | @param strings: The AmpBox to read string(s) from, a mapping of | ||
1564 | 1194 | argument names to string values. | ||
1565 | 1195 | @type strings: AmpBox | ||
1566 | 1196 | |||
1567 | 1197 | @param objects: The dictionary to write object(s) to, a mapping of | ||
1568 | 1198 | names to Python objects. | ||
1569 | 1199 | @type objects: dict | ||
1570 | 1200 | |||
1571 | 1201 | @param proto: an AMP instance. | ||
1572 | 1202 | """ | ||
1573 | 1203 | st = self.retrieve(strings, name, proto) | ||
1574 | 1204 | nk = _wireNameToPythonIdentifier(name) | ||
1575 | 1205 | if self.optional and st is None: | ||
1576 | 1206 | objects[nk] = None | ||
1577 | 1207 | else: | ||
1578 | 1208 | objects[nk] = self.fromStringProto(st, proto) | ||
1579 | 1209 | |||
1580 | 1210 | def toBox(self, name, strings, objects, proto): | ||
1581 | 1211 | """ | ||
1582 | 1212 | Populate an 'out' AmpBox with strings encoded from an 'in' dictionary | ||
1583 | 1213 | mapping names to Python values. | ||
1584 | 1214 | |||
1585 | 1215 | @param name: the argument name to retrieve | ||
1586 | 1216 | @type name: bytes | ||
1587 | 1217 | |||
1588 | 1218 | @param strings: The AmpBox to write string(s) to, a mapping of | ||
1589 | 1219 | argument names to string values. | ||
1590 | 1220 | @type strings: AmpBox | ||
1591 | 1221 | |||
1592 | 1222 | @param objects: The dictionary to read object(s) from, a mapping of | ||
1593 | 1223 | names to Python objects. | ||
1594 | 1224 | |||
1595 | 1225 | @type objects: dict | ||
1596 | 1226 | |||
1597 | 1227 | @param proto: the protocol we are converting for. | ||
1598 | 1228 | @type proto: AMP | ||
1599 | 1229 | """ | ||
1600 | 1230 | obj = self.retrieve(objects, _wireNameToPythonIdentifier(name), proto) | ||
1601 | 1231 | if self.optional and obj is None: | ||
1602 | 1232 | # strings[name] = None | ||
1603 | 1233 | pass | ||
1604 | 1234 | else: | ||
1605 | 1235 | strings[name] = self.toStringProto(obj, proto) | ||
1606 | 1236 | |||
1607 | 1237 | def fromStringProto(self, inString, proto): | ||
1608 | 1238 | """ | ||
1609 | 1239 | Convert a string to a Python value. | ||
1610 | 1240 | |||
1611 | 1241 | @param inString: the string to convert. | ||
1612 | 1242 | |||
1613 | 1243 | @param proto: the protocol we are converting for. | ||
1614 | 1244 | @type proto: AMP | ||
1615 | 1245 | |||
1616 | 1246 | @return: a Python object. | ||
1617 | 1247 | """ | ||
1618 | 1248 | return self.fromString(inString) | ||
1619 | 1249 | |||
1620 | 1250 | def toStringProto(self, inObject, proto): | ||
1621 | 1251 | """ | ||
1622 | 1252 | Convert a Python object to a string. | ||
1623 | 1253 | |||
1624 | 1254 | @param inObject: the object to convert. | ||
1625 | 1255 | |||
1626 | 1256 | @param proto: the protocol we are converting for. | ||
1627 | 1257 | @type proto: AMP | ||
1628 | 1258 | """ | ||
1629 | 1259 | return self.toString(inObject) | ||
1630 | 1260 | |||
1631 | 1261 | def fromString(self, inString): | ||
1632 | 1262 | """ | ||
1633 | 1263 | Convert a string to a Python object. Subclasses must implement this. | ||
1634 | 1264 | |||
1635 | 1265 | @param inString: the string to convert. | ||
1636 | 1266 | @type inString: bytes | ||
1637 | 1267 | |||
1638 | 1268 | @return: the decoded value from inString | ||
1639 | 1269 | """ | ||
1640 | 1270 | |||
1641 | 1271 | def toString(self, inObject): | ||
1642 | 1272 | """ | ||
1643 | 1273 | Convert a Python object into a string for passing over the network. | ||
1644 | 1274 | |||
1645 | 1275 | @param inObject: an object of the type that this Argument is intended | ||
1646 | 1276 | to deal with. | ||
1647 | 1277 | |||
1648 | 1278 | @return: the wire encoding of inObject | ||
1649 | 1279 | @rtype: bytes | ||
1650 | 1280 | """ | ||
1651 | 1281 | |||
1652 | 1282 | |||
1653 | 1283 | class Integer(Argument): | ||
1654 | 1284 | """ | ||
1655 | 1285 | Encode any integer values of any size on the wire as the string | ||
1656 | 1286 | representation. | ||
1657 | 1287 | |||
1658 | 1288 | Example: C{123} becomes C{"123"} | ||
1659 | 1289 | """ | ||
1660 | 1290 | |||
1661 | 1291 | fromString = int | ||
1662 | 1292 | |||
1663 | 1293 | def toString(self, inObject): | ||
1664 | 1294 | return bytes(int(inObject)) | ||
1665 | 1295 | |||
1666 | 1296 | |||
1667 | 1297 | class String(Argument): | ||
1668 | 1298 | """ | ||
1669 | 1299 | Don't do any conversion at all; just pass through 'bytes'. | ||
1670 | 1300 | """ | ||
1671 | 1301 | |||
1672 | 1302 | def toString(self, inObject): | ||
1673 | 1303 | return inObject | ||
1674 | 1304 | |||
1675 | 1305 | def fromString(self, inString): | ||
1676 | 1306 | return inString | ||
1677 | 1307 | |||
1678 | 1308 | |||
1679 | 1309 | class Float(Argument): | ||
1680 | 1310 | """ | ||
1681 | 1311 | Encode floating-point values on the wire as their repr. | ||
1682 | 1312 | """ | ||
1683 | 1313 | |||
1684 | 1314 | fromString = float | ||
1685 | 1315 | toString = repr | ||
1686 | 1316 | |||
1687 | 1317 | |||
1688 | 1318 | class Boolean(Argument): | ||
1689 | 1319 | """ | ||
1690 | 1320 | Encode True or False as "True" or "False" on the wire. | ||
1691 | 1321 | """ | ||
1692 | 1322 | |||
1693 | 1323 | def fromString(self, inString): | ||
1694 | 1324 | if inString == 'True': | ||
1695 | 1325 | return True | ||
1696 | 1326 | elif inString == 'False': | ||
1697 | 1327 | return False | ||
1698 | 1328 | else: | ||
1699 | 1329 | raise TypeError("Bad boolean value: %r" % (inString,)) | ||
1700 | 1330 | |||
1701 | 1331 | def toString(self, inObject): | ||
1702 | 1332 | if inObject: | ||
1703 | 1333 | return 'True' | ||
1704 | 1334 | else: | ||
1705 | 1335 | return 'False' | ||
1706 | 1336 | |||
1707 | 1337 | |||
1708 | 1338 | class Unicode(String): | ||
1709 | 1339 | """ | ||
1710 | 1340 | Encode a unicode string on the wire as UTF-8. | ||
1711 | 1341 | """ | ||
1712 | 1342 | |||
1713 | 1343 | def toString(self, inObject): | ||
1714 | 1344 | # assert isinstance(inObject, unicode) | ||
1715 | 1345 | return String.toString(self, inObject.encode('utf-8')) | ||
1716 | 1346 | |||
1717 | 1347 | def fromString(self, inString): | ||
1718 | 1348 | # assert isinstance(inString, bytes) | ||
1719 | 1349 | return String.fromString(self, inString).decode('utf-8') | ||
1720 | 1350 | |||
1721 | 1351 | |||
1722 | 1352 | class Path(Unicode): | ||
1723 | 1353 | """ | ||
1724 | 1354 | Encode and decode L{filepath.FilePath} instances as paths on the wire. | ||
1725 | 1355 | |||
1726 | 1356 | This is really intended for use with subprocess communication tools: | ||
1727 | 1357 | exchanging pathnames on different machines over a network is not generally | ||
1728 | 1358 | meaningful, but neither is it disallowed; you can use this to communicate | ||
1729 | 1359 | about NFS paths, for example. | ||
1730 | 1360 | """ | ||
1731 | 1361 | |||
1732 | 1362 | def fromString(self, inString): | ||
1733 | 1363 | return filepath.FilePath(Unicode.fromString(self, inString)) | ||
1734 | 1364 | |||
1735 | 1365 | def toString(self, inObject): | ||
1736 | 1366 | return Unicode.toString(self, inObject.path) | ||
1737 | 1367 | |||
1738 | 1368 | |||
1739 | 1369 | class ListOf(Argument): | ||
1740 | 1370 | """ | ||
1741 | 1371 | Encode and decode lists of instances of a single other argument type. | ||
1742 | 1372 | |||
1743 | 1373 | For example, if you want to pass:: | ||
1744 | 1374 | |||
1745 | 1375 | [3, 7, 9, 15] | ||
1746 | 1376 | |||
1747 | 1377 | You can create an argument like this:: | ||
1748 | 1378 | |||
1749 | 1379 | ListOf(Integer()) | ||
1750 | 1380 | |||
1751 | 1381 | The serialized form of the entire list is subject to the limit imposed by | ||
1752 | 1382 | L{MAX_VALUE_LENGTH}. List elements are represented as 32-bit length | ||
1753 | 1383 | prefixed strings. The argument type passed to the L{ListOf} initializer is | ||
1754 | 1384 | responsible for producing the serialized form of each element. | ||
1755 | 1385 | |||
1756 | 1386 | @ivar elementType: The L{Argument} instance used to encode and decode list | ||
1757 | 1387 | elements (note, not an arbitrary L{IArgument} implementation: | ||
1758 | 1388 | arguments must be implemented using only the C{fromString} and | ||
1759 | 1389 | C{toString} methods, not the C{fromBox} and C{toBox} methods). | ||
1760 | 1390 | |||
1761 | 1391 | @param optional: a boolean indicating whether this argument can be | ||
1762 | 1392 | omitted in the protocol. | ||
1763 | 1393 | |||
1764 | 1394 | @since: 10.0 | ||
1765 | 1395 | """ | ||
1766 | 1396 | |||
1767 | 1397 | def __init__(self, elementType, optional=False): | ||
1768 | 1398 | self.elementType = elementType | ||
1769 | 1399 | Argument.__init__(self, optional) | ||
1770 | 1400 | |||
1771 | 1401 | def fromString(self, inString): | ||
1772 | 1402 | """ | ||
1773 | 1403 | Convert the serialized form of a list of instances of some type back | ||
1774 | 1404 | into that list. | ||
1775 | 1405 | """ | ||
1776 | 1406 | strings = [] | ||
1777 | 1407 | parser = Int32StringReceiver() | ||
1778 | 1408 | parser.MAX_LENGTH = MAX_VALUE_LENGTH | ||
1779 | 1409 | parser.stringReceived = strings.append | ||
1780 | 1410 | parser.dataReceived(inString) | ||
1781 | 1411 | return map(self.elementType.fromString, strings) | ||
1782 | 1412 | |||
1783 | 1413 | def toString(self, inObject): | ||
1784 | 1414 | """ | ||
1785 | 1415 | Serialize the given list of objects to a single string. | ||
1786 | 1416 | """ | ||
1787 | 1417 | strings = [] | ||
1788 | 1418 | for obj in inObject: | ||
1789 | 1419 | serialized = self.elementType.toString(obj) | ||
1790 | 1420 | strings.append(pack('!I', len(serialized))) | ||
1791 | 1421 | strings.append(serialized) | ||
1792 | 1422 | return ''.join(strings) | ||
1793 | 1423 | |||
1794 | 1424 | |||
1795 | 1425 | class AmpList(Argument): | ||
1796 | 1426 | """ | ||
1797 | 1427 | Convert a list of dictionaries into a list of AMP boxes on the wire. | ||
1798 | 1428 | |||
1799 | 1429 | For example, if you want to pass:: | ||
1800 | 1430 | |||
1801 | 1431 | [{'a': 7, 'b': u'hello'}, {'a': 9, 'b': u'goodbye'}] | ||
1802 | 1432 | |||
1803 | 1433 | You might use an AmpList like this in your arguments or response list:: | ||
1804 | 1434 | |||
1805 | 1435 | AmpList([('a', Integer()), | ||
1806 | 1436 | ('b', Unicode())]) | ||
1807 | 1437 | """ | ||
1808 | 1438 | |||
1809 | 1439 | def __init__(self, subargs, optional=False): | ||
1810 | 1440 | """ | ||
1811 | 1441 | Create an AmpList. | ||
1812 | 1442 | |||
1813 | 1443 | @param subargs: a list of 2-tuples of ('name', argument) describing the | ||
1814 | 1444 | schema of the dictionaries in the sequence of amp boxes. | ||
1815 | 1445 | |||
1816 | 1446 | @param optional: a boolean indicating whether this argument can be | ||
1817 | 1447 | omitted in the protocol. | ||
1818 | 1448 | """ | ||
1819 | 1449 | self.subargs = subargs | ||
1820 | 1450 | Argument.__init__(self, optional) | ||
1821 | 1451 | |||
1822 | 1452 | def fromStringProto(self, inString, proto): | ||
1823 | 1453 | boxes = parseString(inString) | ||
1824 | 1454 | values = [_stringsToObjects(box, self.subargs, proto) | ||
1825 | 1455 | for box in boxes] | ||
1826 | 1456 | return values | ||
1827 | 1457 | |||
1828 | 1458 | def toStringProto(self, inObject, proto): | ||
1829 | 1459 | return ''.join( | ||
1830 | 1460 | _objectsToStrings(objects, self.subargs, Box(), proto).serialize() | ||
1831 | 1461 | for objects in inObject) | ||
1832 | 1462 | |||
1833 | 1463 | |||
1834 | 1464 | class Descriptor(Integer): | ||
1835 | 1465 | """ | ||
1836 | 1466 | Encode and decode file descriptors for exchange over a UNIX domain socket. | ||
1837 | 1467 | |||
1838 | 1468 | This argument type requires an AMP connection set up over an | ||
1839 | 1469 | L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>} provider (for | ||
1840 | 1470 | example, the kind of connection created by | ||
1841 | 1471 | L{IReactorUNIX.connectUNIX< | ||
1842 | 1472 | twisted.internet.interfaces.IReactorUNIX.connectUNIX>} | ||
1843 | 1473 | and L{UNIXClientEndpoint<twisted.internet.endpoints.UNIXClientEndpoint>}). | ||
1844 | 1474 | |||
1845 | 1475 | There is no correspondence between the integer value of the file | ||
1846 | 1476 | descriptor on the sending and receiving sides, therefore an alternate | ||
1847 | 1477 | approach is taken to matching up received descriptors with particular | ||
1848 | 1478 | L{Descriptor} parameters. The argument is encoded to an ordinal (unique | ||
1849 | 1479 | per connection) for inclusion in the AMP command or response box. The | ||
1850 | 1480 | descriptor itself is sent using | ||
1851 | 1481 | L{IUNIXTransport.sendFileDescriptor< | ||
1852 | 1482 | twisted.internet.interfaces.IUNIXTransport.sendFileDescriptor>}. | ||
1853 | 1483 | The receiver uses the order in which file descriptors are received and the | ||
1854 | 1484 | ordinal value to come up with the received copy of the descriptor. | ||
1855 | 1485 | """ | ||
1856 | 1486 | |||
1857 | 1487 | def fromStringProto(self, inString, proto): | ||
1858 | 1488 | """ | ||
1859 | 1489 | Take a unique identifier associated with a file descriptor which must | ||
1860 | 1490 | have been received by now and use it to look up that descriptor in a | ||
1861 | 1491 | dictionary where they are kept. | ||
1862 | 1492 | |||
1863 | 1493 | @param inString: The base representation (as a byte string) of an | ||
1864 | 1494 | ordinal indicating which file descriptor corresponds to this usage | ||
1865 | 1495 | of this argument. | ||
1866 | 1496 | @type inString: C{bytes} | ||
1867 | 1497 | |||
1868 | 1498 | @param proto: The protocol used to receive this descriptor. This | ||
1869 | 1499 | protocol must be connected via a transport providing | ||
1870 | 1500 | L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}. | ||
1871 | 1501 | @type proto: L{BinaryBoxProtocol} | ||
1872 | 1502 | |||
1873 | 1503 | @return: The file descriptor represented by C{inString}. | ||
1874 | 1504 | @rtype: C{int} | ||
1875 | 1505 | """ | ||
1876 | 1506 | return proto._getDescriptor(int(inString)) | ||
1877 | 1507 | |||
1878 | 1508 | def toStringProto(self, inObject, proto): | ||
1879 | 1509 | """ | ||
1880 | 1510 | Send C{inObject}, an integer file descriptor, over C{proto}'s | ||
1881 | 1511 | connection and return a unique identifier which will allow the | ||
1882 | 1512 | receiver to associate the file descriptor with this argument. | ||
1883 | 1513 | |||
1884 | 1514 | @param inObject: A file descriptor to duplicate over an AMP connection | ||
1885 | 1515 | as the value for this argument. | ||
1886 | 1516 | @type inObject: C{int} | ||
1887 | 1517 | |||
1888 | 1518 | @param proto: The protocol which will be used to send this descriptor. | ||
1889 | 1519 | This protocol must be connected via a transport providing | ||
1890 | 1520 | L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}. | ||
1891 | 1521 | |||
1892 | 1522 | @return: A byte string which can be used by the receiver to reconstruct | ||
1893 | 1523 | the file descriptor. | ||
1894 | 1524 | @type: C{bytes} | ||
1895 | 1525 | """ | ||
1896 | 1526 | identifier = proto._sendFileDescriptor(inObject) | ||
1897 | 1527 | outString = Integer.toStringProto(self, identifier, proto) | ||
1898 | 1528 | return outString | ||
1899 | 1529 | |||
1900 | 1530 | |||
1901 | 1531 | class Command: | ||
1902 | 1532 | """ | ||
1903 | 1533 | Subclass me to specify an AMP Command. | ||
1904 | 1534 | |||
1905 | 1535 | @cvar arguments: A list of 2-tuples of (name, Argument-subclass-instance), | ||
1906 | 1536 | specifying the names and values of the parameters which are required for | ||
1907 | 1537 | this command. | ||
1908 | 1538 | |||
1909 | 1539 | @cvar response: A list like L{arguments}, but instead used for the return | ||
1910 | 1540 | value. | ||
1911 | 1541 | |||
1912 | 1542 | @cvar errors: A mapping of subclasses of L{Exception} to wire-protocol tags | ||
1913 | 1543 | for errors represented as L{bytes}s. Responders which raise keys from this | ||
1914 | 1544 | dictionary will have the error translated to the corresponding tag on the | ||
1915 | 1545 | wire. Invokers which receive Deferreds from invoking this command with | ||
1916 | 1546 | L{AMP.callRemote} will potentially receive Failures with keys from this | ||
1917 | 1547 | mapping as their value. This mapping is inherited; if you declare a | ||
1918 | 1548 | command which handles C{FooError} as 'FOO_ERROR', then subclass it and | ||
1919 | 1549 | specify C{BarError} as 'BAR_ERROR', responders to the subclass may raise | ||
1920 | 1550 | either C{FooError} or C{BarError}, and invokers must be able to deal with | ||
1921 | 1551 | either of those exceptions. | ||
1922 | 1552 | |||
1923 | 1553 | @cvar fatalErrors: like 'errors', but errors in this list will always | ||
1924 | 1554 | terminate the connection, despite being of a recognizable error type. | ||
1925 | 1555 | |||
1926 | 1556 | @cvar commandType: The type of Box used to issue commands; useful only for | ||
1927 | 1557 | protocol-modifying behavior like startTLS or protocol switching. Defaults | ||
1928 | 1558 | to a plain vanilla L{Box}. | ||
1929 | 1559 | |||
1930 | 1560 | @cvar responseType: The type of Box used to respond to this command; only | ||
1931 | 1561 | useful for protocol-modifying behavior like startTLS or protocol switching. | ||
1932 | 1562 | Defaults to a plain vanilla L{Box}. | ||
1933 | 1563 | |||
1934 | 1564 | @ivar requiresAnswer: a boolean; defaults to True. Set it to False on your | ||
1935 | 1565 | subclass if you want callRemote to return None. Note: this is a hint only | ||
1936 | 1566 | to the client side of the protocol. The return-type of a command responder | ||
1937 | 1567 | method must always be a dictionary adhering to the contract specified by | ||
1938 | 1568 | L{response}, because clients are always free to request a response if they | ||
1939 | 1569 | want one. | ||
1940 | 1570 | """ | ||
1941 | 1571 | |||
1942 | 1572 | class __metaclass__(type): | ||
1943 | 1573 | """ | ||
1944 | 1574 | Metaclass hack to establish reverse-mappings for 'errors' and | ||
1945 | 1575 | 'fatalErrors' as class vars. | ||
1946 | 1576 | """ | ||
1947 | 1577 | def __new__(cls, name, bases, attrs): | ||
1948 | 1578 | reverseErrors = attrs['reverseErrors'] = {} | ||
1949 | 1579 | er = attrs['allErrors'] = {} | ||
1950 | 1580 | if 'commandName' not in attrs: | ||
1951 | 1581 | attrs['commandName'] = name | ||
1952 | 1582 | newtype = type.__new__(cls, name, bases, attrs) | ||
1953 | 1583 | errors = {} | ||
1954 | 1584 | fatalErrors = {} | ||
1955 | 1585 | accumulateClassDict(newtype, 'errors', errors) | ||
1956 | 1586 | accumulateClassDict(newtype, 'fatalErrors', fatalErrors) | ||
1957 | 1587 | for v, k in errors.viewitems(): | ||
1958 | 1588 | reverseErrors[k] = v | ||
1959 | 1589 | er[v] = k | ||
1960 | 1590 | for v, k in fatalErrors.viewitems(): | ||
1961 | 1591 | reverseErrors[k] = v | ||
1962 | 1592 | er[v] = k | ||
1963 | 1593 | return newtype | ||
1964 | 1594 | |||
1965 | 1595 | arguments = [] | ||
1966 | 1596 | response = [] | ||
1967 | 1597 | extra = [] | ||
1968 | 1598 | errors = {} | ||
1969 | 1599 | fatalErrors = {} | ||
1970 | 1600 | |||
1971 | 1601 | commandType = Box | ||
1972 | 1602 | responseType = Box | ||
1973 | 1603 | |||
1974 | 1604 | requiresAnswer = True | ||
1975 | 1605 | |||
1976 | 1606 | def __init__(self, **kw): | ||
1977 | 1607 | """ | ||
1978 | 1608 | Create an instance of this command with specified values for its | ||
1979 | 1609 | parameters. | ||
1980 | 1610 | |||
1981 | 1611 | @param kw: a dict containing an appropriate value for each name | ||
1982 | 1612 | specified in the L{arguments} attribute of my class. | ||
1983 | 1613 | |||
1984 | 1614 | @raise InvalidSignature: if you forgot any required arguments. | ||
1985 | 1615 | """ | ||
1986 | 1616 | self.structured = kw | ||
1987 | 1617 | givenArgs = set(kw) | ||
1988 | 1618 | forgotten = [] | ||
1989 | 1619 | for name, arg in self.arguments: | ||
1990 | 1620 | pythonName = _wireNameToPythonIdentifier(name) | ||
1991 | 1621 | if pythonName not in givenArgs and not arg.optional: | ||
1992 | 1622 | forgotten.append(pythonName) | ||
1993 | 1623 | if forgotten: | ||
1994 | 1624 | raise InvalidSignature("forgot %s for %s" % ( | ||
1995 | 1625 | ', '.join(forgotten), self.commandName)) | ||
1996 | 1626 | forgotten = [] | ||
1997 | 1627 | |||
1998 | 1628 | @classmethod | ||
1999 | 1629 | def makeResponse(cls, objects, proto): | ||
2000 | 1630 | """ | ||
2001 | 1631 | Serialize a mapping of arguments using this L{Command}'s | ||
2002 | 1632 | response schema. | ||
2003 | 1633 | |||
2004 | 1634 | @param objects: a dict with keys matching the names specified in | ||
2005 | 1635 | self.response, having values of the types that the Argument objects in | ||
2006 | 1636 | self.response can format. | ||
2007 | 1637 | |||
2008 | 1638 | @param proto: an L{AMP}. | ||
2009 | 1639 | |||
2010 | 1640 | @return: an L{AmpBox}. | ||
2011 | 1641 | """ | ||
2012 | 1642 | try: | ||
2013 | 1643 | responseType = cls.responseType() | ||
2014 | 1644 | except: | ||
2015 | 1645 | return fail() | ||
2016 | 1646 | return _objectsToStrings(objects, cls.response, responseType, proto) | ||
2017 | 1647 | |||
2018 | 1648 | @classmethod | ||
2019 | 1649 | def makeArguments(cls, objects, proto): | ||
2020 | 1650 | """ | ||
2021 | 1651 | Serialize a mapping of arguments using this L{Command}'s | ||
2022 | 1652 | argument schema. | ||
2023 | 1653 | |||
2024 | 1654 | @param objects: a dict with keys similar to the names specified in | ||
2025 | 1655 | self.arguments, having values of the types that the Argument objects in | ||
2026 | 1656 | self.arguments can parse. | ||
2027 | 1657 | |||
2028 | 1658 | @param proto: an L{AMP}. | ||
2029 | 1659 | |||
2030 | 1660 | @return: An instance of this L{Command}'s C{commandType}. | ||
2031 | 1661 | """ | ||
2032 | 1662 | allowedNames = set() | ||
2033 | 1663 | for (argName, ignored) in cls.arguments: | ||
2034 | 1664 | allowedNames.add(_wireNameToPythonIdentifier(argName)) | ||
2035 | 1665 | |||
2036 | 1666 | for intendedArg in objects: | ||
2037 | 1667 | if intendedArg not in allowedNames: | ||
2038 | 1668 | raise InvalidSignature( | ||
2039 | 1669 | "%s is not a valid argument" % (intendedArg,)) | ||
2040 | 1670 | return _objectsToStrings(objects, cls.arguments, cls.commandType(), | ||
2041 | 1671 | proto) | ||
2042 | 1672 | |||
2043 | 1673 | @classmethod | ||
2044 | 1674 | def parseResponse(cls, box, protocol): | ||
2045 | 1675 | """ | ||
2046 | 1676 | Parse a mapping of serialized arguments using this | ||
2047 | 1677 | L{Command}'s response schema. | ||
2048 | 1678 | |||
2049 | 1679 | @param box: A mapping of response-argument names to the | ||
2050 | 1680 | serialized forms of those arguments. | ||
2051 | 1681 | @param protocol: The L{AMP} protocol. | ||
2052 | 1682 | |||
2053 | 1683 | @return: A mapping of response-argument names to the parsed | ||
2054 | 1684 | forms. | ||
2055 | 1685 | """ | ||
2056 | 1686 | return _stringsToObjects(box, cls.response, protocol) | ||
2057 | 1687 | |||
2058 | 1688 | @classmethod | ||
2059 | 1689 | def parseArguments(cls, box, protocol): | ||
2060 | 1690 | """ | ||
2061 | 1691 | Parse a mapping of serialized arguments using this | ||
2062 | 1692 | L{Command}'s argument schema. | ||
2063 | 1693 | |||
2064 | 1694 | @param box: A mapping of argument names to the seralized forms | ||
2065 | 1695 | of those arguments. | ||
2066 | 1696 | @param protocol: The L{AMP} protocol. | ||
2067 | 1697 | |||
2068 | 1698 | @return: A mapping of argument names to the parsed forms. | ||
2069 | 1699 | """ | ||
2070 | 1700 | return _stringsToObjects(box, cls.arguments, protocol) | ||
2071 | 1701 | |||
2072 | 1702 | @classmethod | ||
2073 | 1703 | def responder(cls, methodfunc): | ||
2074 | 1704 | """ | ||
2075 | 1705 | Declare a method to be a responder for a particular command. | ||
2076 | 1706 | |||
2077 | 1707 | This is a decorator. | ||
2078 | 1708 | |||
2079 | 1709 | Use like so:: | ||
2080 | 1710 | |||
2081 | 1711 | class MyCommand(Command): | ||
2082 | 1712 | arguments = [('a', ...), ('b', ...)] | ||
2083 | 1713 | |||
2084 | 1714 | class MyProto(AMP): | ||
2085 | 1715 | def myFunMethod(self, a, b): | ||
2086 | 1716 | ... | ||
2087 | 1717 | MyCommand.responder(myFunMethod) | ||
2088 | 1718 | |||
2089 | 1719 | Notes: Although decorator syntax is not used within Twisted, this | ||
2090 | 1720 | function returns its argument and is therefore safe to use with | ||
2091 | 1721 | decorator syntax. | ||
2092 | 1722 | |||
2093 | 1723 | This is not thread safe. Don't declare AMP subclasses in other | ||
2094 | 1724 | threads. Don't declare responders outside the scope of AMP subclasses; | ||
2095 | 1725 | the behavior is undefined. | ||
2096 | 1726 | |||
2097 | 1727 | @param methodfunc: A function which will later become a method, which | ||
2098 | 1728 | has a keyword signature compatible with this command's L{argument} list | ||
2099 | 1729 | and returns a dictionary with a set of keys compatible with this | ||
2100 | 1730 | command's L{response} list. | ||
2101 | 1731 | |||
2102 | 1732 | @return: the methodfunc parameter. | ||
2103 | 1733 | """ | ||
2104 | 1734 | CommandLocator._currentClassCommands.append((cls, methodfunc)) | ||
2105 | 1735 | return methodfunc | ||
2106 | 1736 | |||
2107 | 1737 | # Our only instance method | ||
2108 | 1738 | def _doCommand(self, proto): | ||
2109 | 1739 | """ | ||
2110 | 1740 | Encode and send this Command to the given protocol. | ||
2111 | 1741 | |||
2112 | 1742 | @param proto: an AMP, representing the connection to send to. | ||
2113 | 1743 | |||
2114 | 1744 | @return: a Deferred which will fire or error appropriately when the | ||
2115 | 1745 | other side responds to the command (or error if the connection is lost | ||
2116 | 1746 | before it is responded to). | ||
2117 | 1747 | """ | ||
2118 | 1748 | |||
2119 | 1749 | def _massageError(error): | ||
2120 | 1750 | error.trap(RemoteAmpError) | ||
2121 | 1751 | rje = error.value | ||
2122 | 1752 | errorType = self.reverseErrors.get(rje.errorCode, | ||
2123 | 1753 | UnknownRemoteError) | ||
2124 | 1754 | return Failure(errorType(rje.description)) | ||
2125 | 1755 | |||
2126 | 1756 | d = proto._sendBoxCommand(self.commandName, | ||
2127 | 1757 | self.makeArguments(self.structured, proto), | ||
2128 | 1758 | self.requiresAnswer) | ||
2129 | 1759 | |||
2130 | 1760 | if self.requiresAnswer: | ||
2131 | 1761 | d.addCallback(self.parseResponse, proto) | ||
2132 | 1762 | d.addErrback(_massageError) | ||
2133 | 1763 | |||
2134 | 1764 | return d | ||
2135 | 1765 | |||
2136 | 1766 | |||
2137 | 1767 | class _NoCertificate: | ||
2138 | 1768 | """ | ||
2139 | 1769 | This is for peers which don't want to use a local certificate. Used by | ||
2140 | 1770 | AMP because AMP's internal language is all about certificates and this | ||
2141 | 1771 | duck-types in the appropriate place; this API isn't really stable though, | ||
2142 | 1772 | so it's not exposed anywhere public. | ||
2143 | 1773 | |||
2144 | 1774 | For clients, it will use ephemeral DH keys, or whatever the default is for | ||
2145 | 1775 | certificate-less clients in OpenSSL. For servers, it will generate a | ||
2146 | 1776 | temporary self-signed certificate with garbage values in the DN and use | ||
2147 | 1777 | that. | ||
2148 | 1778 | """ | ||
2149 | 1779 | |||
2150 | 1780 | def __init__(self, client): | ||
2151 | 1781 | """ | ||
2152 | 1782 | Create a _NoCertificate which either is or isn't for the client side of | ||
2153 | 1783 | the connection. | ||
2154 | 1784 | |||
2155 | 1785 | @param client: True if we are a client and should truly have no | ||
2156 | 1786 | certificate and be anonymous, False if we are a server and actually | ||
2157 | 1787 | have to generate a temporary certificate. | ||
2158 | 1788 | |||
2159 | 1789 | @type client: bool | ||
2160 | 1790 | """ | ||
2161 | 1791 | self.client = client | ||
2162 | 1792 | |||
2163 | 1793 | def options(self, *authorities): | ||
2164 | 1794 | """ | ||
2165 | 1795 | Behaves like L{twisted.internet.ssl.PrivateCertificate.options}(). | ||
2166 | 1796 | """ | ||
2167 | 1797 | if not self.client: | ||
2168 | 1798 | # do some crud with sslverify to generate a temporary self-signed | ||
2169 | 1799 | # certificate. This is SLOOOWWWWW so it is only in the absolute | ||
2170 | 1800 | # worst, most naive case. | ||
2171 | 1801 | |||
2172 | 1802 | # We have to do this because OpenSSL will not let both the server | ||
2173 | 1803 | # and client be anonymous. | ||
2174 | 1804 | sharedDN = DN(CN='TEMPORARY CERTIFICATE') | ||
2175 | 1805 | key = KeyPair.generate() | ||
2176 | 1806 | cr = key.certificateRequest(sharedDN) | ||
2177 | 1807 | sscrd = key.signCertificateRequest( | ||
2178 | 1808 | sharedDN, cr, lambda dn: True, 1) | ||
2179 | 1809 | cert = key.newCertificate(sscrd) | ||
2180 | 1810 | return cert.options(*authorities) | ||
2181 | 1811 | options = dict() | ||
2182 | 1812 | if authorities: | ||
2183 | 1813 | options.update( | ||
2184 | 1814 | verify=True, requireCertificate=True, | ||
2185 | 1815 | caCerts=[auth.original for auth in authorities]) | ||
2186 | 1816 | occo = CertificateOptions(**options) | ||
2187 | 1817 | return occo | ||
2188 | 1818 | |||
2189 | 1819 | |||
2190 | 1820 | class _TLSBox(AmpBox): | ||
2191 | 1821 | """ | ||
2192 | 1822 | I am an AmpBox that, upon being sent, initiates a TLS connection. | ||
2193 | 1823 | """ | ||
2194 | 1824 | |||
2195 | 1825 | __slots__ = [] | ||
2196 | 1826 | |||
2197 | 1827 | def __init__(self): | ||
2198 | 1828 | if ssl is None: | ||
2199 | 1829 | raise RemoteAmpError("TLS_ERROR", "TLS not available") | ||
2200 | 1830 | AmpBox.__init__(self) | ||
2201 | 1831 | |||
2202 | 1832 | def _keyprop(k, default): | ||
2203 | 1833 | return property(lambda self: self.get(k, default)) | ||
2204 | 1834 | |||
2205 | 1835 | # These properties are described in startTLS | ||
2206 | 1836 | certificate = _keyprop('tls_localCertificate', _NoCertificate(False)) | ||
2207 | 1837 | verify = _keyprop('tls_verifyAuthorities', None) | ||
2208 | 1838 | |||
2209 | 1839 | def _sendTo(self, proto): | ||
2210 | 1840 | """ | ||
2211 | 1841 | Send my encoded value to the protocol, then initiate TLS. | ||
2212 | 1842 | """ | ||
2213 | 1843 | ab = AmpBox(self) | ||
2214 | 1844 | for k in ['tls_localCertificate', | ||
2215 | 1845 | 'tls_verifyAuthorities']: | ||
2216 | 1846 | ab.pop(k, None) | ||
2217 | 1847 | ab._sendTo(proto) | ||
2218 | 1848 | proto._startTLS(self.certificate, self.verify) | ||
2219 | 1849 | |||
2220 | 1850 | |||
2221 | 1851 | class _LocalArgument(String): | ||
2222 | 1852 | """ | ||
2223 | 1853 | Local arguments are never actually relayed across the wire. This is just a | ||
2224 | 1854 | shim so that StartTLS can pretend to have some arguments: if arguments | ||
2225 | 1855 | acquire documentation properties, replace this with something nicer later. | ||
2226 | 1856 | """ | ||
2227 | 1857 | |||
2228 | 1858 | def fromBox(self, name, strings, objects, proto): | ||
2229 | 1859 | pass | ||
2230 | 1860 | |||
2231 | 1861 | |||
2232 | 1862 | class StartTLS(Command): | ||
2233 | 1863 | """ | ||
2234 | 1864 | Use, or subclass, me to implement a command that starts TLS. | ||
2235 | 1865 | |||
2236 | 1866 | Callers of StartTLS may pass several special arguments, which affect the | ||
2237 | 1867 | TLS negotiation: | ||
2238 | 1868 | |||
2239 | 1869 | - tls_localCertificate: This is a | ||
2240 | 1870 | twisted.internet.ssl.PrivateCertificate which will be used to secure | ||
2241 | 1871 | the side of the connection it is returned on. | ||
2242 | 1872 | |||
2243 | 1873 | - tls_verifyAuthorities: This is a list of | ||
2244 | 1874 | twisted.internet.ssl.Certificate objects that will be used as the | ||
2245 | 1875 | certificate authorities to verify our peer's certificate. | ||
2246 | 1876 | |||
2247 | 1877 | Each of those special parameters may also be present as a key in the | ||
2248 | 1878 | response dictionary. | ||
2249 | 1879 | """ | ||
2250 | 1880 | |||
2251 | 1881 | arguments = [("tls_localCertificate", _LocalArgument(optional=True)), | ||
2252 | 1882 | ("tls_verifyAuthorities", _LocalArgument(optional=True))] | ||
2253 | 1883 | |||
2254 | 1884 | response = [("tls_localCertificate", _LocalArgument(optional=True)), | ||
2255 | 1885 | ("tls_verifyAuthorities", _LocalArgument(optional=True))] | ||
2256 | 1886 | |||
2257 | 1887 | responseType = _TLSBox | ||
2258 | 1888 | |||
2259 | 1889 | def __init__(self, **kw): | ||
2260 | 1890 | """ | ||
2261 | 1891 | Create a StartTLS command. (This is private. Use AMP.callRemote.) | ||
2262 | 1892 | |||
2263 | 1893 | @param tls_localCertificate: the PrivateCertificate object to use to | ||
2264 | 1894 | secure the connection. If it's None, or unspecified, an ephemeral DH | ||
2265 | 1895 | key is used instead. | ||
2266 | 1896 | |||
2267 | 1897 | @param tls_verifyAuthorities: a list of Certificate objects which | ||
2268 | 1898 | represent root certificates to verify our peer with. | ||
2269 | 1899 | """ | ||
2270 | 1900 | if ssl is None: | ||
2271 | 1901 | raise RuntimeError("TLS not available.") | ||
2272 | 1902 | self.certificate = kw.pop('tls_localCertificate', _NoCertificate(True)) | ||
2273 | 1903 | self.authorities = kw.pop('tls_verifyAuthorities', None) | ||
2274 | 1904 | Command.__init__(self, **kw) | ||
2275 | 1905 | |||
2276 | 1906 | def _doCommand(self, proto): | ||
2277 | 1907 | """ | ||
2278 | 1908 | When a StartTLS command is sent, prepare to start TLS, but don't | ||
2279 | 1909 | actually do it; wait for the acknowledgement, then initiate the TLS | ||
2280 | 1910 | handshake. | ||
2281 | 1911 | """ | ||
2282 | 1912 | d = Command._doCommand(self, proto) | ||
2283 | 1913 | proto._prepareTLS(self.certificate, self.authorities) | ||
2284 | 1914 | |||
2285 | 1915 | # XXX before we get back to user code we are going to start TLS... | ||
2286 | 1916 | def actuallystart(response): | ||
2287 | 1917 | proto._startTLS(self.certificate, self.authorities) | ||
2288 | 1918 | return response | ||
2289 | 1919 | d.addCallback(actuallystart) | ||
2290 | 1920 | |||
2291 | 1921 | return d | ||
2292 | 1922 | |||
2293 | 1923 | |||
2294 | 1924 | class ProtocolSwitchCommand(Command): | ||
2295 | 1925 | """ | ||
2296 | 1926 | Use this command to switch from something Amp-derived to a different | ||
2297 | 1927 | protocol mid-connection. This can be useful to use amp as the | ||
2298 | 1928 | connection-startup negotiation phase. Since TLS is a different layer | ||
2299 | 1929 | entirely, you can use Amp to negotiate the security parameters of your | ||
2300 | 1930 | connection, then switch to a different protocol, and the connection will | ||
2301 | 1931 | remain secured. | ||
2302 | 1932 | """ | ||
2303 | 1933 | |||
2304 | 1934 | def __init__(self, _protoToSwitchToFactory, **kw): | ||
2305 | 1935 | """ | ||
2306 | 1936 | Create a ProtocolSwitchCommand. | ||
2307 | 1937 | |||
2308 | 1938 | @param _protoToSwitchToFactory: a ProtocolFactory which will generate | ||
2309 | 1939 | the Protocol to switch to. | ||
2310 | 1940 | |||
2311 | 1941 | @param kw: Keyword arguments, encoded and handled normally as | ||
2312 | 1942 | L{Command} would. | ||
2313 | 1943 | """ | ||
2314 | 1944 | |||
2315 | 1945 | self.protoToSwitchToFactory = _protoToSwitchToFactory | ||
2316 | 1946 | super(ProtocolSwitchCommand, self).__init__(**kw) | ||
2317 | 1947 | |||
2318 | 1948 | @classmethod | ||
2319 | 1949 | def makeResponse(cls, innerProto, proto): | ||
2320 | 1950 | return _SwitchBox(innerProto) | ||
2321 | 1951 | |||
2322 | 1952 | def _doCommand(self, proto): | ||
2323 | 1953 | """ | ||
2324 | 1954 | When we emit a ProtocolSwitchCommand, lock the protocol, but don't | ||
2325 | 1955 | actually switch to the new protocol unless an acknowledgement is | ||
2326 | 1956 | received. If an error is received, switch back. | ||
2327 | 1957 | """ | ||
2328 | 1958 | d = super(ProtocolSwitchCommand, self)._doCommand(proto) | ||
2329 | 1959 | proto._lockForSwitch() | ||
2330 | 1960 | |||
2331 | 1961 | def switchNow(ign): | ||
2332 | 1962 | innerProto = self.protoToSwitchToFactory.buildProtocol( | ||
2333 | 1963 | proto.transport.getPeer()) | ||
2334 | 1964 | proto._switchTo(innerProto, self.protoToSwitchToFactory) | ||
2335 | 1965 | return ign | ||
2336 | 1966 | |||
2337 | 1967 | def handle(ign): | ||
2338 | 1968 | proto._unlockFromSwitch() | ||
2339 | 1969 | self.protoToSwitchToFactory.clientConnectionFailed( | ||
2340 | 1970 | None, Failure(CONNECTION_LOST)) | ||
2341 | 1971 | return ign | ||
2342 | 1972 | |||
2343 | 1973 | return d.addCallbacks(switchNow, handle) | ||
2344 | 1974 | |||
2345 | 1975 | |||
2346 | 1976 | class _DescriptorExchanger(object): | ||
2347 | 1977 | """ | ||
2348 | 1978 | L{_DescriptorExchanger} is a mixin for L{BinaryBoxProtocol} which adds | ||
2349 | 1979 | support for receiving file descriptors, a feature offered by | ||
2350 | 1980 | L{IUNIXTransport<twisted.internet.interfaces.IUNIXTransport>}. | ||
2351 | 1981 | |||
2352 | 1982 | @ivar _descriptors: Temporary storage for all file descriptors received. | ||
2353 | 1983 | Values in this dictionary are the file descriptors (as integers). Keys | ||
2354 | 1984 | in this dictionary are ordinals giving the order in which each | ||
2355 | 1985 | descriptor was received. The ordering information is used to allow | ||
2356 | 1986 | L{Descriptor} to determine which is the correct descriptor for any | ||
2357 | 1987 | particular usage of that argument type. | ||
2358 | 1988 | @type _descriptors: C{dict} | ||
2359 | 1989 | |||
2360 | 1990 | @ivar _sendingDescriptorCounter: A no-argument callable which returns the | ||
2361 | 1991 | ordinals, starting from 0. This is used to construct values for | ||
2362 | 1992 | C{_sendFileDescriptor}. | ||
2363 | 1993 | |||
2364 | 1994 | @ivar _receivingDescriptorCounter: A no-argument callable which returns the | ||
2365 | 1995 | ordinals, starting from 0. This is used to construct values for | ||
2366 | 1996 | C{fileDescriptorReceived}. | ||
2367 | 1997 | """ | ||
2368 | 1998 | |||
2369 | 1999 | implements(IFileDescriptorReceiver) | ||
2370 | 2000 | |||
2371 | 2001 | def __init__(self): | ||
2372 | 2002 | self._descriptors = {} | ||
2373 | 2003 | self._getDescriptor = self._descriptors.pop | ||
2374 | 2004 | self._sendingDescriptorCounter = count() | ||
2375 | 2005 | self._receivingDescriptorCounter = count() | ||
2376 | 2006 | |||
2377 | 2007 | def _sendFileDescriptor(self, descriptor): | ||
2378 | 2008 | """ | ||
2379 | 2009 | Assign and return the next ordinal to the given descriptor after | ||
2380 | 2010 | sending the descriptor over this protocol's transport. | ||
2381 | 2011 | """ | ||
2382 | 2012 | self.transport.sendFileDescriptor(descriptor) | ||
2383 | 2013 | return next(self._sendingDescriptorCounter) | ||
2384 | 2014 | |||
2385 | 2015 | def fileDescriptorReceived(self, descriptor): | ||
2386 | 2016 | """ | ||
2387 | 2017 | Collect received file descriptors to be claimed later by L{Descriptor}. | ||
2388 | 2018 | |||
2389 | 2019 | @param descriptor: The received file descriptor. | ||
2390 | 2020 | @type descriptor: C{int} | ||
2391 | 2021 | """ | ||
2392 | 2022 | self._descriptors[next(self._receivingDescriptorCounter)] = descriptor | ||
2393 | 2023 | |||
2394 | 2024 | |||
2395 | 2025 | class BinaryBoxProtocol(StatefulStringProtocol, Int32StringReceiver, | ||
2396 | 2026 | _DescriptorExchanger): | ||
2397 | 2027 | """ | ||
2398 | 2028 | A protocol for receiving L{AmpBox}es - key/value pairs - via | ||
2399 | 2029 | length-prefixed strings. A box is composed of: | ||
2400 | 2030 | |||
2401 | 2031 | - any number of key-value pairs, described by: | ||
2402 | 2032 | - a 4-byte network-endian packed key length (of which the first | ||
2403 | 2033 | 3 bytes must be null, and the last must be non-null: i.e. the | ||
2404 | 2034 | value of the length must be 1-255) | ||
2405 | 2035 | - a key, comprised of that many bytes | ||
2406 | 2036 | - a 4-byte network-endian unsigned value length (up to the maximum | ||
2407 | 2037 | of C{MAX_VALUE_LENGTH}) | ||
2408 | 2038 | - a value, comprised of that many bytes | ||
2409 | 2039 | - 4 null bytes | ||
2410 | 2040 | |||
2411 | 2041 | In other words, an even number of strings prefixed with packed unsigned | ||
2412 | 2042 | 32-bit integers, and then a 0-length string to indicate the end of the box. | ||
2413 | 2043 | |||
2414 | 2044 | This protocol also implements 2 extra private bits of functionality related | ||
2415 | 2045 | to the byte boundaries between messages; it can start TLS between two given | ||
2416 | 2046 | boxes or switch to an entirely different protocol. However, due to some | ||
2417 | 2047 | tricky elements of the implementation, the public interface to this | ||
2418 | 2048 | functionality is L{ProtocolSwitchCommand} and L{StartTLS}. | ||
2419 | 2049 | |||
2420 | 2050 | @ivar _keyLengthLimitExceeded: A flag which is only true when the | ||
2421 | 2051 | connection is being closed because a key length prefix which was longer | ||
2422 | 2052 | than allowed by the protocol was received. | ||
2423 | 2053 | |||
2424 | 2054 | @ivar boxReceiver: an L{IBoxReceiver} provider, whose L{ampBoxReceived} | ||
2425 | 2055 | method will be invoked for each L{AmpBox} that is received. | ||
2426 | 2056 | """ | ||
2427 | 2057 | |||
2428 | 2058 | implements(IBoxSender) | ||
2429 | 2059 | |||
2430 | 2060 | _justStartedTLS = False | ||
2431 | 2061 | _startingTLSBuffer = None | ||
2432 | 2062 | _locked = False | ||
2433 | 2063 | _currentKey = None | ||
2434 | 2064 | _currentBox = None | ||
2435 | 2065 | |||
2436 | 2066 | _keyLengthLimitExceeded = False | ||
2437 | 2067 | |||
2438 | 2068 | hostCertificate = None | ||
2439 | 2069 | noPeerCertificate = False # for tests | ||
2440 | 2070 | innerProtocol = None | ||
2441 | 2071 | innerProtocolClientFactory = None | ||
2442 | 2072 | |||
2443 | 2073 | def __init__(self, boxReceiver): | ||
2444 | 2074 | _DescriptorExchanger.__init__(self) | ||
2445 | 2075 | self.boxReceiver = boxReceiver | ||
2446 | 2076 | |||
2447 | 2077 | def _switchTo(self, newProto, clientFactory=None): | ||
2448 | 2078 | """ | ||
2449 | 2079 | Switch this BinaryBoxProtocol's transport to a new protocol. You need | ||
2450 | 2080 | to do this 'simultaneously' on both ends of a connection; the easiest | ||
2451 | 2081 | way to do this is to use a subclass of ProtocolSwitchCommand. | ||
2452 | 2082 | |||
2453 | 2083 | @param newProto: the new protocol instance to switch to. | ||
2454 | 2084 | |||
2455 | 2085 | @param clientFactory: the ClientFactory to send the | ||
2456 | 2086 | L{clientConnectionLost} notification to. | ||
2457 | 2087 | """ | ||
2458 | 2088 | # All the data that Int32StringReceiver has not yet dealt with belongs | ||
2459 | 2089 | # to our new protocol: luckily it's keeping that in a handy (although | ||
2460 | 2090 | # ostensibly internal) variable for us: | ||
2461 | 2091 | newProtoData = self.recvd | ||
2462 | 2092 | # We're quite possibly in the middle of a 'dataReceived' loop in | ||
2463 | 2093 | # Int32StringReceiver: let's make sure that the next iteration, the | ||
2464 | 2094 | # loop will break and not attempt to look at something that isn't a | ||
2465 | 2095 | # length prefix. | ||
2466 | 2096 | self.recvd = '' | ||
2467 | 2097 | # Finally, do the actual work of setting up the protocol and delivering | ||
2468 | 2098 | # its first chunk of data, if one is available. | ||
2469 | 2099 | self.innerProtocol = newProto | ||
2470 | 2100 | self.innerProtocolClientFactory = clientFactory | ||
2471 | 2101 | newProto.makeConnection(self.transport) | ||
2472 | 2102 | if newProtoData: | ||
2473 | 2103 | newProto.dataReceived(newProtoData) | ||
2474 | 2104 | |||
2475 | 2105 | def sendBox(self, box): | ||
2476 | 2106 | """ | ||
2477 | 2107 | Send a amp32.Box to my peer. | ||
2478 | 2108 | |||
2479 | 2109 | Note: transport.write is never called outside of this method. | ||
2480 | 2110 | |||
2481 | 2111 | @param box: an AmpBox. | ||
2482 | 2112 | |||
2483 | 2113 | @raise ProtocolSwitched: if the protocol has previously been switched. | ||
2484 | 2114 | |||
2485 | 2115 | @raise ConnectionLost: if the connection has previously been lost. | ||
2486 | 2116 | """ | ||
2487 | 2117 | if self._locked: | ||
2488 | 2118 | raise ProtocolSwitched( | ||
2489 | 2119 | "This connection has switched: no AMP traffic allowed.") | ||
2490 | 2120 | if self.transport is None: | ||
2491 | 2121 | raise ConnectionLost() | ||
2492 | 2122 | if self._startingTLSBuffer is not None: | ||
2493 | 2123 | self._startingTLSBuffer.append(box) | ||
2494 | 2124 | else: | ||
2495 | 2125 | self.transport.write(box.serialize()) | ||
2496 | 2126 | |||
2497 | 2127 | def makeConnection(self, transport): | ||
2498 | 2128 | """ | ||
2499 | 2129 | Notify L{boxReceiver} that it is about to receive boxes from this | ||
2500 | 2130 | protocol by invoking L{startReceivingBoxes}. | ||
2501 | 2131 | """ | ||
2502 | 2132 | self.transport = transport | ||
2503 | 2133 | self.boxReceiver.startReceivingBoxes(self) | ||
2504 | 2134 | self.connectionMade() | ||
2505 | 2135 | |||
2506 | 2136 | def dataReceived(self, data): | ||
2507 | 2137 | """ | ||
2508 | 2138 | Either parse incoming data as L{AmpBox}es or relay it to our nested | ||
2509 | 2139 | protocol. | ||
2510 | 2140 | """ | ||
2511 | 2141 | if self._justStartedTLS: | ||
2512 | 2142 | self._justStartedTLS = False | ||
2513 | 2143 | # If we already have an inner protocol, then we don't deliver data to | ||
2514 | 2144 | # the protocol parser any more; we just hand it off. | ||
2515 | 2145 | if self.innerProtocol is not None: | ||
2516 | 2146 | self.innerProtocol.dataReceived(data) | ||
2517 | 2147 | return | ||
2518 | 2148 | return Int32StringReceiver.dataReceived(self, data) | ||
2519 | 2149 | |||
2520 | 2150 | def connectionLost(self, reason): | ||
2521 | 2151 | """ | ||
2522 | 2152 | The connection was lost; notify any nested protocol. | ||
2523 | 2153 | """ | ||
2524 | 2154 | if self.innerProtocol is not None: | ||
2525 | 2155 | self.innerProtocol.connectionLost(reason) | ||
2526 | 2156 | if self.innerProtocolClientFactory is not None: | ||
2527 | 2157 | self.innerProtocolClientFactory.clientConnectionLost( | ||
2528 | 2158 | None, reason) | ||
2529 | 2159 | if self._keyLengthLimitExceeded: | ||
2530 | 2160 | failReason = Failure(TooLong(True, False, None, None)) | ||
2531 | 2161 | elif reason.check(ConnectionClosed) and self._justStartedTLS: | ||
2532 | 2162 | # We just started TLS and haven't received any data. This means | ||
2533 | 2163 | # the other connection didn't like our cert (although they may not | ||
2534 | 2164 | # have told us why - later Twisted should make 'reason' into a TLS | ||
2535 | 2165 | # error.) | ||
2536 | 2166 | failReason = PeerVerifyError( | ||
2537 | 2167 | "Peer rejected our certificate for an unknown reason.") | ||
2538 | 2168 | else: | ||
2539 | 2169 | failReason = reason | ||
2540 | 2170 | self.boxReceiver.stopReceivingBoxes(failReason) | ||
2541 | 2171 | |||
2542 | 2172 | # The longest key allowed | ||
2543 | 2173 | _MAX_KEY_LENGTH = MAX_KEY_LENGTH | ||
2544 | 2174 | |||
2545 | 2175 | # The longest value allowed (this is somewhat redundant, as longer values | ||
2546 | 2176 | # cannot be encoded - ah well). | ||
2547 | 2177 | _MAX_VALUE_LENGTH = MAX_VALUE_LENGTH | ||
2548 | 2178 | |||
2549 | 2179 | # The first thing received is a key. | ||
2550 | 2180 | MAX_LENGTH = _MAX_KEY_LENGTH | ||
2551 | 2181 | |||
2552 | 2182 | def proto_init(self, string): | ||
2553 | 2183 | """ | ||
2554 | 2184 | String received in the 'init' state. | ||
2555 | 2185 | """ | ||
2556 | 2186 | self._currentBox = AmpBox() | ||
2557 | 2187 | return self.proto_key(string) | ||
2558 | 2188 | |||
2559 | 2189 | def proto_key(self, string): | ||
2560 | 2190 | """ | ||
2561 | 2191 | String received in the 'key' state. If the key is empty, a complete | ||
2562 | 2192 | box has been received. | ||
2563 | 2193 | """ | ||
2564 | 2194 | if string: | ||
2565 | 2195 | self._currentKey = string | ||
2566 | 2196 | self.MAX_LENGTH = self._MAX_VALUE_LENGTH | ||
2567 | 2197 | return 'value' | ||
2568 | 2198 | else: | ||
2569 | 2199 | self.boxReceiver.ampBoxReceived(self._currentBox) | ||
2570 | 2200 | self._currentBox = None | ||
2571 | 2201 | return 'init' | ||
2572 | 2202 | |||
2573 | 2203 | def proto_value(self, string): | ||
2574 | 2204 | """ | ||
2575 | 2205 | String received in the 'value' state. | ||
2576 | 2206 | """ | ||
2577 | 2207 | self._currentBox[self._currentKey] = string | ||
2578 | 2208 | self._currentKey = None | ||
2579 | 2209 | self.MAX_LENGTH = self._MAX_KEY_LENGTH | ||
2580 | 2210 | return 'key' | ||
2581 | 2211 | |||
2582 | 2212 | def lengthLimitExceeded(self, length): | ||
2583 | 2213 | """ | ||
2584 | 2214 | The key length limit was exceeded. Disconnect the transport and make | ||
2585 | 2215 | sure a meaningful exception is reported. | ||
2586 | 2216 | """ | ||
2587 | 2217 | self._keyLengthLimitExceeded = True | ||
2588 | 2218 | self.transport.loseConnection() | ||
2589 | 2219 | |||
2590 | 2220 | def _lockForSwitch(self): | ||
2591 | 2221 | """ | ||
2592 | 2222 | Lock this binary protocol so that no further boxes may be sent. This | ||
2593 | 2223 | is used when sending a request to switch underlying protocols. You | ||
2594 | 2224 | probably want to subclass ProtocolSwitchCommand rather than calling | ||
2595 | 2225 | this directly. | ||
2596 | 2226 | """ | ||
2597 | 2227 | self._locked = True | ||
2598 | 2228 | |||
2599 | 2229 | def _unlockFromSwitch(self): | ||
2600 | 2230 | """ | ||
2601 | 2231 | Unlock this locked binary protocol so that further boxes may be sent | ||
2602 | 2232 | again. This is used after an attempt to switch protocols has failed | ||
2603 | 2233 | for some reason. | ||
2604 | 2234 | """ | ||
2605 | 2235 | if self.innerProtocol is not None: | ||
2606 | 2236 | raise ProtocolSwitched( | ||
2607 | 2237 | "Protocol already switched. Cannot unlock.") | ||
2608 | 2238 | self._locked = False | ||
2609 | 2239 | |||
2610 | 2240 | def _prepareTLS(self, certificate, verifyAuthorities): | ||
2611 | 2241 | """ | ||
2612 | 2242 | Used by StartTLSCommand to put us into the state where we don't | ||
2613 | 2243 | actually send things that get sent, instead we buffer them. see | ||
2614 | 2244 | L{_sendBox}. | ||
2615 | 2245 | """ | ||
2616 | 2246 | self._startingTLSBuffer = [] | ||
2617 | 2247 | if self.hostCertificate is not None: | ||
2618 | 2248 | raise OnlyOneTLS( | ||
2619 | 2249 | "Previously authenticated connection between %s and %s " | ||
2620 | 2250 | "is trying to re-establish as %s" % ( | ||
2621 | 2251 | self.hostCertificate, | ||
2622 | 2252 | self.peerCertificate, | ||
2623 | 2253 | (certificate, verifyAuthorities))) | ||
2624 | 2254 | |||
2625 | 2255 | def _startTLS(self, certificate, verifyAuthorities): | ||
2626 | 2256 | """ | ||
2627 | 2257 | Used by TLSBox to initiate the SSL handshake. | ||
2628 | 2258 | |||
2629 | 2259 | @param certificate: a L{twisted.internet.ssl.PrivateCertificate} for | ||
2630 | 2260 | use locally. | ||
2631 | 2261 | |||
2632 | 2262 | @param verifyAuthorities: L{twisted.internet.ssl.Certificate} instances | ||
2633 | 2263 | representing certificate authorities which will verify our peer. | ||
2634 | 2264 | """ | ||
2635 | 2265 | self.hostCertificate = certificate | ||
2636 | 2266 | self._justStartedTLS = True | ||
2637 | 2267 | if verifyAuthorities is None: | ||
2638 | 2268 | verifyAuthorities = () | ||
2639 | 2269 | self.transport.startTLS(certificate.options(*verifyAuthorities)) | ||
2640 | 2270 | stlsb = self._startingTLSBuffer | ||
2641 | 2271 | if stlsb is not None: | ||
2642 | 2272 | self._startingTLSBuffer = None | ||
2643 | 2273 | for box in stlsb: | ||
2644 | 2274 | self.sendBox(box) | ||
2645 | 2275 | |||
2646 | 2276 | def _getPeerCertificate(self): | ||
2647 | 2277 | if self.noPeerCertificate: | ||
2648 | 2278 | return None | ||
2649 | 2279 | return Certificate.peerFromTransport(self.transport) | ||
2650 | 2280 | peerCertificate = property(_getPeerCertificate) | ||
2651 | 2281 | |||
2652 | 2282 | def unhandledError(self, failure): | ||
2653 | 2283 | """ | ||
2654 | 2284 | The buck stops here. This error was completely unhandled, time to | ||
2655 | 2285 | terminate the connection. | ||
2656 | 2286 | """ | ||
2657 | 2287 | log.err( | ||
2658 | 2288 | failure, | ||
2659 | 2289 | "Amp server or network failure unhandled by client application. " | ||
2660 | 2290 | "Dropping connection! To avoid, add errbacks to ALL remote " | ||
2661 | 2291 | "commands!") | ||
2662 | 2292 | if self.transport is not None: | ||
2663 | 2293 | self.transport.loseConnection() | ||
2664 | 2294 | |||
2665 | 2295 | def _defaultStartTLSResponder(self): | ||
2666 | 2296 | """ | ||
2667 | 2297 | The default TLS responder doesn't specify any certificate or anything. | ||
2668 | 2298 | |||
2669 | 2299 | From a security perspective, it's little better than a plain-text | ||
2670 | 2300 | connection - but it is still a *bit* better, so it's included for | ||
2671 | 2301 | convenience. | ||
2672 | 2302 | |||
2673 | 2303 | You probably want to override this by providing your own | ||
2674 | 2304 | C{StartTLS.responder}. | ||
2675 | 2305 | """ | ||
2676 | 2306 | return {} | ||
2677 | 2307 | StartTLS.responder(_defaultStartTLSResponder) | ||
2678 | 2308 | |||
2679 | 2309 | |||
2680 | 2310 | class AMP(BinaryBoxProtocol, BoxDispatcher, | ||
2681 | 2311 | CommandLocator, SimpleStringLocator): | ||
2682 | 2312 | """ | ||
2683 | 2313 | This protocol is an AMP connection. See the module docstring for protocol | ||
2684 | 2314 | details. | ||
2685 | 2315 | """ | ||
2686 | 2316 | |||
2687 | 2317 | _ampInitialized = False | ||
2688 | 2318 | |||
2689 | 2319 | def __init__(self, boxReceiver=None, locator=None): | ||
2690 | 2320 | # For backwards compatibility. When AMP did not separate parsing logic | ||
2691 | 2321 | # (L{BinaryBoxProtocol}), request-response logic (L{BoxDispatcher}) and | ||
2692 | 2322 | # command routing (L{CommandLocator}), it did not have a constructor. | ||
2693 | 2323 | # Now it does, so old subclasses might have defined their own that did | ||
2694 | 2324 | # not upcall. If this flag isn't set, we'll call the constructor in | ||
2695 | 2325 | # makeConnection before anything actually happens. | ||
2696 | 2326 | self._ampInitialized = True | ||
2697 | 2327 | if boxReceiver is None: | ||
2698 | 2328 | boxReceiver = self | ||
2699 | 2329 | if locator is None: | ||
2700 | 2330 | locator = self | ||
2701 | 2331 | BoxDispatcher.__init__(self, locator) | ||
2702 | 2332 | BinaryBoxProtocol.__init__(self, boxReceiver) | ||
2703 | 2333 | |||
2704 | 2334 | def locateResponder(self, name): | ||
2705 | 2335 | """ | ||
2706 | 2336 | Unify the implementations of L{CommandLocator} and | ||
2707 | 2337 | L{SimpleStringLocator} to perform both kinds of dispatch, preferring | ||
2708 | 2338 | L{CommandLocator}. | ||
2709 | 2339 | """ | ||
2710 | 2340 | firstResponder = CommandLocator.locateResponder(self, name) | ||
2711 | 2341 | if firstResponder is not None: | ||
2712 | 2342 | return firstResponder | ||
2713 | 2343 | secondResponder = SimpleStringLocator.locateResponder(self, name) | ||
2714 | 2344 | return secondResponder | ||
2715 | 2345 | |||
2716 | 2346 | def __repr__(self): | ||
2717 | 2347 | """ | ||
2718 | 2348 | A verbose string representation which gives us information about this | ||
2719 | 2349 | AMP connection. | ||
2720 | 2350 | """ | ||
2721 | 2351 | if self.innerProtocol is not None: | ||
2722 | 2352 | innerRepr = ' inner %r' % (self.innerProtocol,) | ||
2723 | 2353 | else: | ||
2724 | 2354 | innerRepr = '' | ||
2725 | 2355 | return '<%s%s at 0x%x>' % ( | ||
2726 | 2356 | self.__class__.__name__, innerRepr, id(self)) | ||
2727 | 2357 | |||
2728 | 2358 | def makeConnection(self, transport): | ||
2729 | 2359 | """ | ||
2730 | 2360 | Emit a helpful log message when the connection is made. | ||
2731 | 2361 | """ | ||
2732 | 2362 | if not self._ampInitialized: | ||
2733 | 2363 | # See comment in the constructor re: backward compatibility. I | ||
2734 | 2364 | # should probably emit a deprecation warning here. | ||
2735 | 2365 | AMP.__init__(self) | ||
2736 | 2366 | # Save these so we can emit a similar log message in L{connectionLost}. | ||
2737 | 2367 | self._transportPeer = transport.getPeer() | ||
2738 | 2368 | self._transportHost = transport.getHost() | ||
2739 | 2369 | log.msg("%s connection established (HOST:%s PEER:%s)" % ( | ||
2740 | 2370 | self.__class__.__name__, | ||
2741 | 2371 | self._transportHost, | ||
2742 | 2372 | self._transportPeer)) | ||
2743 | 2373 | BinaryBoxProtocol.makeConnection(self, transport) | ||
2744 | 2374 | |||
2745 | 2375 | def connectionLost(self, reason): | ||
2746 | 2376 | """ | ||
2747 | 2377 | Emit a helpful log message when the connection is lost. | ||
2748 | 2378 | """ | ||
2749 | 2379 | log.msg("%s connection lost (HOST:%s PEER:%s)" % | ||
2750 | 2380 | (self.__class__.__name__, | ||
2751 | 2381 | self._transportHost, | ||
2752 | 2382 | self._transportPeer)) | ||
2753 | 2383 | BinaryBoxProtocol.connectionLost(self, reason) | ||
2754 | 2384 | self.transport = None | ||
2755 | 2385 | |||
2756 | 2386 | |||
2757 | 2387 | class _ParserHelper: | ||
2758 | 2388 | """ | ||
2759 | 2389 | A box receiver which records all boxes received. | ||
2760 | 2390 | """ | ||
2761 | 2391 | |||
2762 | 2392 | def __init__(self): | ||
2763 | 2393 | self.boxes = [] | ||
2764 | 2394 | |||
2765 | 2395 | def getPeer(self): | ||
2766 | 2396 | return 'string' | ||
2767 | 2397 | |||
2768 | 2398 | def getHost(self): | ||
2769 | 2399 | return 'string' | ||
2770 | 2400 | |||
2771 | 2401 | disconnecting = False | ||
2772 | 2402 | |||
2773 | 2403 | def startReceivingBoxes(self, sender): | ||
2774 | 2404 | """ | ||
2775 | 2405 | No initialization is required. | ||
2776 | 2406 | """ | ||
2777 | 2407 | |||
2778 | 2408 | def ampBoxReceived(self, box): | ||
2779 | 2409 | self.boxes.append(box) | ||
2780 | 2410 | |||
2781 | 2411 | # Synchronous helpers | ||
2782 | 2412 | @classmethod | ||
2783 | 2413 | def parse(cls, fileObj): | ||
2784 | 2414 | """ | ||
2785 | 2415 | Parse some amp data stored in a file. | ||
2786 | 2416 | |||
2787 | 2417 | @param fileObj: a file-like object. | ||
2788 | 2418 | |||
2789 | 2419 | @return: a list of AmpBoxes encoded in the given file. | ||
2790 | 2420 | """ | ||
2791 | 2421 | parserHelper = cls() | ||
2792 | 2422 | bbp = BinaryBoxProtocol(boxReceiver=parserHelper) | ||
2793 | 2423 | bbp.makeConnection(parserHelper) | ||
2794 | 2424 | bbp.dataReceived(fileObj.read()) | ||
2795 | 2425 | return parserHelper.boxes | ||
2796 | 2426 | |||
2797 | 2427 | @classmethod | ||
2798 | 2428 | def parseString(cls, data): | ||
2799 | 2429 | """ | ||
2800 | 2430 | Parse some amp data stored in a string. | ||
2801 | 2431 | |||
2802 | 2432 | @param data: a bytes holding some amp-encoded data. | ||
2803 | 2433 | |||
2804 | 2434 | @return: a list of AmpBoxes encoded in the given string. | ||
2805 | 2435 | """ | ||
2806 | 2436 | return cls.parse(BytesIO(data)) | ||
2807 | 2437 | |||
2808 | 2438 | |||
2809 | 2439 | parse = _ParserHelper.parse | ||
2810 | 2440 | parseString = _ParserHelper.parseString | ||
2811 | 2441 | |||
2812 | 2442 | |||
2813 | 2443 | def _stringsToObjects(strings, arglist, proto): | ||
2814 | 2444 | """ | ||
2815 | 2445 | Convert an AmpBox to a dictionary of python objects, converting through a | ||
2816 | 2446 | given arglist. | ||
2817 | 2447 | |||
2818 | 2448 | @param strings: an AmpBox (or dict of strings) | ||
2819 | 2449 | |||
2820 | 2450 | @param arglist: a list of 2-tuples of strings and Argument objects, as | ||
2821 | 2451 | described in L{Command.arguments}. | ||
2822 | 2452 | |||
2823 | 2453 | @param proto: an L{AMP} instance. | ||
2824 | 2454 | |||
2825 | 2455 | @return: the converted dictionary mapping names to argument objects. | ||
2826 | 2456 | """ | ||
2827 | 2457 | objects = {} | ||
2828 | 2458 | myStrings = strings.copy() | ||
2829 | 2459 | for argname, argparser in arglist: | ||
2830 | 2460 | argparser.fromBox(argname, myStrings, objects, proto) | ||
2831 | 2461 | return objects | ||
2832 | 2462 | |||
2833 | 2463 | |||
2834 | 2464 | def _objectsToStrings(objects, arglist, strings, proto): | ||
2835 | 2465 | """ | ||
2836 | 2466 | Convert a dictionary of python objects to an AmpBox, converting through a | ||
2837 | 2467 | given arglist. | ||
2838 | 2468 | |||
2839 | 2469 | @param objects: a dict mapping names to python objects | ||
2840 | 2470 | |||
2841 | 2471 | @param arglist: a list of 2-tuples of strings and Argument objects, as | ||
2842 | 2472 | described in L{Command.arguments}. | ||
2843 | 2473 | |||
2844 | 2474 | @param strings: [OUT PARAMETER] An object providing the L{dict} | ||
2845 | 2475 | interface which will be populated with serialized data. | ||
2846 | 2476 | |||
2847 | 2477 | @param proto: an L{AMP} instance. | ||
2848 | 2478 | |||
2849 | 2479 | @return: The converted dictionary mapping names to encoded argument | ||
2850 | 2480 | strings (identical to C{strings}). | ||
2851 | 2481 | """ | ||
2852 | 2482 | myObjects = objects.copy() | ||
2853 | 2483 | for argname, argparser in arglist: | ||
2854 | 2484 | argparser.toBox(argname, strings, myObjects, proto) | ||
2855 | 2485 | return strings | ||
2856 | 2486 | |||
2857 | 2487 | |||
2858 | 2488 | class _FixedOffsetTZInfo(datetime.tzinfo): | ||
2859 | 2489 | """ | ||
2860 | 2490 | Represents a fixed timezone offset (without daylight saving time). | ||
2861 | 2491 | |||
2862 | 2492 | @ivar name: A C{bytes} giving the name of this timezone; the name just | ||
2863 | 2493 | includes how much time this offset represents. | ||
2864 | 2494 | |||
2865 | 2495 | @ivar offset: A C{datetime.timedelta} giving the amount of time this | ||
2866 | 2496 | timezone is offset. | ||
2867 | 2497 | """ | ||
2868 | 2498 | |||
2869 | 2499 | def __init__(self, sign, hours, minutes): | ||
2870 | 2500 | self.name = '%s%02i:%02i' % (sign, hours, minutes) | ||
2871 | 2501 | if sign == '-': | ||
2872 | 2502 | hours = -hours | ||
2873 | 2503 | minutes = -minutes | ||
2874 | 2504 | elif sign != '+': | ||
2875 | 2505 | raise ValueError('invalid sign for timezone %r' % (sign,)) | ||
2876 | 2506 | self.offset = datetime.timedelta(hours=hours, minutes=minutes) | ||
2877 | 2507 | |||
2878 | 2508 | def utcoffset(self, dt): | ||
2879 | 2509 | """ | ||
2880 | 2510 | Return this timezone's offset from UTC. | ||
2881 | 2511 | """ | ||
2882 | 2512 | return self.offset | ||
2883 | 2513 | |||
2884 | 2514 | def dst(self, dt): | ||
2885 | 2515 | """ | ||
2886 | 2516 | Return a zero C{datetime.timedelta} for the daylight saving time | ||
2887 | 2517 | offset, since there is never one. | ||
2888 | 2518 | """ | ||
2889 | 2519 | return datetime.timedelta(0) | ||
2890 | 2520 | |||
2891 | 2521 | def tzname(self, dt): | ||
2892 | 2522 | """ | ||
2893 | 2523 | Return a string describing this timezone. | ||
2894 | 2524 | """ | ||
2895 | 2525 | return self.name | ||
2896 | 2526 | |||
2897 | 2527 | |||
2898 | 2528 | utc = _FixedOffsetTZInfo('+', 0, 0) | ||
2899 | 2529 | |||
2900 | 2530 | |||
2901 | 2531 | class Decimal(Argument): | ||
2902 | 2532 | """ | ||
2903 | 2533 | Encodes C{decimal.Decimal} instances. | ||
2904 | 2534 | |||
2905 | 2535 | There are several ways in which a decimal value might be encoded. | ||
2906 | 2536 | |||
2907 | 2537 | Special values are encoded as special strings:: | ||
2908 | 2538 | |||
2909 | 2539 | - Positive infinity is encoded as C{"Infinity"} | ||
2910 | 2540 | - Negative infinity is encoded as C{"-Infinity"} | ||
2911 | 2541 | - Quiet not-a-number is encoded as either C{"NaN"} or C{"-NaN"} | ||
2912 | 2542 | - Signalling not-a-number is encoded as either C{"sNaN"} or C{"-sNaN"} | ||
2913 | 2543 | |||
2914 | 2544 | Normal values are encoded using the base ten string representation, using | ||
2915 | 2545 | engineering notation to indicate magnitude without precision, and "normal" | ||
2916 | 2546 | digits to indicate precision. For example:: | ||
2917 | 2547 | |||
2918 | 2548 | - C{"1"} represents the value I{1} with precision to one place. | ||
2919 | 2549 | - C{"-1"} represents the value I{-1} with precision to one place. | ||
2920 | 2550 | - C{"1.0"} represents the value I{1} with precision to two places. | ||
2921 | 2551 | - C{"10"} represents the value I{10} with precision to two places. | ||
2922 | 2552 | - C{"1E+2"} represents the value I{10} with precision to one place. | ||
2923 | 2553 | - C{"1E-1"} represents the value I{0.1} with precision to one place. | ||
2924 | 2554 | - C{"1.5E+2"} represents the value I{15} with precision to two places. | ||
2925 | 2555 | |||
2926 | 2556 | U{http://speleotrove.com/decimal/} should be considered the authoritative | ||
2927 | 2557 | specification for the format. | ||
2928 | 2558 | """ | ||
2929 | 2559 | |||
2930 | 2560 | fromString = decimal.Decimal | ||
2931 | 2561 | |||
2932 | 2562 | def toString(self, inObject): | ||
2933 | 2563 | """ | ||
2934 | 2564 | Serialize a C{decimal.Decimal} instance to the specified wire format. | ||
2935 | 2565 | """ | ||
2936 | 2566 | if isinstance(inObject, decimal.Decimal): | ||
2937 | 2567 | # Hopefully decimal.Decimal.__bytes__ actually does what we want. | ||
2938 | 2568 | return bytes(inObject) | ||
2939 | 2569 | raise ValueError( | ||
2940 | 2570 | "amp32.Decimal can only encode instances of decimal.Decimal") | ||
2941 | 2571 | |||
2942 | 2572 | |||
2943 | 2573 | class DateTime(Argument): | ||
2944 | 2574 | """ | ||
2945 | 2575 | Encodes C{datetime.datetime} instances. | ||
2946 | 2576 | |||
2947 | 2577 | Wire format: '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i'. Fields in | ||
2948 | 2578 | order are: year, month, day, hour, minute, second, microsecond, timezone | ||
2949 | 2579 | direction (+ or -), timezone hour, timezone minute. Encoded string is | ||
2950 | 2580 | always exactly 32 characters long. This format is compatible with ISO 8601, | ||
2951 | 2581 | but that does not mean all ISO 8601 dates can be accepted. | ||
2952 | 2582 | |||
2953 | 2583 | Also, note that the datetime module's notion of a "timezone" can be | ||
2954 | 2584 | complex, but the wire format includes only a fixed offset, so the | ||
2955 | 2585 | conversion is not lossless. A lossless transmission of a C{datetime} | ||
2956 | 2586 | instance is not feasible since the receiving end would require a Python | ||
2957 | 2587 | interpreter. | ||
2958 | 2588 | |||
2959 | 2589 | @ivar _positions: A sequence of slices giving the positions of various | ||
2960 | 2590 | interesting parts of the wire format. | ||
2961 | 2591 | """ | ||
2962 | 2592 | |||
2963 | 2593 | _positions = [ | ||
2964 | 2594 | slice(0, 4), slice(5, 7), slice(8, 10), # year, month, day | ||
2965 | 2595 | slice(11, 13), slice(14, 16), slice(17, 19), # hour, minute, second | ||
2966 | 2596 | slice(20, 26), # microsecond | ||
2967 | 2597 | # intentionally skip timezone direction, as it is not an integer | ||
2968 | 2598 | slice(27, 29), slice(30, 32) # timezone hour, timezone minute | ||
2969 | 2599 | ] | ||
2970 | 2600 | |||
2971 | 2601 | def fromString(self, s): | ||
2972 | 2602 | """ | ||
2973 | 2603 | Parse a string containing a date and time in the wire format into a | ||
2974 | 2604 | C{datetime.datetime} instance. | ||
2975 | 2605 | """ | ||
2976 | 2606 | if len(s) != 32: | ||
2977 | 2607 | raise ValueError('invalid date format %r' % (s,)) | ||
2978 | 2608 | |||
2979 | 2609 | values = [int(s[p]) for p in self._positions] | ||
2980 | 2610 | sign = s[26] | ||
2981 | 2611 | timezone = _FixedOffsetTZInfo(sign, *values[7:]) | ||
2982 | 2612 | values[7:] = [timezone] | ||
2983 | 2613 | return datetime.datetime(*values) | ||
2984 | 2614 | |||
2985 | 2615 | def toString(self, i): | ||
2986 | 2616 | """ | ||
2987 | 2617 | Serialize a C{datetime.datetime} instance to a string in the specified | ||
2988 | 2618 | wire format. | ||
2989 | 2619 | """ | ||
2990 | 2620 | offset = i.utcoffset() | ||
2991 | 2621 | if offset is None: | ||
2992 | 2622 | raise ValueError( | ||
2993 | 2623 | 'amp32.DateTime cannot serialize naive datetime instances. ' | ||
2994 | 2624 | 'You may find amp32.utc useful.') | ||
2995 | 2625 | |||
2996 | 2626 | minutesOffset = (offset.days * 86400 + offset.seconds) // 60 | ||
2997 | 2627 | |||
2998 | 2628 | if minutesOffset > 0: | ||
2999 | 2629 | sign = '+' | ||
3000 | 2630 | else: | ||
3001 | 2631 | sign = '-' | ||
3002 | 2632 | |||
3003 | 2633 | # strftime has no way to format the microseconds, or put a ':' in the | ||
3004 | 2634 | # timezone. Suprise! | ||
3005 | 2635 | |||
3006 | 2636 | return '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i' % ( | ||
3007 | 2637 | i.year, | ||
3008 | 2638 | i.month, | ||
3009 | 2639 | i.day, | ||
3010 | 2640 | i.hour, | ||
3011 | 2641 | i.minute, | ||
3012 | 2642 | i.second, | ||
3013 | 2643 | i.microsecond, | ||
3014 | 2644 | sign, | ||
3015 | 2645 | abs(minutesOffset) // 60, | ||
3016 | 2646 | abs(minutesOffset) % 60) | ||
3017 | 0 | 2647 | ||
3018 | === modified file 'src/provisioningserver/rpc/arguments.py' | |||
3019 | --- src/provisioningserver/rpc/arguments.py 2014-10-30 11:22:41 +0000 | |||
3020 | +++ src/provisioningserver/rpc/arguments.py 2014-11-12 12:20:32 +0000 | |||
3021 | @@ -25,10 +25,10 @@ | |||
3022 | 25 | import zlib | 25 | import zlib |
3023 | 26 | 26 | ||
3024 | 27 | from apiclient.utils import ascii_url | 27 | from apiclient.utils import ascii_url |
3029 | 28 | from twisted.protocols import amp | 28 | from provisioningserver.rpc import amp32 |
3030 | 29 | 29 | ||
3031 | 30 | 30 | ||
3032 | 31 | class Bytes(amp.Argument): | 31 | class Bytes(amp32.Argument): |
3033 | 32 | """Encode a structure on the wire as bytes. | 32 | """Encode a structure on the wire as bytes. |
3034 | 33 | 33 | ||
3035 | 34 | In truth, this does nothing more than assert that the inputs are | 34 | In truth, this does nothing more than assert that the inputs are |
3036 | @@ -41,11 +41,11 @@ | |||
3037 | 41 | return inObject | 41 | return inObject |
3038 | 42 | 42 | ||
3039 | 43 | def fromString(self, inString): | 43 | def fromString(self, inString): |
3041 | 44 | # inString is always a byte string, as defined by amp.Argument. | 44 | # inString is always a byte string, as defined by amp32.Argument. |
3042 | 45 | return inString | 45 | return inString |
3043 | 46 | 46 | ||
3044 | 47 | 47 | ||
3046 | 48 | class Choice(amp.Argument): | 48 | class Choice(amp32.Argument): |
3047 | 49 | """Encode a choice to a predefined bytestring on the wire.""" | 49 | """Encode a choice to a predefined bytestring on the wire.""" |
3048 | 50 | 50 | ||
3049 | 51 | def __init__(self, choices, optional=False): | 51 | def __init__(self, choices, optional=False): |
3050 | @@ -76,7 +76,7 @@ | |||
3051 | 76 | return self._decode[inString] | 76 | return self._decode[inString] |
3052 | 77 | 77 | ||
3053 | 78 | 78 | ||
3055 | 79 | class ParsedURL(amp.Argument): | 79 | class ParsedURL(amp32.Argument): |
3056 | 80 | """Encode a URL on the wire. | 80 | """Encode a URL on the wire. |
3057 | 81 | 81 | ||
3058 | 82 | The URL should be an instance of :py:class:`~urlparse.ParseResult` | 82 | The URL should be an instance of :py:class:`~urlparse.ParseResult` |
3059 | @@ -105,11 +105,11 @@ | |||
3060 | 105 | return urlparse.urlparse(inString) | 105 | return urlparse.urlparse(inString) |
3061 | 106 | 106 | ||
3062 | 107 | 107 | ||
3064 | 108 | class StructureAsJSON(amp.Argument): | 108 | class StructureAsJSON(amp32.Argument): |
3065 | 109 | """Encode a structure on the wire as JSON, compressed with zlib. | 109 | """Encode a structure on the wire as JSON, compressed with zlib. |
3066 | 110 | 110 | ||
3067 | 111 | The compressed size of the structure should not exceed | 111 | The compressed size of the structure should not exceed |
3069 | 112 | :py:data:`~twisted.protocols.amp.MAX_VALUE_LENGTH`, or ``0xffff`` | 112 | :py:data:`~provisioningserver.rpc.amp32.MAX_VALUE_LENGTH`, or ``0xffff`` |
3070 | 113 | bytes. This is pretty hard to be sure of ahead of time, so only use | 113 | bytes. This is pretty hard to be sure of ahead of time, so only use |
3071 | 114 | this for small structures that won't go near the limit. | 114 | this for small structures that won't go near the limit. |
3072 | 115 | """ | 115 | """ |
3073 | @@ -121,8 +121,8 @@ | |||
3074 | 121 | return json.loads(zlib.decompress(inString)) | 121 | return json.loads(zlib.decompress(inString)) |
3075 | 122 | 122 | ||
3076 | 123 | 123 | ||
3079 | 124 | class CompressedAmpList(amp.AmpList): | 124 | class CompressedAmpList(amp32.AmpList): |
3080 | 125 | """An :py:class:`amp.AmpList` that's compressed on the wire. | 125 | """An :py:class:`amp32.AmpList` that's compressed on the wire. |
3081 | 126 | 126 | ||
3082 | 127 | The serialised form is transparently compressed and decompressed with | 127 | The serialised form is transparently compressed and decompressed with |
3083 | 128 | zlib. This can be useful when there's a lot of repetition in the list | 128 | zlib. This can be useful when there's a lot of repetition in the list |
3084 | 129 | 129 | ||
3085 | === modified file 'src/provisioningserver/rpc/cluster.py' | |||
3086 | --- src/provisioningserver/rpc/cluster.py 2014-10-23 18:21:41 +0000 | |||
3087 | +++ src/provisioningserver/rpc/cluster.py 2014-11-12 12:20:32 +0000 | |||
3088 | @@ -37,7 +37,10 @@ | |||
3089 | 37 | PowerActionFail, | 37 | PowerActionFail, |
3090 | 38 | UnknownPowerType, | 38 | UnknownPowerType, |
3091 | 39 | ) | 39 | ) |
3093 | 40 | from provisioningserver.rpc import exceptions | 40 | from provisioningserver.rpc import ( |
3094 | 41 | amp32, | ||
3095 | 42 | exceptions, | ||
3096 | 43 | ) | ||
3097 | 41 | from provisioningserver.rpc.arguments import ( | 44 | from provisioningserver.rpc.arguments import ( |
3098 | 42 | Bytes, | 45 | Bytes, |
3099 | 43 | ParsedURL, | 46 | ParsedURL, |
3100 | @@ -47,10 +50,9 @@ | |||
3101 | 47 | Authenticate, | 50 | Authenticate, |
3102 | 48 | Identify, | 51 | Identify, |
3103 | 49 | ) | 52 | ) |
3108 | 50 | from twisted.protocols import amp | 53 | |
3109 | 51 | 54 | ||
3110 | 52 | 55 | class ListBootImages(amp32.Command): | |
3107 | 53 | class ListBootImages(amp.Command): | ||
3111 | 54 | """List the boot images available on this cluster controller. | 56 | """List the boot images available on this cluster controller. |
3112 | 55 | 57 | ||
3113 | 56 | :since: 1.5 | 58 | :since: 1.5 |
3114 | @@ -58,20 +60,20 @@ | |||
3115 | 58 | 60 | ||
3116 | 59 | arguments = [] | 61 | arguments = [] |
3117 | 60 | response = [ | 62 | response = [ |
3127 | 61 | (b"images", amp.AmpList( | 63 | (b"images", amp32.AmpList( |
3128 | 62 | [(b"osystem", amp.Unicode()), | 64 | [(b"osystem", amp32.Unicode()), |
3129 | 63 | (b"architecture", amp.Unicode()), | 65 | (b"architecture", amp32.Unicode()), |
3130 | 64 | (b"subarchitecture", amp.Unicode()), | 66 | (b"subarchitecture", amp32.Unicode()), |
3131 | 65 | (b"release", amp.Unicode()), | 67 | (b"release", amp32.Unicode()), |
3132 | 66 | (b"label", amp.Unicode()), | 68 | (b"label", amp32.Unicode()), |
3133 | 67 | (b"purpose", amp.Unicode()), | 69 | (b"purpose", amp32.Unicode()), |
3134 | 68 | (b"xinstall_type", amp.Unicode()), | 70 | (b"xinstall_type", amp32.Unicode()), |
3135 | 69 | (b"xinstall_path", amp.Unicode())])) | 71 | (b"xinstall_path", amp32.Unicode())])) |
3136 | 70 | ] | 72 | ] |
3137 | 71 | errors = [] | 73 | errors = [] |
3138 | 72 | 74 | ||
3139 | 73 | 75 | ||
3141 | 74 | class DescribePowerTypes(amp.Command): | 76 | class DescribePowerTypes(amp32.Command): |
3142 | 75 | """Get a JSON Schema describing this cluster's power types. | 77 | """Get a JSON Schema describing this cluster's power types. |
3143 | 76 | 78 | ||
3144 | 77 | :since: 1.5 | 79 | :since: 1.5 |
3145 | @@ -84,7 +86,7 @@ | |||
3146 | 84 | errors = [] | 86 | errors = [] |
3147 | 85 | 87 | ||
3148 | 86 | 88 | ||
3150 | 87 | class ListSupportedArchitectures(amp.Command): | 89 | class ListSupportedArchitectures(amp32.Command): |
3151 | 88 | """Report the cluster's supported architectures. | 90 | """Report the cluster's supported architectures. |
3152 | 89 | 91 | ||
3153 | 90 | :since: 1.5 | 92 | :since: 1.5 |
3154 | @@ -92,15 +94,15 @@ | |||
3155 | 92 | 94 | ||
3156 | 93 | arguments = [] | 95 | arguments = [] |
3157 | 94 | response = [ | 96 | response = [ |
3161 | 95 | (b"architectures", amp.AmpList([ | 97 | (b"architectures", amp32.AmpList([ |
3162 | 96 | (b"name", amp.Unicode()), | 98 | (b"name", amp32.Unicode()), |
3163 | 97 | (b"description", amp.Unicode()), | 99 | (b"description", amp32.Unicode()), |
3164 | 98 | ])), | 100 | ])), |
3165 | 99 | ] | 101 | ] |
3166 | 100 | errors = [] | 102 | errors = [] |
3167 | 101 | 103 | ||
3168 | 102 | 104 | ||
3170 | 103 | class ListOperatingSystems(amp.Command): | 105 | class ListOperatingSystems(amp32.Command): |
3171 | 104 | """Report the cluster's supported operating systems. | 106 | """Report the cluster's supported operating systems. |
3172 | 105 | 107 | ||
3173 | 106 | :since: 1.7 | 108 | :since: 1.7 |
3174 | @@ -108,34 +110,34 @@ | |||
3175 | 108 | 110 | ||
3176 | 109 | arguments = [] | 111 | arguments = [] |
3177 | 110 | response = [ | 112 | response = [ |
3186 | 111 | (b"osystems", amp.AmpList([ | 113 | (b"osystems", amp32.AmpList([ |
3187 | 112 | (b"name", amp.Unicode()), | 114 | (b"name", amp32.Unicode()), |
3188 | 113 | (b"title", amp.Unicode()), | 115 | (b"title", amp32.Unicode()), |
3189 | 114 | (b"releases", amp.AmpList([ | 116 | (b"releases", amp32.AmpList([ |
3190 | 115 | (b"name", amp.Unicode()), | 117 | (b"name", amp32.Unicode()), |
3191 | 116 | (b"title", amp.Unicode()), | 118 | (b"title", amp32.Unicode()), |
3192 | 117 | (b"requires_license_key", amp.Boolean()), | 119 | (b"requires_license_key", amp32.Boolean()), |
3193 | 118 | (b"can_commission", amp.Boolean()), | 120 | (b"can_commission", amp32.Boolean()), |
3194 | 119 | ])), | 121 | ])), |
3197 | 120 | (b"default_release", amp.Unicode(optional=True)), | 122 | (b"default_release", amp32.Unicode(optional=True)), |
3198 | 121 | (b"default_commissioning_release", amp.Unicode(optional=True)), | 123 | (b"default_commissioning_release", amp32.Unicode(optional=True)), |
3199 | 122 | ])), | 124 | ])), |
3200 | 123 | ] | 125 | ] |
3201 | 124 | errors = [] | 126 | errors = [] |
3202 | 125 | 127 | ||
3203 | 126 | 128 | ||
3205 | 127 | class GetOSReleaseTitle(amp.Command): | 129 | class GetOSReleaseTitle(amp32.Command): |
3206 | 128 | """Get the title for the operating systems release. | 130 | """Get the title for the operating systems release. |
3207 | 129 | 131 | ||
3208 | 130 | :since: 1.7 | 132 | :since: 1.7 |
3209 | 131 | """ | 133 | """ |
3210 | 132 | 134 | ||
3211 | 133 | arguments = [ | 135 | arguments = [ |
3214 | 134 | (b"osystem", amp.Unicode()), | 136 | (b"osystem", amp32.Unicode()), |
3215 | 135 | (b"release", amp.Unicode()), | 137 | (b"release", amp32.Unicode()), |
3216 | 136 | ] | 138 | ] |
3217 | 137 | response = [ | 139 | response = [ |
3219 | 138 | (b"title", amp.Unicode()), | 140 | (b"title", amp32.Unicode()), |
3220 | 139 | ] | 141 | ] |
3221 | 140 | errors = { | 142 | errors = { |
3222 | 141 | exceptions.NoSuchOperatingSystem: ( | 143 | exceptions.NoSuchOperatingSystem: ( |
3223 | @@ -143,19 +145,19 @@ | |||
3224 | 143 | } | 145 | } |
3225 | 144 | 146 | ||
3226 | 145 | 147 | ||
3228 | 146 | class ValidateLicenseKey(amp.Command): | 148 | class ValidateLicenseKey(amp32.Command): |
3229 | 147 | """Validate an OS license key. | 149 | """Validate an OS license key. |
3230 | 148 | 150 | ||
3231 | 149 | :since: 1.7 | 151 | :since: 1.7 |
3232 | 150 | """ | 152 | """ |
3233 | 151 | 153 | ||
3234 | 152 | arguments = [ | 154 | arguments = [ |
3238 | 153 | (b"osystem", amp.Unicode()), | 155 | (b"osystem", amp32.Unicode()), |
3239 | 154 | (b"release", amp.Unicode()), | 156 | (b"release", amp32.Unicode()), |
3240 | 155 | (b"key", amp.Unicode()), | 157 | (b"key", amp32.Unicode()), |
3241 | 156 | ] | 158 | ] |
3242 | 157 | response = [ | 159 | response = [ |
3244 | 158 | (b"is_valid", amp.Boolean()), | 160 | (b"is_valid", amp32.Boolean()), |
3245 | 159 | ] | 161 | ] |
3246 | 160 | errors = { | 162 | errors = { |
3247 | 161 | exceptions.NoSuchOperatingSystem: ( | 163 | exceptions.NoSuchOperatingSystem: ( |
3248 | @@ -163,20 +165,20 @@ | |||
3249 | 163 | } | 165 | } |
3250 | 164 | 166 | ||
3251 | 165 | 167 | ||
3253 | 166 | class GetPreseedData(amp.Command): | 168 | class GetPreseedData(amp32.Command): |
3254 | 167 | """Get OS-specific preseed data. | 169 | """Get OS-specific preseed data. |
3255 | 168 | 170 | ||
3256 | 169 | :since: 1.7 | 171 | :since: 1.7 |
3257 | 170 | """ | 172 | """ |
3258 | 171 | 173 | ||
3259 | 172 | arguments = [ | 174 | arguments = [ |
3267 | 173 | (b"osystem", amp.Unicode()), | 175 | (b"osystem", amp32.Unicode()), |
3268 | 174 | (b"preseed_type", amp.Unicode()), | 176 | (b"preseed_type", amp32.Unicode()), |
3269 | 175 | (b"node_system_id", amp.Unicode()), | 177 | (b"node_system_id", amp32.Unicode()), |
3270 | 176 | (b"node_hostname", amp.Unicode()), | 178 | (b"node_hostname", amp32.Unicode()), |
3271 | 177 | (b"consumer_key", amp.Unicode()), | 179 | (b"consumer_key", amp32.Unicode()), |
3272 | 178 | (b"token_key", amp.Unicode()), | 180 | (b"token_key", amp32.Unicode()), |
3273 | 179 | (b"token_secret", amp.Unicode()), | 181 | (b"token_secret", amp32.Unicode()), |
3274 | 180 | (b"metadata_url", ParsedURL()), | 182 | (b"metadata_url", ParsedURL()), |
3275 | 181 | ] | 183 | ] |
3276 | 182 | response = [ | 184 | response = [ |
3277 | @@ -190,16 +192,16 @@ | |||
3278 | 190 | } | 192 | } |
3279 | 191 | 193 | ||
3280 | 192 | 194 | ||
3282 | 193 | class ComposeCurtinNetworkPreseed(amp.Command): | 195 | class ComposeCurtinNetworkPreseed(amp32.Command): |
3283 | 194 | """Generate Curtin network preseed for a node. | 196 | """Generate Curtin network preseed for a node. |
3284 | 195 | 197 | ||
3285 | 196 | :since: 1.7 | 198 | :since: 1.7 |
3286 | 197 | """ | 199 | """ |
3287 | 198 | 200 | ||
3288 | 199 | arguments = [ | 201 | arguments = [ |
3290 | 200 | (b"osystem", amp.Unicode()), | 202 | (b"osystem", amp32.Unicode()), |
3291 | 201 | (b"config", StructureAsJSON()), | 203 | (b"config", StructureAsJSON()), |
3293 | 202 | (b"disable_ipv4", amp.Boolean()), | 204 | (b"disable_ipv4", amp32.Boolean()), |
3294 | 203 | ] | 205 | ] |
3295 | 204 | response = [ | 206 | response = [ |
3296 | 205 | (b"data", StructureAsJSON()), | 207 | (b"data", StructureAsJSON()), |
3297 | @@ -209,16 +211,16 @@ | |||
3298 | 209 | } | 211 | } |
3299 | 210 | 212 | ||
3300 | 211 | 213 | ||
3302 | 212 | class _Power(amp.Command): | 214 | class _Power(amp32.Command): |
3303 | 213 | """Base class for power control commands. | 215 | """Base class for power control commands. |
3304 | 214 | 216 | ||
3305 | 215 | :since: 1.7 | 217 | :since: 1.7 |
3306 | 216 | """ | 218 | """ |
3307 | 217 | 219 | ||
3308 | 218 | arguments = [ | 220 | arguments = [ |
3312 | 219 | (b"system_id", amp.Unicode()), | 221 | (b"system_id", amp32.Unicode()), |
3313 | 220 | (b"hostname", amp.Unicode()), | 222 | (b"hostname", amp32.Unicode()), |
3314 | 221 | (b"power_type", amp.Unicode()), | 223 | (b"power_type", amp32.Unicode()), |
3315 | 222 | # We can't define a tighter schema here because this is a highly | 224 | # We can't define a tighter schema here because this is a highly |
3316 | 223 | # variable bag of arguments from a variety of sources. | 225 | # variable bag of arguments from a variety of sources. |
3317 | 224 | (b"context", StructureAsJSON()), | 226 | (b"context", StructureAsJSON()), |
3318 | @@ -256,29 +258,29 @@ | |||
3319 | 256 | :since: 1.7 | 258 | :since: 1.7 |
3320 | 257 | """ | 259 | """ |
3321 | 258 | response = [ | 260 | response = [ |
3323 | 259 | (b"state", amp.Unicode()), | 261 | (b"state", amp32.Unicode()), |
3324 | 260 | ] | 262 | ] |
3325 | 261 | 263 | ||
3326 | 262 | 264 | ||
3328 | 263 | class _ConfigureDHCP(amp.Command): | 265 | class _ConfigureDHCP(amp32.Command): |
3329 | 264 | """Configure a DHCP server. | 266 | """Configure a DHCP server. |
3330 | 265 | 267 | ||
3331 | 266 | :since: 1.7 | 268 | :since: 1.7 |
3332 | 267 | """ | 269 | """ |
3333 | 268 | arguments = [ | 270 | arguments = [ |
3347 | 269 | (b"omapi_key", amp.Unicode()), | 271 | (b"omapi_key", amp32.Unicode()), |
3348 | 270 | (b"subnet_configs", amp.AmpList([ | 272 | (b"subnet_configs", amp32.AmpList([ |
3349 | 271 | (b"subnet", amp.Unicode()), | 273 | (b"subnet", amp32.Unicode()), |
3350 | 272 | (b"subnet_mask", amp.Unicode()), | 274 | (b"subnet_mask", amp32.Unicode()), |
3351 | 273 | (b"subnet_cidr", amp.Unicode()), | 275 | (b"subnet_cidr", amp32.Unicode()), |
3352 | 274 | (b"broadcast_ip", amp.Unicode()), | 276 | (b"broadcast_ip", amp32.Unicode()), |
3353 | 275 | (b"interface", amp.Unicode()), | 277 | (b"interface", amp32.Unicode()), |
3354 | 276 | (b"router_ip", amp.Unicode()), | 278 | (b"router_ip", amp32.Unicode()), |
3355 | 277 | (b"dns_servers", amp.Unicode()), | 279 | (b"dns_servers", amp32.Unicode()), |
3356 | 278 | (b"ntp_server", amp.Unicode()), | 280 | (b"ntp_server", amp32.Unicode()), |
3357 | 279 | (b"domain_name", amp.Unicode()), | 281 | (b"domain_name", amp32.Unicode()), |
3358 | 280 | (b"ip_range_low", amp.Unicode()), | 282 | (b"ip_range_low", amp32.Unicode()), |
3359 | 281 | (b"ip_range_high", amp.Unicode()), | 283 | (b"ip_range_high", amp32.Unicode()), |
3360 | 282 | ])), | 284 | ])), |
3361 | 283 | ] | 285 | ] |
3362 | 284 | response = [] | 286 | response = [] |
3363 | @@ -299,18 +301,18 @@ | |||
3364 | 299 | """ | 301 | """ |
3365 | 300 | 302 | ||
3366 | 301 | 303 | ||
3368 | 302 | class CreateHostMaps(amp.Command): | 304 | class CreateHostMaps(amp32.Command): |
3369 | 303 | """Create host maps in the DHCP server's configuration. | 305 | """Create host maps in the DHCP server's configuration. |
3370 | 304 | 306 | ||
3371 | 305 | :since: 1.7 | 307 | :since: 1.7 |
3372 | 306 | """ | 308 | """ |
3373 | 307 | 309 | ||
3374 | 308 | arguments = [ | 310 | arguments = [ |
3378 | 309 | (b"mappings", amp.AmpList([ | 311 | (b"mappings", amp32.AmpList([ |
3379 | 310 | (b"ip_address", amp.Unicode()), | 312 | (b"ip_address", amp32.Unicode()), |
3380 | 311 | (b"mac_address", amp.Unicode()), | 313 | (b"mac_address", amp32.Unicode()), |
3381 | 312 | ])), | 314 | ])), |
3383 | 313 | (b"shared_key", amp.Unicode()), | 315 | (b"shared_key", amp32.Unicode()), |
3384 | 314 | ] | 316 | ] |
3385 | 315 | response = [] | 317 | response = [] |
3386 | 316 | errors = { | 318 | errors = { |
3387 | @@ -319,15 +321,15 @@ | |||
3388 | 319 | } | 321 | } |
3389 | 320 | 322 | ||
3390 | 321 | 323 | ||
3392 | 322 | class RemoveHostMaps(amp.Command): | 324 | class RemoveHostMaps(amp32.Command): |
3393 | 323 | """Remove host maps from the DHCP server's configuration. | 325 | """Remove host maps from the DHCP server's configuration. |
3394 | 324 | 326 | ||
3395 | 325 | :since: 1.7 | 327 | :since: 1.7 |
3396 | 326 | """ | 328 | """ |
3397 | 327 | 329 | ||
3398 | 328 | arguments = [ | 330 | arguments = [ |
3401 | 329 | (b"ip_addresses", amp.ListOf(amp.Unicode())), | 331 | (b"ip_addresses", amp32.ListOf(amp32.Unicode())), |
3402 | 330 | (b"shared_key", amp.Unicode()), | 332 | (b"shared_key", amp32.Unicode()), |
3403 | 331 | ] | 333 | ] |
3404 | 332 | response = [] | 334 | response = [] |
3405 | 333 | errors = { | 335 | errors = { |
3406 | @@ -336,7 +338,7 @@ | |||
3407 | 336 | } | 338 | } |
3408 | 337 | 339 | ||
3409 | 338 | 340 | ||
3411 | 339 | class ImportBootImages(amp.Command): | 341 | class ImportBootImages(amp32.Command): |
3412 | 340 | """Import boot images and report the final | 342 | """Import boot images and report the final |
3413 | 341 | boot images that exist on the cluster. | 343 | boot images that exist on the cluster. |
3414 | 342 | 344 | ||
3415 | @@ -344,15 +346,15 @@ | |||
3416 | 344 | """ | 346 | """ |
3417 | 345 | 347 | ||
3418 | 346 | arguments = [ | 348 | arguments = [ |
3421 | 347 | (b"sources", amp.AmpList( | 349 | (b"sources", amp32.AmpList( |
3422 | 348 | [(b"url", amp.Unicode()), | 350 | [(b"url", amp32.Unicode()), |
3423 | 349 | (b"keyring_data", Bytes()), | 351 | (b"keyring_data", Bytes()), |
3430 | 350 | (b"selections", amp.AmpList( | 352 | (b"selections", amp32.AmpList( |
3431 | 351 | [(b"os", amp.Unicode()), | 353 | [(b"os", amp32.Unicode()), |
3432 | 352 | (b"release", amp.Unicode()), | 354 | (b"release", amp32.Unicode()), |
3433 | 353 | (b"arches", amp.ListOf(amp.Unicode())), | 355 | (b"arches", amp32.ListOf(amp32.Unicode())), |
3434 | 354 | (b"subarches", amp.ListOf(amp.Unicode())), | 356 | (b"subarches", amp32.ListOf(amp32.Unicode())), |
3435 | 355 | (b"labels", amp.ListOf(amp.Unicode()))]))])), | 357 | (b"labels", amp32.ListOf(amp32.Unicode()))]))])), |
3436 | 356 | (b"http_proxy", ParsedURL(optional=True)), | 358 | (b"http_proxy", ParsedURL(optional=True)), |
3437 | 357 | (b"https_proxy", ParsedURL(optional=True)), | 359 | (b"https_proxy", ParsedURL(optional=True)), |
3438 | 358 | ] | 360 | ] |
3439 | @@ -360,80 +362,80 @@ | |||
3440 | 360 | errors = [] | 362 | errors = [] |
3441 | 361 | 363 | ||
3442 | 362 | 364 | ||
3444 | 363 | class StartMonitors(amp.Command): | 365 | class StartMonitors(amp32.Command): |
3445 | 364 | """Starts monitors(s) on the cluster. | 366 | """Starts monitors(s) on the cluster. |
3446 | 365 | 367 | ||
3447 | 366 | :since: 1.7 | 368 | :since: 1.7 |
3448 | 367 | """ | 369 | """ |
3449 | 368 | 370 | ||
3450 | 369 | arguments = [ | 371 | arguments = [ |
3453 | 370 | (b"monitors", amp.AmpList( | 372 | (b"monitors", amp32.AmpList( |
3454 | 371 | [(b"deadline", amp.DateTime()), | 373 | [(b"deadline", amp32.DateTime()), |
3455 | 372 | (b"context", StructureAsJSON()), | 374 | (b"context", StructureAsJSON()), |
3457 | 373 | (b"id", amp.Unicode()), | 375 | (b"id", amp32.Unicode()), |
3458 | 374 | ])) | 376 | ])) |
3459 | 375 | ] | 377 | ] |
3460 | 376 | response = [] | 378 | response = [] |
3461 | 377 | errors = [] | 379 | errors = [] |
3462 | 378 | 380 | ||
3463 | 379 | 381 | ||
3465 | 380 | class CancelMonitor(amp.Command): | 382 | class CancelMonitor(amp32.Command): |
3466 | 381 | """Cancels an existing monitor on the cluster. | 383 | """Cancels an existing monitor on the cluster. |
3467 | 382 | 384 | ||
3468 | 383 | :since: 1.7 | 385 | :since: 1.7 |
3469 | 384 | """ | 386 | """ |
3470 | 385 | 387 | ||
3471 | 386 | arguments = [ | 388 | arguments = [ |
3473 | 387 | (b"id", amp.Unicode()), | 389 | (b"id", amp32.Unicode()), |
3474 | 388 | ] | 390 | ] |
3475 | 389 | response = [] | 391 | response = [] |
3476 | 390 | error = [] | 392 | error = [] |
3477 | 391 | 393 | ||
3478 | 392 | 394 | ||
3480 | 393 | class EvaluateTag(amp.Command): | 395 | class EvaluateTag(amp32.Command): |
3481 | 394 | """Evaluate a tag against all of the cluster's nodes. | 396 | """Evaluate a tag against all of the cluster's nodes. |
3482 | 395 | 397 | ||
3483 | 396 | :since: 1.7 | 398 | :since: 1.7 |
3484 | 397 | """ | 399 | """ |
3485 | 398 | 400 | ||
3486 | 399 | arguments = [ | 401 | arguments = [ |
3492 | 400 | (b"tag_name", amp.Unicode()), | 402 | (b"tag_name", amp32.Unicode()), |
3493 | 401 | (b"tag_definition", amp.Unicode()), | 403 | (b"tag_definition", amp32.Unicode()), |
3494 | 402 | (b"tag_nsmap", amp.AmpList([ | 404 | (b"tag_nsmap", amp32.AmpList([ |
3495 | 403 | (b"prefix", amp.Unicode()), | 405 | (b"prefix", amp32.Unicode()), |
3496 | 404 | (b"uri", amp.Unicode()), | 406 | (b"uri", amp32.Unicode()), |
3497 | 405 | ])), | 407 | ])), |
3498 | 406 | # A 3-part credential string for the web API. | 408 | # A 3-part credential string for the web API. |
3500 | 407 | (b"credentials", amp.Unicode()), | 409 | (b"credentials", amp32.Unicode()), |
3501 | 408 | ] | 410 | ] |
3502 | 409 | response = [] | 411 | response = [] |
3503 | 410 | errors = [] | 412 | errors = [] |
3504 | 411 | 413 | ||
3505 | 412 | 414 | ||
3507 | 413 | class AddVirsh(amp.Command): | 415 | class AddVirsh(amp32.Command): |
3508 | 414 | """Probe for and enlist virsh VMs attached to the cluster. | 416 | """Probe for and enlist virsh VMs attached to the cluster. |
3509 | 415 | 417 | ||
3510 | 416 | :since: 1.7 | 418 | :since: 1.7 |
3511 | 417 | """ | 419 | """ |
3512 | 418 | 420 | ||
3513 | 419 | arguments = [ | 421 | arguments = [ |
3516 | 420 | (b"poweraddr", amp.Unicode()), | 422 | (b"poweraddr", amp32.Unicode()), |
3517 | 421 | (b"password", amp.Unicode(optional=True)), | 423 | (b"password", amp32.Unicode(optional=True)), |
3518 | 422 | ] | 424 | ] |
3519 | 423 | response = [] | 425 | response = [] |
3520 | 424 | errors = [] | 426 | errors = [] |
3521 | 425 | 427 | ||
3522 | 426 | 428 | ||
3524 | 427 | class AddSeaMicro15k(amp.Command): | 429 | class AddSeaMicro15k(amp32.Command): |
3525 | 428 | """Probe for and enlist seamicro15k machines attached to the cluster. | 430 | """Probe for and enlist seamicro15k machines attached to the cluster. |
3526 | 429 | 431 | ||
3527 | 430 | :since: 1.7 | 432 | :since: 1.7 |
3528 | 431 | """ | 433 | """ |
3529 | 432 | arguments = [ | 434 | arguments = [ |
3534 | 433 | (b"mac", amp.Unicode()), | 435 | (b"mac", amp32.Unicode()), |
3535 | 434 | (b"username", amp.Unicode()), | 436 | (b"username", amp32.Unicode()), |
3536 | 435 | (b"password", amp.Unicode()), | 437 | (b"password", amp32.Unicode()), |
3537 | 436 | (b"power_control", amp.Unicode(optional=True)), | 438 | (b"power_control", amp32.Unicode(optional=True)), |
3538 | 437 | ] | 439 | ] |
3539 | 438 | response = [] | 440 | response = [] |
3540 | 439 | errors = { | 441 | errors = { |
3541 | @@ -441,41 +443,41 @@ | |||
3542 | 441 | } | 443 | } |
3543 | 442 | 444 | ||
3544 | 443 | 445 | ||
3546 | 444 | class EnlistNodesFromMSCM(amp.Command): | 446 | class EnlistNodesFromMSCM(amp32.Command): |
3547 | 445 | """Probe for and enlist mscm machines attached to the cluster. | 447 | """Probe for and enlist mscm machines attached to the cluster. |
3548 | 446 | 448 | ||
3549 | 447 | :since: 1.7 | 449 | :since: 1.7 |
3550 | 448 | """ | 450 | """ |
3551 | 449 | arguments = [ | 451 | arguments = [ |
3555 | 450 | (b"host", amp.Unicode()), | 452 | (b"host", amp32.Unicode()), |
3556 | 451 | (b"username", amp.Unicode()), | 453 | (b"username", amp32.Unicode()), |
3557 | 452 | (b"password", amp.Unicode()), | 454 | (b"password", amp32.Unicode()), |
3558 | 453 | ] | 455 | ] |
3559 | 454 | response = [] | 456 | response = [] |
3560 | 455 | errors = {} | 457 | errors = {} |
3561 | 456 | 458 | ||
3562 | 457 | 459 | ||
3564 | 458 | class EnlistNodesFromUCSM(amp.Command): | 460 | class EnlistNodesFromUCSM(amp32.Command): |
3565 | 459 | """Probe for and enlist ucsm machines attached to the cluster. | 461 | """Probe for and enlist ucsm machines attached to the cluster. |
3566 | 460 | 462 | ||
3567 | 461 | :since: 1.7 | 463 | :since: 1.7 |
3568 | 462 | """ | 464 | """ |
3569 | 463 | arguments = [ | 465 | arguments = [ |
3573 | 464 | (b"url", amp.Unicode()), | 466 | (b"url", amp32.Unicode()), |
3574 | 465 | (b"username", amp.Unicode()), | 467 | (b"username", amp32.Unicode()), |
3575 | 466 | (b"password", amp.Unicode()), | 468 | (b"password", amp32.Unicode()), |
3576 | 467 | ] | 469 | ] |
3577 | 468 | response = [] | 470 | response = [] |
3578 | 469 | errors = {} | 471 | errors = {} |
3579 | 470 | 472 | ||
3580 | 471 | 473 | ||
3582 | 472 | class IsImportBootImagesRunning(amp.Command): | 474 | class IsImportBootImagesRunning(amp32.Command): |
3583 | 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. |
3584 | 474 | 476 | ||
3585 | 475 | :since: 1.7 | 477 | :since: 1.7 |
3586 | 476 | """ | 478 | """ |
3587 | 477 | arguments = [] | 479 | arguments = [] |
3588 | 478 | response = [ | 480 | response = [ |
3590 | 479 | (b"running", amp.Boolean()), | 481 | (b"running", amp32.Boolean()), |
3591 | 480 | ] | 482 | ] |
3592 | 481 | errors = {} | 483 | errors = {} |
3593 | 482 | 484 | ||
3594 | === modified file 'src/provisioningserver/rpc/clusterservice.py' | |||
3595 | --- src/provisioningserver/rpc/clusterservice.py 2014-11-10 15:11:58 +0000 | |||
3596 | +++ src/provisioningserver/rpc/clusterservice.py 2014-11-12 12:20:32 +0000 | |||
3597 | @@ -44,6 +44,7 @@ | |||
3598 | 44 | from provisioningserver.logger.utils import log_call | 44 | from provisioningserver.logger.utils import log_call |
3599 | 45 | from provisioningserver.network import discover_networks | 45 | from provisioningserver.network import discover_networks |
3600 | 46 | from provisioningserver.rpc import ( | 46 | from provisioningserver.rpc import ( |
3601 | 47 | amp32, | ||
3602 | 47 | cluster, | 48 | cluster, |
3603 | 48 | common, | 49 | common, |
3604 | 49 | dhcp, | 50 | dhcp, |
3605 | @@ -97,7 +98,6 @@ | |||
3606 | 97 | ConnectionClosed, | 98 | ConnectionClosed, |
3607 | 98 | ) | 99 | ) |
3608 | 99 | from twisted.internet.threads import deferToThread | 100 | from twisted.internet.threads import deferToThread |
3609 | 100 | from twisted.protocols import amp | ||
3610 | 101 | from twisted.python import log | 101 | from twisted.python import log |
3611 | 102 | from twisted.web import http | 102 | from twisted.web import http |
3612 | 103 | import twisted.web.client | 103 | import twisted.web.client |
3613 | @@ -312,11 +312,11 @@ | |||
3614 | 312 | cancel_monitor(id) | 312 | cancel_monitor(id) |
3615 | 313 | return {} | 313 | return {} |
3616 | 314 | 314 | ||
3618 | 315 | @amp.StartTLS.responder | 315 | @amp32.StartTLS.responder |
3619 | 316 | def get_tls_parameters(self): | 316 | def get_tls_parameters(self): |
3620 | 317 | """get_tls_parameters() | 317 | """get_tls_parameters() |
3621 | 318 | 318 | ||
3623 | 319 | Implementation of :py:class:`~twisted.protocols.amp.StartTLS`. | 319 | Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`. |
3624 | 320 | """ | 320 | """ |
3625 | 321 | try: | 321 | try: |
3626 | 322 | from provisioningserver.rpc.testing import tls | 322 | from provisioningserver.rpc.testing import tls |
3627 | @@ -551,7 +551,7 @@ | |||
3628 | 551 | 551 | ||
3629 | 552 | @inlineCallbacks | 552 | @inlineCallbacks |
3630 | 553 | def secureConnection(self): | 553 | def secureConnection(self): |
3632 | 554 | yield self.callRemote(amp.StartTLS, **self.get_tls_parameters()) | 554 | yield self.callRemote(amp32.StartTLS, **self.get_tls_parameters()) |
3633 | 555 | 555 | ||
3634 | 556 | # For some weird reason (it's mentioned in Twisted's source), | 556 | # For some weird reason (it's mentioned in Twisted's source), |
3635 | 557 | # TLS negotiation does not complete until we do something with | 557 | # TLS negotiation does not complete until we do something with |
3636 | 558 | 558 | ||
3637 | === modified file 'src/provisioningserver/rpc/common.py' | |||
3638 | --- src/provisioningserver/rpc/common.py 2014-10-03 12:52:35 +0000 | |||
3639 | +++ src/provisioningserver/rpc/common.py 2014-11-12 12:20:32 +0000 | |||
3640 | @@ -19,22 +19,22 @@ | |||
3641 | 19 | "RPCProtocol", | 19 | "RPCProtocol", |
3642 | 20 | ] | 20 | ] |
3643 | 21 | 21 | ||
3644 | 22 | from provisioningserver.rpc import amp32 | ||
3645 | 22 | from provisioningserver.rpc.interfaces import IConnection | 23 | from provisioningserver.rpc.interfaces import IConnection |
3646 | 23 | from provisioningserver.utils.twisted import asynchronous | 24 | from provisioningserver.utils.twisted import asynchronous |
3647 | 24 | from twisted.internet.defer import Deferred | 25 | from twisted.internet.defer import Deferred |
3652 | 25 | from twisted.protocols import amp | 26 | |
3653 | 26 | 27 | ||
3654 | 27 | 28 | class Identify(amp32.Command): | |
3651 | 28 | class Identify(amp.Command): | ||
3655 | 29 | """Request the identity of the remote side, e.g. its UUID. | 29 | """Request the identity of the remote side, e.g. its UUID. |
3656 | 30 | 30 | ||
3657 | 31 | :since: 1.5 | 31 | :since: 1.5 |
3658 | 32 | """ | 32 | """ |
3659 | 33 | 33 | ||
3664 | 34 | response = [(b"ident", amp.Unicode())] | 34 | response = [(b"ident", amp32.Unicode())] |
3665 | 35 | 35 | ||
3666 | 36 | 36 | ||
3667 | 37 | class Authenticate(amp.Command): | 37 | class Authenticate(amp32.Command): |
3668 | 38 | """Authenticate the remote side. | 38 | """Authenticate the remote side. |
3669 | 39 | 39 | ||
3670 | 40 | The procedure is as follows: | 40 | The procedure is as follows: |
3671 | @@ -63,19 +63,19 @@ | |||
3672 | 63 | """ | 63 | """ |
3673 | 64 | 64 | ||
3674 | 65 | arguments = [ | 65 | arguments = [ |
3676 | 66 | (b"message", amp.String()), | 66 | (b"message", amp32.String()), |
3677 | 67 | ] | 67 | ] |
3678 | 68 | response = [ | 68 | response = [ |
3681 | 69 | (b"digest", amp.String()), | 69 | (b"digest", amp32.String()), |
3682 | 70 | (b"salt", amp.String()), # Is 'salt' the right term here? | 70 | (b"salt", amp32.String()), # Is 'salt' the right term here? |
3683 | 71 | ] | 71 | ] |
3684 | 72 | errors = [] | 72 | errors = [] |
3685 | 73 | 73 | ||
3686 | 74 | 74 | ||
3687 | 75 | class Client: | 75 | class Client: |
3689 | 76 | """Wrapper around an :class:`amp.AMP` instance. | 76 | """Wrapper around an :class:`amp32.AMP` instance. |
3690 | 77 | 77 | ||
3692 | 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, |
3693 | 79 | with alterations to make it suitable for use from a thread outside | 79 | with alterations to make it suitable for use from a thread outside |
3694 | 80 | of the reactor. | 80 | of the reactor. |
3695 | 81 | """ | 81 | """ |
3696 | @@ -107,7 +107,7 @@ | |||
3697 | 107 | different stack from the caller's, e.g. when calling into the | 107 | different stack from the caller's, e.g. when calling into the |
3698 | 108 | Twisted reactor from a thread. | 108 | Twisted reactor from a thread. |
3699 | 109 | 109 | ||
3701 | 110 | :param cmd: The `amp.Command` child class representing the remote | 110 | :param cmd: The `amp32.Command` child class representing the remote |
3702 | 111 | method to be invoked. | 111 | method to be invoked. |
3703 | 112 | :param kwargs: Any parameters to the remote method. Only keyword | 112 | :param kwargs: Any parameters to the remote method. Only keyword |
3704 | 113 | arguments are accepted. | 113 | arguments are accepted. |
3705 | @@ -143,10 +143,10 @@ | |||
3706 | 143 | return hash(self._conn) | 143 | return hash(self._conn) |
3707 | 144 | 144 | ||
3708 | 145 | 145 | ||
3711 | 146 | class RPCProtocol(amp.AMP, object): | 146 | class RPCProtocol(amp32.AMP, object): |
3712 | 147 | """A specialisation of `amp.AMP`. | 147 | """A specialisation of `amp32.AMP`. |
3713 | 148 | 148 | ||
3715 | 149 | It's hard to track exactly when an `amp.AMP` protocol is connected to its | 149 | It's hard to track exactly when an `amp32.AMP` protocol is connected to its |
3716 | 150 | transport, or disconnected, from the "outside". It's necessary to subclass | 150 | transport, or disconnected, from the "outside". It's necessary to subclass |
3717 | 151 | and override `connectionMade` and `connectionLost` and signal from there, | 151 | and override `connectionMade` and `connectionLost` and signal from there, |
3718 | 152 | which is what this class does. | 152 | which is what this class does. |
3719 | 153 | 153 | ||
3720 | === modified file 'src/provisioningserver/rpc/monitors.py' | |||
3721 | --- src/provisioningserver/rpc/monitors.py 2014-10-16 11:12:48 +0000 | |||
3722 | +++ src/provisioningserver/rpc/monitors.py 2014-11-12 12:20:32 +0000 | |||
3723 | @@ -20,11 +20,13 @@ | |||
3724 | 20 | from datetime import datetime | 20 | from datetime import datetime |
3725 | 21 | 21 | ||
3726 | 22 | from provisioningserver.logger import get_maas_logger | 22 | from provisioningserver.logger import get_maas_logger |
3728 | 23 | from provisioningserver.rpc import getRegionClient | 23 | from provisioningserver.rpc import ( |
3729 | 24 | amp32, | ||
3730 | 25 | getRegionClient, | ||
3731 | 26 | ) | ||
3732 | 24 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable | 27 | from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
3733 | 25 | from provisioningserver.rpc.region import MonitorExpired | 28 | from provisioningserver.rpc.region import MonitorExpired |
3734 | 26 | from twisted.internet import reactor | 29 | from twisted.internet import reactor |
3735 | 27 | from twisted.protocols import amp | ||
3736 | 28 | 30 | ||
3737 | 29 | 31 | ||
3738 | 30 | maaslog = get_maas_logger("monitors") | 32 | maaslog = get_maas_logger("monitors") |
3739 | @@ -47,7 +49,7 @@ | |||
3740 | 47 | monitor ID. | 49 | monitor ID. |
3741 | 48 | """ | 50 | """ |
3742 | 49 | for monitor in monitors: | 51 | for monitor in monitors: |
3744 | 50 | delay = monitor["deadline"] - datetime.now(amp.utc) | 52 | delay = monitor["deadline"] - datetime.now(amp32.utc) |
3745 | 51 | monitor_id = monitor["id"] | 53 | monitor_id = monitor["id"] |
3746 | 52 | if monitor_id in running_monitors: | 54 | if monitor_id in running_monitors: |
3747 | 53 | dc, _ = running_monitors.pop(monitor_id) | 55 | dc, _ = running_monitors.pop(monitor_id) |
3748 | 54 | 56 | ||
3749 | === modified file 'src/provisioningserver/rpc/region.py' | |||
3750 | --- src/provisioningserver/rpc/region.py 2014-10-30 11:29:22 +0000 | |||
3751 | +++ src/provisioningserver/rpc/region.py 2014-11-12 12:20:32 +0000 | |||
3752 | @@ -40,6 +40,7 @@ | |||
3753 | 40 | "UpdateNodePowerState", | 40 | "UpdateNodePowerState", |
3754 | 41 | ] | 41 | ] |
3755 | 42 | 42 | ||
3756 | 43 | from provisioningserver.rpc import amp32 | ||
3757 | 43 | from provisioningserver.rpc.arguments import ( | 44 | from provisioningserver.rpc.arguments import ( |
3758 | 44 | Bytes, | 45 | Bytes, |
3759 | 45 | Choice, | 46 | Choice, |
3760 | @@ -59,10 +60,9 @@ | |||
3761 | 59 | NoSuchEventType, | 60 | NoSuchEventType, |
3762 | 60 | NoSuchNode, | 61 | NoSuchNode, |
3763 | 61 | ) | 62 | ) |
3768 | 62 | from twisted.protocols import amp | 63 | |
3769 | 63 | 64 | ||
3770 | 64 | 65 | class Register(amp32.Command): | |
3767 | 65 | class Register(amp.Command): | ||
3771 | 66 | """Register a cluster with the region controller. | 66 | """Register a cluster with the region controller. |
3772 | 67 | 67 | ||
3773 | 68 | This is the last part of the Authenticate and Register two-step. See | 68 | This is the last part of the Authenticate and Register two-step. See |
3774 | @@ -72,11 +72,11 @@ | |||
3775 | 72 | """ | 72 | """ |
3776 | 73 | 73 | ||
3777 | 74 | arguments = [ | 74 | arguments = [ |
3783 | 75 | (b"uuid", amp.Unicode()), | 75 | (b"uuid", amp32.Unicode()), |
3784 | 76 | (b"networks", amp.AmpList([ | 76 | (b"networks", amp32.AmpList([ |
3785 | 77 | (b"interface", amp.Unicode()), | 77 | (b"interface", amp32.Unicode()), |
3786 | 78 | (b"ip", amp.Unicode()), | 78 | (b"ip", amp32.Unicode()), |
3787 | 79 | (b"subnet_mask", amp.Unicode()), | 79 | (b"subnet_mask", amp32.Unicode()), |
3788 | 80 | ], optional=True)), | 80 | ], optional=True)), |
3789 | 81 | # The URL for the region as seen by the cluster. | 81 | # The URL for the region as seen by the cluster. |
3790 | 82 | (b"url", ParsedURL(optional=True)), | 82 | (b"url", ParsedURL(optional=True)), |
3791 | @@ -87,7 +87,7 @@ | |||
3792 | 87 | } | 87 | } |
3793 | 88 | 88 | ||
3794 | 89 | 89 | ||
3796 | 90 | class ReportBootImages(amp.Command): | 90 | class ReportBootImages(amp32.Command): |
3797 | 91 | """Report boot images available on the invoking cluster controller. | 91 | """Report boot images available on the invoking cluster controller. |
3798 | 92 | 92 | ||
3799 | 93 | :since: 1.5 | 93 | :since: 1.5 |
3800 | @@ -95,18 +95,18 @@ | |||
3801 | 95 | 95 | ||
3802 | 96 | arguments = [ | 96 | arguments = [ |
3803 | 97 | # The cluster UUID. | 97 | # The cluster UUID. |
3810 | 98 | (b"uuid", amp.Unicode()), | 98 | (b"uuid", amp32.Unicode()), |
3811 | 99 | (b"images", amp.AmpList( | 99 | (b"images", amp32.AmpList( |
3812 | 100 | [(b"architecture", amp.Unicode()), | 100 | [(b"architecture", amp32.Unicode()), |
3813 | 101 | (b"subarchitecture", amp.Unicode()), | 101 | (b"subarchitecture", amp32.Unicode()), |
3814 | 102 | (b"release", amp.Unicode()), | 102 | (b"release", amp32.Unicode()), |
3815 | 103 | (b"purpose", amp.Unicode())])), | 103 | (b"purpose", amp32.Unicode())])), |
3816 | 104 | ] | 104 | ] |
3817 | 105 | response = [] | 105 | response = [] |
3818 | 106 | errors = [] | 106 | errors = [] |
3819 | 107 | 107 | ||
3820 | 108 | 108 | ||
3822 | 109 | class GetBootSources(amp.Command): | 109 | class GetBootSources(amp32.Command): |
3823 | 110 | """Report boot sources and selections for the given cluster. | 110 | """Report boot sources and selections for the given cluster. |
3824 | 111 | 111 | ||
3825 | 112 | :since: 1.6 | 112 | :since: 1.6 |
3826 | @@ -115,22 +115,22 @@ | |||
3827 | 115 | 115 | ||
3828 | 116 | arguments = [ | 116 | arguments = [ |
3829 | 117 | # The cluster UUID. | 117 | # The cluster UUID. |
3831 | 118 | (b"uuid", amp.Unicode()), | 118 | (b"uuid", amp32.Unicode()), |
3832 | 119 | ] | 119 | ] |
3833 | 120 | response = [ | 120 | response = [ |
3836 | 121 | (b"sources", amp.AmpList( | 121 | (b"sources", amp32.AmpList( |
3837 | 122 | [(b"url", amp.Unicode()), | 122 | [(b"url", amp32.Unicode()), |
3838 | 123 | (b"keyring_data", Bytes()), | 123 | (b"keyring_data", Bytes()), |
3844 | 124 | (b"selections", amp.AmpList( | 124 | (b"selections", amp32.AmpList( |
3845 | 125 | [(b"release", amp.Unicode()), | 125 | [(b"release", amp32.Unicode()), |
3846 | 126 | (b"arches", amp.ListOf(amp.Unicode())), | 126 | (b"arches", amp32.ListOf(amp32.Unicode())), |
3847 | 127 | (b"subarches", amp.ListOf(amp.Unicode())), | 127 | (b"subarches", amp32.ListOf(amp32.Unicode())), |
3848 | 128 | (b"labels", amp.ListOf(amp.Unicode()))]))])), | 128 | (b"labels", amp32.ListOf(amp32.Unicode()))]))])), |
3849 | 129 | ] | 129 | ] |
3850 | 130 | errors = [] | 130 | errors = [] |
3851 | 131 | 131 | ||
3852 | 132 | 132 | ||
3854 | 133 | class GetBootSourcesV2(amp.Command): | 133 | class GetBootSourcesV2(amp32.Command): |
3855 | 134 | """Report boot sources and selections for the given cluster. | 134 | """Report boot sources and selections for the given cluster. |
3856 | 135 | 135 | ||
3857 | 136 | Includes the new os field for the selections. | 136 | Includes the new os field for the selections. |
3858 | @@ -140,33 +140,33 @@ | |||
3859 | 140 | 140 | ||
3860 | 141 | arguments = [ | 141 | arguments = [ |
3861 | 142 | # The cluster UUID. | 142 | # The cluster UUID. |
3863 | 143 | (b"uuid", amp.Unicode()), | 143 | (b"uuid", amp32.Unicode()), |
3864 | 144 | ] | 144 | ] |
3865 | 145 | response = [ | 145 | response = [ |
3868 | 146 | (b"sources", amp.AmpList( | 146 | (b"sources", amp32.AmpList( |
3869 | 147 | [(b"url", amp.Unicode()), | 147 | [(b"url", amp32.Unicode()), |
3870 | 148 | (b"keyring_data", Bytes()), | 148 | (b"keyring_data", Bytes()), |
3877 | 149 | (b"selections", amp.AmpList( | 149 | (b"selections", amp32.AmpList( |
3878 | 150 | [(b"os", amp.Unicode()), | 150 | [(b"os", amp32.Unicode()), |
3879 | 151 | (b"release", amp.Unicode()), | 151 | (b"release", amp32.Unicode()), |
3880 | 152 | (b"arches", amp.ListOf(amp.Unicode())), | 152 | (b"arches", amp32.ListOf(amp32.Unicode())), |
3881 | 153 | (b"subarches", amp.ListOf(amp.Unicode())), | 153 | (b"subarches", amp32.ListOf(amp32.Unicode())), |
3882 | 154 | (b"labels", amp.ListOf(amp.Unicode()))]))])), | 154 | (b"labels", amp32.ListOf(amp32.Unicode()))]))])), |
3883 | 155 | ] | 155 | ] |
3884 | 156 | errors = [] | 156 | errors = [] |
3885 | 157 | 157 | ||
3886 | 158 | 158 | ||
3888 | 159 | class UpdateLeases(amp.Command): | 159 | class UpdateLeases(amp32.Command): |
3889 | 160 | """Report DHCP leases on the invoking cluster controller. | 160 | """Report DHCP leases on the invoking cluster controller. |
3890 | 161 | 161 | ||
3891 | 162 | :since: 1.7 | 162 | :since: 1.7 |
3892 | 163 | """ | 163 | """ |
3893 | 164 | arguments = [ | 164 | arguments = [ |
3894 | 165 | # The cluster UUID. | 165 | # The cluster UUID. |
3896 | 166 | (b"uuid", amp.Unicode()), | 166 | (b"uuid", amp32.Unicode()), |
3897 | 167 | (b"mappings", CompressedAmpList( | 167 | (b"mappings", CompressedAmpList( |
3900 | 168 | [(b"ip", amp.Unicode()), | 168 | [(b"ip", amp32.Unicode()), |
3901 | 169 | (b"mac", amp.Unicode())])) | 169 | (b"mac", amp32.Unicode())])) |
3902 | 170 | ] | 170 | ] |
3903 | 171 | response = [] | 171 | response = [] |
3904 | 172 | errors = { | 172 | errors = { |
3905 | @@ -174,7 +174,7 @@ | |||
3906 | 174 | } | 174 | } |
3907 | 175 | 175 | ||
3908 | 176 | 176 | ||
3910 | 177 | class GetArchiveMirrors(amp.Command): | 177 | class GetArchiveMirrors(amp32.Command): |
3911 | 178 | """Return the Main and Port mirrors to use. | 178 | """Return the Main and Port mirrors to use. |
3912 | 179 | 179 | ||
3913 | 180 | :since: 1.7 | 180 | :since: 1.7 |
3914 | @@ -187,7 +187,7 @@ | |||
3915 | 187 | errors = [] | 187 | errors = [] |
3916 | 188 | 188 | ||
3917 | 189 | 189 | ||
3919 | 190 | class GetProxies(amp.Command): | 190 | class GetProxies(amp32.Command): |
3920 | 191 | """Return the HTTP and HTTPS proxies to use. | 191 | """Return the HTTP and HTTPS proxies to use. |
3921 | 192 | 192 | ||
3922 | 193 | :since: 1.6 | 193 | :since: 1.6 |
3923 | @@ -201,7 +201,7 @@ | |||
3924 | 201 | errors = [] | 201 | errors = [] |
3925 | 202 | 202 | ||
3926 | 203 | 203 | ||
3928 | 204 | class GetClusterStatus(amp.Command): | 204 | class GetClusterStatus(amp32.Command): |
3929 | 205 | """Return the status of the given cluster. | 205 | """Return the status of the given cluster. |
3930 | 206 | 206 | ||
3931 | 207 | :since: 1.7 | 207 | :since: 1.7 |
3932 | @@ -209,7 +209,7 @@ | |||
3933 | 209 | 209 | ||
3934 | 210 | arguments = [ | 210 | arguments = [ |
3935 | 211 | # The cluster UUID. | 211 | # The cluster UUID. |
3937 | 212 | (b"uuid", amp.Unicode()), | 212 | (b"uuid", amp32.Unicode()), |
3938 | 213 | ] | 213 | ] |
3939 | 214 | _response_status_choices = { | 214 | _response_status_choices = { |
3940 | 215 | 0: b"PENDING", # NODEGROUP_STATUS.PENDING | 215 | 0: b"PENDING", # NODEGROUP_STATUS.PENDING |
3941 | @@ -224,7 +224,7 @@ | |||
3942 | 224 | } | 224 | } |
3943 | 225 | 225 | ||
3944 | 226 | 226 | ||
3946 | 227 | class MarkNodeFailed(amp.Command): | 227 | class MarkNodeFailed(amp32.Command): |
3947 | 228 | """Mark a node as 'broken'. | 228 | """Mark a node as 'broken'. |
3948 | 229 | 229 | ||
3949 | 230 | :since: 1.7 | 230 | :since: 1.7 |
3950 | @@ -232,9 +232,9 @@ | |||
3951 | 232 | 232 | ||
3952 | 233 | arguments = [ | 233 | arguments = [ |
3953 | 234 | # The node's system_id. | 234 | # The node's system_id. |
3955 | 235 | (b"system_id", amp.Unicode()), | 235 | (b"system_id", amp32.Unicode()), |
3956 | 236 | # The error description. | 236 | # The error description. |
3958 | 237 | (b"error_description", amp.Unicode()), | 237 | (b"error_description", amp32.Unicode()), |
3959 | 238 | ] | 238 | ] |
3960 | 239 | response = [] | 239 | response = [] |
3961 | 240 | errors = { | 240 | errors = { |
3962 | @@ -243,7 +243,7 @@ | |||
3963 | 243 | } | 243 | } |
3964 | 244 | 244 | ||
3965 | 245 | 245 | ||
3967 | 246 | class ListNodePowerParameters(amp.Command): | 246 | class ListNodePowerParameters(amp32.Command): |
3968 | 247 | """Return the list of power parameters for nodes | 247 | """Return the list of power parameters for nodes |
3969 | 248 | that this cluster controls. | 248 | that this cluster controls. |
3970 | 249 | 249 | ||
3971 | @@ -255,14 +255,14 @@ | |||
3972 | 255 | 255 | ||
3973 | 256 | arguments = [ | 256 | arguments = [ |
3974 | 257 | # The cluster UUID. | 257 | # The cluster UUID. |
3976 | 258 | (b"uuid", amp.Unicode()), | 258 | (b"uuid", amp32.Unicode()), |
3977 | 259 | ] | 259 | ] |
3978 | 260 | response = [ | 260 | response = [ |
3984 | 261 | (b"nodes", amp.AmpList( | 261 | (b"nodes", amp32.AmpList( |
3985 | 262 | [(b"system_id", amp.Unicode()), | 262 | [(b"system_id", amp32.Unicode()), |
3986 | 263 | (b"hostname", amp.Unicode()), | 263 | (b"hostname", amp32.Unicode()), |
3987 | 264 | (b"power_state", amp.Unicode()), | 264 | (b"power_state", amp32.Unicode()), |
3988 | 265 | (b"power_type", amp.Unicode()), | 265 | (b"power_type", amp32.Unicode()), |
3989 | 266 | # We can't define a tighter schema here because this is a highly | 266 | # We can't define a tighter schema here because this is a highly |
3990 | 267 | # variable bag of arguments from a variety of sources. | 267 | # variable bag of arguments from a variety of sources. |
3991 | 268 | (b"context", StructureAsJSON())])), | 268 | (b"context", StructureAsJSON())])), |
3992 | @@ -272,7 +272,7 @@ | |||
3993 | 272 | } | 272 | } |
3994 | 273 | 273 | ||
3995 | 274 | 274 | ||
3997 | 275 | class UpdateNodePowerState(amp.Command): | 275 | class UpdateNodePowerState(amp32.Command): |
3998 | 276 | """Update Node Power State. | 276 | """Update Node Power State. |
3999 | 277 | 277 | ||
4000 | 278 | :since: 1.7 | 278 | :since: 1.7 |
4001 | @@ -280,120 +280,120 @@ | |||
4002 | 280 | 280 | ||
4003 | 281 | arguments = [ | 281 | arguments = [ |
4004 | 282 | # The node's system_id. | 282 | # The node's system_id. |
4006 | 283 | (b"system_id", amp.Unicode()), | 283 | (b"system_id", amp32.Unicode()), |
4007 | 284 | # The node's power_state. | 284 | # The node's power_state. |
4009 | 285 | (b"power_state", amp.Unicode()), | 285 | (b"power_state", amp32.Unicode()), |
4010 | 286 | ] | 286 | ] |
4011 | 287 | response = [] | 287 | response = [] |
4012 | 288 | errors = {NoSuchNode: b"NoSuchNode"} | 288 | errors = {NoSuchNode: b"NoSuchNode"} |
4013 | 289 | 289 | ||
4014 | 290 | 290 | ||
4016 | 291 | class RegisterEventType(amp.Command): | 291 | class RegisterEventType(amp32.Command): |
4017 | 292 | """Register an event type. | 292 | """Register an event type. |
4018 | 293 | 293 | ||
4019 | 294 | :since: 1.7 | 294 | :since: 1.7 |
4020 | 295 | """ | 295 | """ |
4021 | 296 | 296 | ||
4022 | 297 | arguments = [ | 297 | arguments = [ |
4026 | 298 | (b"name", amp.Unicode()), | 298 | (b"name", amp32.Unicode()), |
4027 | 299 | (b"description", amp.Unicode()), | 299 | (b"description", amp32.Unicode()), |
4028 | 300 | (b"level", amp.Integer()), | 300 | (b"level", amp32.Integer()), |
4029 | 301 | ] | 301 | ] |
4030 | 302 | response = [] | 302 | response = [] |
4031 | 303 | errors = [] | 303 | errors = [] |
4032 | 304 | 304 | ||
4033 | 305 | 305 | ||
4071 | 306 | class SendEvent(amp.Command): | 306 | class SendEvent(amp32.Command): |
4072 | 307 | """Send an event. | 307 | """Send an event. |
4073 | 308 | 308 | ||
4074 | 309 | :since: 1.7 | 309 | :since: 1.7 |
4075 | 310 | """ | 310 | """ |
4076 | 311 | 311 | ||
4077 | 312 | arguments = [ | 312 | arguments = [ |
4078 | 313 | (b"system_id", amp.Unicode()), | 313 | (b"system_id", amp32.Unicode()), |
4079 | 314 | (b"type_name", amp.Unicode()), | 314 | (b"type_name", amp32.Unicode()), |
4080 | 315 | (b"description", amp.Unicode()), | 315 | (b"description", amp32.Unicode()), |
4081 | 316 | ] | 316 | ] |
4082 | 317 | response = [] | 317 | response = [] |
4083 | 318 | errors = { | 318 | errors = { |
4084 | 319 | NoSuchNode: b"NoSuchNode", | 319 | NoSuchNode: b"NoSuchNode", |
4085 | 320 | NoSuchEventType: b"NoSuchEventType" | 320 | NoSuchEventType: b"NoSuchEventType" |
4086 | 321 | } | 321 | } |
4087 | 322 | 322 | ||
4088 | 323 | 323 | ||
4089 | 324 | class SendEventMACAddress(amp.Command): | 324 | class SendEventMACAddress(amp32.Command): |
4090 | 325 | """Send an event. | 325 | """Send an event. |
4091 | 326 | 326 | ||
4092 | 327 | :since: 1.7 | 327 | :since: 1.7 |
4093 | 328 | """ | 328 | """ |
4094 | 329 | 329 | ||
4095 | 330 | arguments = [ | 330 | arguments = [ |
4096 | 331 | (b"mac_address", amp.Unicode()), | 331 | (b"mac_address", amp32.Unicode()), |
4097 | 332 | (b"type_name", amp.Unicode()), | 332 | (b"type_name", amp32.Unicode()), |
4098 | 333 | (b"description", amp.Unicode()), | 333 | (b"description", amp32.Unicode()), |
4099 | 334 | ] | 334 | ] |
4100 | 335 | response = [] | 335 | response = [] |
4101 | 336 | errors = { | 336 | errors = { |
4102 | 337 | NoSuchNode: b"NoSuchNode", | 337 | NoSuchNode: b"NoSuchNode", |
4103 | 338 | NoSuchEventType: b"NoSuchEventType" | 338 | NoSuchEventType: b"NoSuchEventType" |
4104 | 339 | } | 339 | } |
4105 | 340 | 340 | ||
4106 | 341 | 341 | ||
4107 | 342 | class ReportForeignDHCPServer(amp.Command): | 342 | class ReportForeignDHCPServer(amp32.Command): |
4108 | 343 | """Report a foreign DHCP server on the cluster's network. | 343 | """Report a foreign DHCP server on the cluster's network. |
4109 | 344 | 344 | ||
4110 | 345 | :since: 1.7 | 345 | :since: 1.7 |
4111 | 346 | """ | 346 | """ |
4112 | 347 | 347 | ||
4113 | 348 | arguments = [ | 348 | arguments = [ |
4117 | 349 | (b"cluster_uuid", amp.Unicode()), | 349 | (b"cluster_uuid", amp32.Unicode()), |
4118 | 350 | (b"interface_name", amp.Unicode()), | 350 | (b"interface_name", amp32.Unicode()), |
4119 | 351 | (b"foreign_dhcp_ip", amp.Unicode(optional=True)), | 351 | (b"foreign_dhcp_ip", amp32.Unicode(optional=True)), |
4120 | 352 | ] | 352 | ] |
4121 | 353 | response = [] | 353 | response = [] |
4122 | 354 | errors = [] | 354 | errors = [] |
4123 | 355 | 355 | ||
4124 | 356 | 356 | ||
4126 | 357 | class GetClusterInterfaces(amp.Command): | 357 | class GetClusterInterfaces(amp32.Command): |
4127 | 358 | """Fetch the known interfaces for a cluster from the region. | 358 | """Fetch the known interfaces for a cluster from the region. |
4128 | 359 | 359 | ||
4129 | 360 | :since: 1.7 | 360 | :since: 1.7 |
4130 | 361 | """ | 361 | """ |
4131 | 362 | 362 | ||
4132 | 363 | arguments = [ | 363 | arguments = [ |
4134 | 364 | (b"cluster_uuid", amp.Unicode()), | 364 | (b"cluster_uuid", amp32.Unicode()), |
4135 | 365 | ] | 365 | ] |
4136 | 366 | response = [ | 366 | response = [ |
4141 | 367 | (b"interfaces", amp.AmpList( | 367 | (b"interfaces", amp32.AmpList( |
4142 | 368 | [(b"name", amp.Unicode()), | 368 | [(b"name", amp32.Unicode()), |
4143 | 369 | (b"interface", amp.Unicode()), | 369 | (b"interface", amp32.Unicode()), |
4144 | 370 | (b"ip", amp.Unicode())])) | 370 | (b"ip", amp32.Unicode())])) |
4145 | 371 | ] | 371 | ] |
4146 | 372 | errors = [] | 372 | errors = [] |
4147 | 373 | 373 | ||
4148 | 374 | 374 | ||
4150 | 375 | class CreateNode(amp.Command): | 375 | class CreateNode(amp32.Command): |
4151 | 376 | """Create a node on a given cluster. | 376 | """Create a node on a given cluster. |
4152 | 377 | 377 | ||
4153 | 378 | :since: 1.7 | 378 | :since: 1.7 |
4154 | 379 | """ | 379 | """ |
4155 | 380 | 380 | ||
4156 | 381 | arguments = [ | 381 | arguments = [ |
4162 | 382 | (b'cluster_uuid', amp.Unicode()), | 382 | (b'cluster_uuid', amp32.Unicode()), |
4163 | 383 | (b'architecture', amp.Unicode()), | 383 | (b'architecture', amp32.Unicode()), |
4164 | 384 | (b'power_type', amp.Unicode()), | 384 | (b'power_type', amp32.Unicode()), |
4165 | 385 | (b'power_parameters', amp.Unicode()), | 385 | (b'power_parameters', amp32.Unicode()), |
4166 | 386 | (b'mac_addresses', amp.ListOf(amp.Unicode())), | 386 | (b'mac_addresses', amp32.ListOf(amp32.Unicode())), |
4167 | 387 | ] | 387 | ] |
4168 | 388 | response = [ | 388 | response = [ |
4170 | 389 | (b'system_id', amp.Unicode()), | 389 | (b'system_id', amp32.Unicode()), |
4171 | 390 | ] | 390 | ] |
4172 | 391 | errors = { | 391 | errors = { |
4173 | 392 | NodeAlreadyExists: b"NodeAlreadyExists", | 392 | NodeAlreadyExists: b"NodeAlreadyExists", |
4174 | 393 | } | 393 | } |
4175 | 394 | 394 | ||
4176 | 395 | 395 | ||
4178 | 396 | class MonitorExpired(amp.Command): | 396 | class MonitorExpired(amp32.Command): |
4179 | 397 | """Called by a cluster when a running monitor hits its deadline. | 397 | """Called by a cluster when a running monitor hits its deadline. |
4180 | 398 | 398 | ||
4181 | 399 | The original context parameter from the StartMonitors call is returned. | 399 | The original context parameter from the StartMonitors call is returned. |
4182 | @@ -402,14 +402,14 @@ | |||
4183 | 402 | """ | 402 | """ |
4184 | 403 | 403 | ||
4185 | 404 | arguments = [ | 404 | arguments = [ |
4187 | 405 | (b"id", amp.Unicode()), | 405 | (b"id", amp32.Unicode()), |
4188 | 406 | (b"context", StructureAsJSON()), | 406 | (b"context", StructureAsJSON()), |
4189 | 407 | ] | 407 | ] |
4190 | 408 | response = [] | 408 | response = [] |
4191 | 409 | errors = [] | 409 | errors = [] |
4192 | 410 | 410 | ||
4193 | 411 | 411 | ||
4195 | 412 | class ReloadCluster(amp.Command): | 412 | class ReloadCluster(amp32.Command): |
4196 | 413 | """Called by a cluster when it wants to reload its state. | 413 | """Called by a cluster when it wants to reload its state. |
4197 | 414 | 414 | ||
4198 | 415 | The region may respond with many different calls to the cluster | 415 | The region may respond with many different calls to the cluster |
4199 | @@ -424,30 +424,30 @@ | |||
4200 | 424 | """ | 424 | """ |
4201 | 425 | 425 | ||
4202 | 426 | arguments = [ | 426 | arguments = [ |
4204 | 427 | (b"cluster_uuid", amp.Unicode()), | 427 | (b"cluster_uuid", amp32.Unicode()), |
4205 | 428 | ] | 428 | ] |
4206 | 429 | response = [] | 429 | response = [] |
4207 | 430 | errors = [] | 430 | errors = [] |
4208 | 431 | 431 | ||
4209 | 432 | 432 | ||
4211 | 433 | class RequestNodeInfoByMACAddress(amp.Command): | 433 | class RequestNodeInfoByMACAddress(amp32.Command): |
4212 | 434 | """Request Node information by mac address. | 434 | """Request Node information by mac address. |
4213 | 435 | 435 | ||
4214 | 436 | :since: 1.7 | 436 | :since: 1.7 |
4215 | 437 | """ | 437 | """ |
4216 | 438 | 438 | ||
4217 | 439 | arguments = [ | 439 | arguments = [ |
4219 | 440 | (b"mac_address", amp.Unicode()), | 440 | (b"mac_address", amp32.Unicode()), |
4220 | 441 | ] | 441 | ] |
4221 | 442 | response = [ | 442 | response = [ |
4230 | 443 | (b"system_id", amp.Unicode()), | 443 | (b"system_id", amp32.Unicode()), |
4231 | 444 | (b"hostname", amp.Unicode()), | 444 | (b"hostname", amp32.Unicode()), |
4232 | 445 | (b"status", amp.Integer()), | 445 | (b"status", amp32.Integer()), |
4233 | 446 | (b"boot_type", amp.Unicode()), | 446 | (b"boot_type", amp32.Unicode()), |
4234 | 447 | (b"osystem", amp.Unicode()), | 447 | (b"osystem", amp32.Unicode()), |
4235 | 448 | (b"distro_series", amp.Unicode()), | 448 | (b"distro_series", amp32.Unicode()), |
4236 | 449 | (b"architecture", amp.Unicode()), | 449 | (b"architecture", amp32.Unicode()), |
4237 | 450 | (b"purpose", amp.Unicode()), | 450 | (b"purpose", amp32.Unicode()), |
4238 | 451 | ] | 451 | ] |
4239 | 452 | errors = { | 452 | errors = { |
4240 | 453 | NoSuchNode: b"NoSuchNode", | 453 | NoSuchNode: b"NoSuchNode", |
4241 | 454 | 454 | ||
4242 | === modified file 'src/provisioningserver/rpc/testing/__init__.py' | |||
4243 | --- src/provisioningserver/rpc/testing/__init__.py 2014-10-10 16:44:15 +0000 | |||
4244 | +++ src/provisioningserver/rpc/testing/__init__.py 2014-11-12 12:20:32 +0000 | |||
4245 | @@ -45,7 +45,10 @@ | |||
4246 | 45 | sentinel, | 45 | sentinel, |
4247 | 46 | ) | 46 | ) |
4248 | 47 | import provisioningserver | 47 | import provisioningserver |
4250 | 48 | from provisioningserver.rpc import region | 48 | from provisioningserver.rpc import ( |
4251 | 49 | amp32, | ||
4252 | 50 | region, | ||
4253 | 51 | ) | ||
4254 | 49 | from provisioningserver.rpc.clusterservice import ( | 52 | from provisioningserver.rpc.clusterservice import ( |
4255 | 50 | Cluster, | 53 | Cluster, |
4256 | 51 | ClusterClient, | 54 | ClusterClient, |
4257 | @@ -80,7 +83,6 @@ | |||
4258 | 80 | ) | 83 | ) |
4259 | 81 | from twisted.internet.protocol import Factory | 84 | from twisted.internet.protocol import Factory |
4260 | 82 | from twisted.internet.task import Clock | 85 | from twisted.internet.task import Clock |
4261 | 83 | from twisted.protocols import amp | ||
4262 | 84 | from twisted.python import ( | 86 | from twisted.python import ( |
4263 | 85 | log, | 87 | log, |
4264 | 86 | reflect, | 88 | reflect, |
4265 | @@ -100,11 +102,11 @@ | |||
4266 | 100 | d.addCallback(command.parseResponse, protocol) | 102 | d.addCallback(command.parseResponse, protocol) |
4267 | 101 | 103 | ||
4268 | 102 | def eb_massage_error(error): | 104 | def eb_massage_error(error): |
4270 | 103 | if error.check(amp.RemoteAmpError): | 105 | if error.check(amp32.RemoteAmpError): |
4271 | 104 | # Convert remote errors back into local errors using the | 106 | # Convert remote errors back into local errors using the |
4272 | 105 | # command's error map if possible. | 107 | # command's error map if possible. |
4273 | 106 | error_type = command.reverseErrors.get( | 108 | error_type = command.reverseErrors.get( |
4275 | 107 | error.value.errorCode, amp.UnknownRemoteError) | 109 | error.value.errorCode, amp32.UnknownRemoteError) |
4276 | 108 | return Failure(error_type(error.value.description)) | 110 | return Failure(error_type(error.value.description)) |
4277 | 109 | else: | 111 | else: |
4278 | 110 | # Exceptions raised in responders that aren't declared in that | 112 | # Exceptions raised in responders that aren't declared in that |
4279 | @@ -112,7 +114,7 @@ | |||
4280 | 112 | # in RemoteAmpError. This is because call_responder() bypasses the | 114 | # in RemoteAmpError. This is because call_responder() bypasses the |
4281 | 113 | # network marshall/unmarshall steps, where these exceptions would | 115 | # network marshall/unmarshall steps, where these exceptions would |
4282 | 114 | # ordinarily get squashed. | 116 | # ordinarily get squashed. |
4284 | 115 | return Failure(amp.UnknownRemoteError("%s: %s" % ( | 117 | return Failure(amp32.UnknownRemoteError("%s: %s" % ( |
4285 | 116 | reflect.qual(error.type), reflect.safe_str(error.value)))) | 118 | reflect.qual(error.type), reflect.safe_str(error.value)))) |
4286 | 117 | d.addErrback(eb_massage_error) | 119 | d.addErrback(eb_massage_error) |
4287 | 118 | 120 | ||
4288 | @@ -261,7 +263,7 @@ | |||
4289 | 261 | def addEventLoop(self, protocol): | 263 | def addEventLoop(self, protocol): |
4290 | 262 | """Add a new stub event-loop using the given `protocol`. | 264 | """Add a new stub event-loop using the given `protocol`. |
4291 | 263 | 265 | ||
4293 | 264 | The `protocol` should be an instance of `amp.AMP`. | 266 | The `protocol` should be an instance of `amp32.AMP`. |
4294 | 265 | 267 | ||
4295 | 266 | :returns: py:class:`twisted.test.iosim.IOPump` | 268 | :returns: py:class:`twisted.test.iosim.IOPump` |
4296 | 267 | """ | 269 | """ |
4297 | @@ -281,8 +283,8 @@ | |||
4298 | 281 | commands = commands + (region.Authenticate,) | 283 | commands = commands + (region.Authenticate,) |
4299 | 282 | if region.Register not in commands: | 284 | if region.Register not in commands: |
4300 | 283 | commands = commands + (region.Register,) | 285 | commands = commands + (region.Register,) |
4303 | 284 | if amp.StartTLS not in commands: | 286 | if amp32.StartTLS not in commands: |
4304 | 285 | commands = commands + (amp.StartTLS,) | 287 | commands = commands + (amp32.StartTLS,) |
4305 | 286 | protocol_factory = make_amp_protocol_factory(*commands) | 288 | protocol_factory = make_amp_protocol_factory(*commands) |
4306 | 287 | protocol = protocol_factory() | 289 | protocol = protocol_factory() |
4307 | 288 | eventloop = self.getEventLoopName(protocol) | 290 | eventloop = self.getEventLoopName(protocol) |
4308 | 289 | 291 | ||
4309 | === modified file 'src/provisioningserver/rpc/testing/tls.py' | |||
4310 | --- src/provisioningserver/rpc/testing/tls.py 2014-09-03 12:23:11 +0000 | |||
4311 | +++ src/provisioningserver/rpc/testing/tls.py 2014-11-12 12:20:32 +0000 | |||
4312 | @@ -26,7 +26,7 @@ | |||
4313 | 26 | def get_tls_parameters(private_cert_name, trust_cert_name): | 26 | def get_tls_parameters(private_cert_name, trust_cert_name): |
4314 | 27 | """get_tls_parameters() | 27 | """get_tls_parameters() |
4315 | 28 | 28 | ||
4317 | 29 | Implementation of :py:class:`~twisted.protocols.amp.StartTLS`. | 29 | Implementation of :py:class:`~provisioningserver.rpc.amp32.StartTLS`. |
4318 | 30 | """ | 30 | """ |
4319 | 31 | testing = filepath.FilePath(__file__).parent() | 31 | testing = filepath.FilePath(__file__).parent() |
4320 | 32 | with testing.child(private_cert_name).open() as fin: | 32 | with testing.child(private_cert_name).open() as fin: |
4321 | 33 | 33 | ||
4322 | === added file 'src/provisioningserver/rpc/tests/test_amp32.py' | |||
4323 | --- src/provisioningserver/rpc/tests/test_amp32.py 1970-01-01 00:00:00 +0000 | |||
4324 | +++ src/provisioningserver/rpc/tests/test_amp32.py 2014-11-12 12:20:32 +0000 | |||
4325 | @@ -0,0 +1,3270 @@ | |||
4326 | 1 | # Copyright (c) 2005 Divmod, Inc. | ||
4327 | 2 | # Copyright (c) Twisted Matrix Laboratories. | ||
4328 | 3 | # See LICENSE.Twisted for details. | ||
4329 | 4 | |||
4330 | 5 | """ | ||
4331 | 6 | Tests for L{provisioningserver.rpc.amp32}. | ||
4332 | 7 | """ | ||
4333 | 8 | |||
4334 | 9 | from __future__ import ( | ||
4335 | 10 | absolute_import, | ||
4336 | 11 | print_function, | ||
4337 | 12 | # unicode_literals, | ||
4338 | 13 | ) | ||
4339 | 14 | |||
4340 | 15 | str = None | ||
4341 | 16 | |||
4342 | 17 | __metaclass__ = type | ||
4343 | 18 | __all__ = [] | ||
4344 | 19 | |||
4345 | 20 | import datetime | ||
4346 | 21 | import decimal | ||
4347 | 22 | from warnings import ( | ||
4348 | 23 | catch_warnings, | ||
4349 | 24 | simplefilter, | ||
4350 | 25 | ) | ||
4351 | 26 | |||
4352 | 27 | from maastesting.fixtures import TempDirectory | ||
4353 | 28 | from maastesting.testcase import ( | ||
4354 | 29 | MAASTestCase, | ||
4355 | 30 | MAASTwistedRunTest, | ||
4356 | 31 | ) | ||
4357 | 32 | from provisioningserver.rpc import amp32 | ||
4358 | 33 | from provisioningserver.rpc.testing import TwistedLoggerFixture | ||
4359 | 34 | from testtools.deferredruntest import assert_fails_with | ||
4360 | 35 | from testtools.matchers import ( | ||
4361 | 36 | Equals, | ||
4362 | 37 | HasLength, | ||
4363 | 38 | Is, | ||
4364 | 39 | IsInstance, | ||
4365 | 40 | StartsWith, | ||
4366 | 41 | ) | ||
4367 | 42 | from twisted.internet import ( | ||
4368 | 43 | defer, | ||
4369 | 44 | error, | ||
4370 | 45 | interfaces, | ||
4371 | 46 | protocol, | ||
4372 | 47 | reactor, | ||
4373 | 48 | ) | ||
4374 | 49 | from twisted.internet.address import UNIXAddress | ||
4375 | 50 | from twisted.internet.error import ConnectionLost | ||
4376 | 51 | from twisted.python import filepath | ||
4377 | 52 | from twisted.python.failure import Failure | ||
4378 | 53 | from twisted.test import iosim | ||
4379 | 54 | from twisted.test.proto_helpers import StringTransport | ||
4380 | 55 | from zope.interface import implements | ||
4381 | 56 | from zope.interface.verify import ( | ||
4382 | 57 | verifyClass, | ||
4383 | 58 | verifyObject, | ||
4384 | 59 | ) | ||
4385 | 60 | |||
4386 | 61 | |||
4387 | 62 | try: | ||
4388 | 63 | from twisted.internet import ssl | ||
4389 | 64 | except ImportError: | ||
4390 | 65 | ssl = None | ||
4391 | 66 | else: | ||
4392 | 67 | if not ssl.supported: | ||
4393 | 68 | ssl = None | ||
4394 | 69 | |||
4395 | 70 | if ssl is None: | ||
4396 | 71 | skipSSL = "SSL not available" | ||
4397 | 72 | else: | ||
4398 | 73 | skipSSL = None | ||
4399 | 74 | |||
4400 | 75 | |||
4401 | 76 | class AMP32TestCase(MAASTestCase): | ||
4402 | 77 | """Common test class for testing AMP with 32-bit length prefixes. | ||
4403 | 78 | |||
4404 | 79 | All the tests in this module, as well as the implementation in `amp32`, | ||
4405 | 80 | are derived from Twisted's `amp` implementation. Twisted uses Trial and | ||
4406 | 81 | the AMP tests depend on it. Many of these dependencies have been recast | ||
4407 | 82 | into uses of `testtools` or plain `unittest` and so on. This class remains | ||
4408 | 83 | as the retirement home for the last few bits for which there are not yet | ||
4409 | 84 | clean and uninvasive replacements. | ||
4410 | 85 | """ | ||
4411 | 86 | |||
4412 | 87 | run_tests_with = MAASTwistedRunTest.make_factory(timeout=5) | ||
4413 | 88 | |||
4414 | 89 | def setUp(self): | ||
4415 | 90 | super(AMP32TestCase, self).setUp() | ||
4416 | 91 | # Capture all Twisted logs. Normally this isn't necessary, but these | ||
4417 | 92 | # tests are very noisy without this. This is also required for | ||
4418 | 93 | # getLoggedFailures(). | ||
4419 | 94 | self.logger = self.useFixture(TwistedLoggerFixture()) | ||
4420 | 95 | |||
4421 | 96 | def getLoggedFailures(self, error_type): | ||
4422 | 97 | """Replacement for Trial's `flushLoggedErrors`. | ||
4423 | 98 | |||
4424 | 99 | This only queries the log for errors, whereas `flushLoggedErrors` also | ||
4425 | 100 | removes those errors from the log. The reason for this is that Twisted | ||
4426 | 101 | will fail a test if any errors are logged. | ||
4427 | 102 | |||
4428 | 103 | In time it would make sense to adopt Trial's approach. That's too | ||
4429 | 104 | invasive at present, so this remains here as a reminder. | ||
4430 | 105 | |||
4431 | 106 | See :py:func:`twisted.trial.testcase.TestCase.flushLoggedErrors`. | ||
4432 | 107 | """ | ||
4433 | 108 | errors = (log for log in self.logger.logs if log["isError"]) | ||
4434 | 109 | failures = (log['failure'] for log in errors if 'failure' in log) | ||
4435 | 110 | return list(f for f in failures if f.check(error_type)) | ||
4436 | 111 | |||
4437 | 112 | def assertWarns(self, category, message, filename, f, *args, **kwargs): | ||
4438 | 113 | """Replacement for Trial's `assertWarns`. | ||
4439 | 114 | |||
4440 | 115 | Trial's implementation is slightly more featureful, but this does much | ||
4441 | 116 | the same. Could be recast as a :py:class:`testtools.matchers.Matcher`. | ||
4442 | 117 | |||
4443 | 118 | See :py:func:`twisted.trial.testcase.TestCase.assertWarns`. | ||
4444 | 119 | """ | ||
4445 | 120 | with catch_warnings(record=True) as log: | ||
4446 | 121 | simplefilter('always') | ||
4447 | 122 | result = f(*args, **kwargs) | ||
4448 | 123 | |||
4449 | 124 | self.assertThat(log, HasLength(1)) | ||
4450 | 125 | [warning] = log | ||
4451 | 126 | self.expectThat(category, Is(warning.category)) | ||
4452 | 127 | self.expectThat(warning.message, IsInstance(Warning)) | ||
4453 | 128 | self.expectThat(message, Equals(warning.message[0])) | ||
4454 | 129 | self.expectThat(filename, StartsWith(warning.filename)) | ||
4455 | 130 | |||
4456 | 131 | return result | ||
4457 | 132 | |||
4458 | 133 | |||
4459 | 134 | class TestProto(protocol.Protocol): | ||
4460 | 135 | """ | ||
4461 | 136 | A trivial protocol for use in testing where a L{Protocol} is expected. | ||
4462 | 137 | |||
4463 | 138 | @ivar instanceId: the id of this instance | ||
4464 | 139 | @ivar onConnLost: deferred that will fired when the connection is lost | ||
4465 | 140 | @ivar dataToSend: data to send on the protocol | ||
4466 | 141 | """ | ||
4467 | 142 | |||
4468 | 143 | instanceCount = 0 | ||
4469 | 144 | |||
4470 | 145 | def __init__(self, onConnLost, dataToSend): | ||
4471 | 146 | self.onConnLost = onConnLost | ||
4472 | 147 | self.dataToSend = dataToSend | ||
4473 | 148 | self.instanceId = TestProto.instanceCount | ||
4474 | 149 | TestProto.instanceCount = TestProto.instanceCount + 1 | ||
4475 | 150 | |||
4476 | 151 | def connectionMade(self): | ||
4477 | 152 | self.data = [] | ||
4478 | 153 | self.transport.write(self.dataToSend) | ||
4479 | 154 | |||
4480 | 155 | def dataReceived(self, bytes): | ||
4481 | 156 | self.data.append(bytes) | ||
4482 | 157 | |||
4483 | 158 | def connectionLost(self, reason): | ||
4484 | 159 | self.onConnLost.callback(self.data) | ||
4485 | 160 | |||
4486 | 161 | def __repr__(self): | ||
4487 | 162 | """ | ||
4488 | 163 | Custom repr for testing to avoid coupling amp tests with repr from | ||
4489 | 164 | L{Protocol} | ||
4490 | 165 | |||
4491 | 166 | Returns a string which contains a unique identifier that can be looked | ||
4492 | 167 | up using the instanceId property:: | ||
4493 | 168 | |||
4494 | 169 | <TestProto #3> | ||
4495 | 170 | """ | ||
4496 | 171 | return "<TestProto #%d>" % (self.instanceId,) | ||
4497 | 172 | |||
4498 | 173 | |||
4499 | 174 | class SimpleSymmetricProtocol(amp32.AMP): | ||
4500 | 175 | |||
4501 | 176 | def sendHello(self, text): | ||
4502 | 177 | return self.callRemoteString("hello", hello=text) | ||
4503 | 178 | |||
4504 | 179 | def amp_HELLO(self, box): | ||
4505 | 180 | return amp32.Box(hello=box['hello']) | ||
4506 | 181 | |||
4507 | 182 | def amp_HOWDOYOUDO(self, box): | ||
4508 | 183 | return amp32.QuitBox(howdoyoudo='world') | ||
4509 | 184 | |||
4510 | 185 | |||
4511 | 186 | class UnfriendlyGreeting(Exception): | ||
4512 | 187 | """Greeting was insufficiently kind. | ||
4513 | 188 | """ | ||
4514 | 189 | |||
4515 | 190 | |||
4516 | 191 | class DeathThreat(Exception): | ||
4517 | 192 | """Greeting was insufficiently kind. | ||
4518 | 193 | """ | ||
4519 | 194 | |||
4520 | 195 | |||
4521 | 196 | class UnknownProtocol(Exception): | ||
4522 | 197 | """Asked to switch to the wrong protocol. | ||
4523 | 198 | """ | ||
4524 | 199 | |||
4525 | 200 | |||
4526 | 201 | class TransportPeer(amp32.Argument): | ||
4527 | 202 | |||
4528 | 203 | # this serves as some informal documentation for how to get variables from | ||
4529 | 204 | # the protocol or your environment and pass them to methods as arguments. | ||
4530 | 205 | def retrieve(self, d, name, proto): | ||
4531 | 206 | return '' | ||
4532 | 207 | |||
4533 | 208 | def fromStringProto(self, notAString, proto): | ||
4534 | 209 | return proto.transport.getPeer() | ||
4535 | 210 | |||
4536 | 211 | def toBox(self, name, strings, objects, proto): | ||
4537 | 212 | return | ||
4538 | 213 | |||
4539 | 214 | |||
4540 | 215 | class Hello(amp32.Command): | ||
4541 | 216 | |||
4542 | 217 | commandName = 'hello' | ||
4543 | 218 | |||
4544 | 219 | arguments = [ | ||
4545 | 220 | ('hello', amp32.String()), | ||
4546 | 221 | ('optional', amp32.Boolean(optional=True)), | ||
4547 | 222 | ('print', amp32.Unicode(optional=True)), | ||
4548 | 223 | ('from', TransportPeer(optional=True)), | ||
4549 | 224 | ('mixedCase', amp32.String(optional=True)), | ||
4550 | 225 | ('dash-arg', amp32.String(optional=True)), | ||
4551 | 226 | ('underscore_arg', amp32.String(optional=True)), | ||
4552 | 227 | ] | ||
4553 | 228 | |||
4554 | 229 | response = [ | ||
4555 | 230 | ('hello', amp32.String()), | ||
4556 | 231 | ('print', amp32.Unicode(optional=True)), | ||
4557 | 232 | ] | ||
4558 | 233 | |||
4559 | 234 | errors = {UnfriendlyGreeting: 'UNFRIENDLY'} | ||
4560 | 235 | |||
4561 | 236 | fatalErrors = {DeathThreat: 'DEAD'} | ||
4562 | 237 | |||
4563 | 238 | |||
4564 | 239 | class NoAnswerHello(Hello): | ||
4565 | 240 | |||
4566 | 241 | commandName = Hello.commandName | ||
4567 | 242 | requiresAnswer = False | ||
4568 | 243 | |||
4569 | 244 | |||
4570 | 245 | class FutureHello(amp32.Command): | ||
4571 | 246 | |||
4572 | 247 | commandName = 'hello' | ||
4573 | 248 | arguments = [ | ||
4574 | 249 | ('hello', amp32.String()), | ||
4575 | 250 | ('optional', amp32.Boolean(optional=True)), | ||
4576 | 251 | ('print', amp32.Unicode(optional=True)), | ||
4577 | 252 | ('from', TransportPeer(optional=True)), | ||
4578 | 253 | # addt'l arguments should generally be added at the end, and be | ||
4579 | 254 | # optional... | ||
4580 | 255 | ('bonus', amp32.String(optional=True)), | ||
4581 | 256 | ] | ||
4582 | 257 | |||
4583 | 258 | response = [ | ||
4584 | 259 | ('hello', amp32.String()), | ||
4585 | 260 | ('print', amp32.Unicode(optional=True)), | ||
4586 | 261 | ] | ||
4587 | 262 | |||
4588 | 263 | errors = {UnfriendlyGreeting: 'UNFRIENDLY'} | ||
4589 | 264 | |||
4590 | 265 | |||
4591 | 266 | class WTF(amp32.Command): | ||
4592 | 267 | """ | ||
4593 | 268 | An example of an invalid command. | ||
4594 | 269 | """ | ||
4595 | 270 | |||
4596 | 271 | |||
4597 | 272 | class BrokenReturn(amp32.Command): | ||
4598 | 273 | """ | ||
4599 | 274 | An example of a perfectly good command, but the handler is going to return | ||
4600 | 275 | None... | ||
4601 | 276 | """ | ||
4602 | 277 | |||
4603 | 278 | commandName = 'broken_return' | ||
4604 | 279 | |||
4605 | 280 | |||
4606 | 281 | class Goodbye(amp32.Command): | ||
4607 | 282 | |||
4608 | 283 | # commandName left blank on purpose: this tests implicit command names. | ||
4609 | 284 | response = [('goodbye', amp32.String())] | ||
4610 | 285 | responseType = amp32.QuitBox | ||
4611 | 286 | |||
4612 | 287 | |||
4613 | 288 | class Howdoyoudo(amp32.Command): | ||
4614 | 289 | |||
4615 | 290 | commandName = 'howdoyoudo' | ||
4616 | 291 | # responseType = amp32.QuitBox | ||
4617 | 292 | |||
4618 | 293 | |||
4619 | 294 | class WaitForever(amp32.Command): | ||
4620 | 295 | |||
4621 | 296 | commandName = 'wait_forever' | ||
4622 | 297 | |||
4623 | 298 | |||
4624 | 299 | class GetList(amp32.Command): | ||
4625 | 300 | |||
4626 | 301 | commandName = 'getlist' | ||
4627 | 302 | arguments = [('length', amp32.Integer())] | ||
4628 | 303 | response = [('body', amp32.AmpList([('x', amp32.Integer())]))] | ||
4629 | 304 | |||
4630 | 305 | |||
4631 | 306 | class DontRejectMe(amp32.Command): | ||
4632 | 307 | |||
4633 | 308 | commandName = 'dontrejectme' | ||
4634 | 309 | arguments = [ | ||
4635 | 310 | ('magicWord', amp32.Unicode()), | ||
4636 | 311 | ('list', amp32.AmpList([('name', amp32.Unicode())], optional=True)), | ||
4637 | 312 | ] | ||
4638 | 313 | response = [('response', amp32.Unicode())] | ||
4639 | 314 | |||
4640 | 315 | |||
4641 | 316 | class SecuredPing(amp32.Command): | ||
4642 | 317 | |||
4643 | 318 | # XXX TODO: actually make this refuse to send over an insecure connection | ||
4644 | 319 | response = [('pinged', amp32.Boolean())] | ||
4645 | 320 | |||
4646 | 321 | |||
4647 | 322 | class TestSwitchProto(amp32.ProtocolSwitchCommand): | ||
4648 | 323 | |||
4649 | 324 | commandName = 'Switch-Proto' | ||
4650 | 325 | arguments = [ | ||
4651 | 326 | ('name', amp32.String()), | ||
4652 | 327 | ] | ||
4653 | 328 | errors = {UnknownProtocol: 'UNKNOWN'} | ||
4654 | 329 | |||
4655 | 330 | |||
4656 | 331 | class SingleUseFactory(protocol.ClientFactory): | ||
4657 | 332 | |||
4658 | 333 | def __init__(self, proto): | ||
4659 | 334 | self.proto = proto | ||
4660 | 335 | self.proto.factory = self | ||
4661 | 336 | |||
4662 | 337 | def buildProtocol(self, addr): | ||
4663 | 338 | p, self.proto = self.proto, None | ||
4664 | 339 | return p | ||
4665 | 340 | |||
4666 | 341 | reasonFailed = None | ||
4667 | 342 | |||
4668 | 343 | def clientConnectionFailed(self, connector, reason): | ||
4669 | 344 | self.reasonFailed = reason | ||
4670 | 345 | return | ||
4671 | 346 | |||
4672 | 347 | |||
4673 | 348 | THING_I_DONT_UNDERSTAND = 'gwebol nargo' | ||
4674 | 349 | |||
4675 | 350 | |||
4676 | 351 | class ThingIDontUnderstandError(Exception): | ||
4677 | 352 | pass | ||
4678 | 353 | |||
4679 | 354 | |||
4680 | 355 | class FactoryNotifier(amp32.AMP): | ||
4681 | 356 | |||
4682 | 357 | factory = None | ||
4683 | 358 | |||
4684 | 359 | def connectionMade(self): | ||
4685 | 360 | if self.factory is not None: | ||
4686 | 361 | self.factory.theProto = self | ||
4687 | 362 | if hasattr(self.factory, 'onMade'): | ||
4688 | 363 | self.factory.onMade.callback(None) | ||
4689 | 364 | |||
4690 | 365 | @SecuredPing.responder | ||
4691 | 366 | def emitpong(self): | ||
4692 | 367 | from twisted.internet.interfaces import ISSLTransport | ||
4693 | 368 | if not ISSLTransport.providedBy(self.transport): | ||
4694 | 369 | raise DeathThreat("only send secure pings over secure channels") | ||
4695 | 370 | return {'pinged': True} | ||
4696 | 371 | |||
4697 | 372 | |||
4698 | 373 | class SimpleSymmetricCommandProtocol(FactoryNotifier): | ||
4699 | 374 | |||
4700 | 375 | maybeLater = None | ||
4701 | 376 | |||
4702 | 377 | def __init__(self, onConnLost=None): | ||
4703 | 378 | amp32.AMP.__init__(self) | ||
4704 | 379 | self.onConnLost = onConnLost | ||
4705 | 380 | |||
4706 | 381 | def sendHello(self, text): | ||
4707 | 382 | return self.callRemote(Hello, hello=text) | ||
4708 | 383 | |||
4709 | 384 | def sendUnicodeHello(self, text, translation): | ||
4710 | 385 | return self.callRemote(Hello, hello=text, Print=translation) | ||
4711 | 386 | |||
4712 | 387 | greeted = False | ||
4713 | 388 | |||
4714 | 389 | @Hello.responder | ||
4715 | 390 | def cmdHello(self, hello, From, optional=None, Print=None, | ||
4716 | 391 | mixedCase=None, dash_arg=None, underscore_arg=None): | ||
4717 | 392 | assert From == self.transport.getPeer() | ||
4718 | 393 | if hello == THING_I_DONT_UNDERSTAND: | ||
4719 | 394 | raise ThingIDontUnderstandError() | ||
4720 | 395 | if hello.startswith('fuck'): | ||
4721 | 396 | raise UnfriendlyGreeting("Don't be a dick.") | ||
4722 | 397 | if hello == 'die': | ||
4723 | 398 | raise DeathThreat("aieeeeeeeee") | ||
4724 | 399 | result = dict(hello=hello) | ||
4725 | 400 | if Print is not None: | ||
4726 | 401 | result.update(dict(Print=Print)) | ||
4727 | 402 | self.greeted = True | ||
4728 | 403 | return result | ||
4729 | 404 | |||
4730 | 405 | @GetList.responder | ||
4731 | 406 | def cmdGetlist(self, length): | ||
4732 | 407 | return {'body': [dict(x=1)] * length} | ||
4733 | 408 | |||
4734 | 409 | @DontRejectMe.responder | ||
4735 | 410 | def okiwont(self, magicWord, list=None): | ||
4736 | 411 | if list is None: | ||
4737 | 412 | response = u'list omitted' | ||
4738 | 413 | else: | ||
4739 | 414 | response = u'%s accepted' % (list[0]['name']) | ||
4740 | 415 | return dict(response=response) | ||
4741 | 416 | |||
4742 | 417 | @WaitForever.responder | ||
4743 | 418 | def waitforit(self): | ||
4744 | 419 | self.waiting = defer.Deferred() | ||
4745 | 420 | return self.waiting | ||
4746 | 421 | |||
4747 | 422 | @Howdoyoudo.responder | ||
4748 | 423 | def howdo(self): | ||
4749 | 424 | return dict(howdoyoudo='world') | ||
4750 | 425 | |||
4751 | 426 | @Goodbye.responder | ||
4752 | 427 | def saybye(self): | ||
4753 | 428 | return dict(goodbye="everyone") | ||
4754 | 429 | |||
4755 | 430 | def switchToTestProtocol(self, fail=False): | ||
4756 | 431 | if fail: | ||
4757 | 432 | name = 'no-proto' | ||
4758 | 433 | else: | ||
4759 | 434 | name = 'test-proto' | ||
4760 | 435 | p = TestProto(self.onConnLost, SWITCH_CLIENT_DATA) | ||
4761 | 436 | return self.callRemote( | ||
4762 | 437 | TestSwitchProto, | ||
4763 | 438 | SingleUseFactory(p), name=name).addCallback(lambda ign: p) | ||
4764 | 439 | |||
4765 | 440 | @TestSwitchProto.responder | ||
4766 | 441 | def switchit(self, name): | ||
4767 | 442 | if name == 'test-proto': | ||
4768 | 443 | return TestProto(self.onConnLost, SWITCH_SERVER_DATA) | ||
4769 | 444 | raise UnknownProtocol(name) | ||
4770 | 445 | |||
4771 | 446 | @BrokenReturn.responder | ||
4772 | 447 | def donothing(self): | ||
4773 | 448 | return None | ||
4774 | 449 | |||
4775 | 450 | |||
4776 | 451 | class DeferredSymmetricCommandProtocol(SimpleSymmetricCommandProtocol): | ||
4777 | 452 | |||
4778 | 453 | @TestSwitchProto.responder | ||
4779 | 454 | def switchit(self, name): | ||
4780 | 455 | if name == 'test-proto': | ||
4781 | 456 | self.maybeLaterProto = TestProto( | ||
4782 | 457 | self.onConnLost, SWITCH_SERVER_DATA) | ||
4783 | 458 | self.maybeLater = defer.Deferred() | ||
4784 | 459 | return self.maybeLater | ||
4785 | 460 | raise UnknownProtocol(name) | ||
4786 | 461 | |||
4787 | 462 | |||
4788 | 463 | class BadNoAnswerCommandProtocol(SimpleSymmetricCommandProtocol): | ||
4789 | 464 | |||
4790 | 465 | @NoAnswerHello.responder | ||
4791 | 466 | def badResponder( | ||
4792 | 467 | self, hello, From, optional=None, Print=None, mixedCase=None, | ||
4793 | 468 | dash_arg=None, underscore_arg=None): | ||
4794 | 469 | """ | ||
4795 | 470 | This responder does nothing and forgets to return a dictionary. | ||
4796 | 471 | """ | ||
4797 | 472 | |||
4798 | 473 | |||
4799 | 474 | class NoAnswerCommandProtocol(SimpleSymmetricCommandProtocol): | ||
4800 | 475 | |||
4801 | 476 | @NoAnswerHello.responder | ||
4802 | 477 | def goodNoAnswerResponder( | ||
4803 | 478 | self, hello, From, optional=None, Print=None, | ||
4804 | 479 | mixedCase=None, dash_arg=None, underscore_arg=None): | ||
4805 | 480 | return dict(hello=hello + "-noanswer") | ||
4806 | 481 | |||
4807 | 482 | |||
4808 | 483 | def connectedServerAndClient( | ||
4809 | 484 | ServerClass=SimpleSymmetricProtocol, | ||
4810 | 485 | ClientClass=SimpleSymmetricProtocol, | ||
4811 | 486 | *a, **kw): | ||
4812 | 487 | """Returns a 3-tuple: (client, server, pump) | ||
4813 | 488 | """ | ||
4814 | 489 | return iosim.connectedServerAndClient( | ||
4815 | 490 | ServerClass, ClientClass, | ||
4816 | 491 | *a, **kw) | ||
4817 | 492 | |||
4818 | 493 | |||
4819 | 494 | class TotallyDumbProtocol(protocol.Protocol): | ||
4820 | 495 | |||
4821 | 496 | buf = '' | ||
4822 | 497 | |||
4823 | 498 | def dataReceived(self, data): | ||
4824 | 499 | self.buf += data | ||
4825 | 500 | |||
4826 | 501 | |||
4827 | 502 | class LiteralAmp(amp32.AMP): | ||
4828 | 503 | |||
4829 | 504 | def __init__(self): | ||
4830 | 505 | self.boxes = [] | ||
4831 | 506 | |||
4832 | 507 | def ampBoxReceived(self, box): | ||
4833 | 508 | self.boxes.append(box) | ||
4834 | 509 | return | ||
4835 | 510 | |||
4836 | 511 | |||
4837 | 512 | class AmpBoxTests(AMP32TestCase): | ||
4838 | 513 | """ | ||
4839 | 514 | Test a few essential properties of AMP boxes, mostly with respect to | ||
4840 | 515 | serialization correctness. | ||
4841 | 516 | """ | ||
4842 | 517 | |||
4843 | 518 | def test_serializeStr(self): | ||
4844 | 519 | """ | ||
4845 | 520 | Make sure that strs serialize to strs. | ||
4846 | 521 | """ | ||
4847 | 522 | a = amp32.AmpBox(key='value') | ||
4848 | 523 | self.assertEqual(type(a.serialize()), bytes) | ||
4849 | 524 | |||
4850 | 525 | def test_serializeUnicodeKeyRaises(self): | ||
4851 | 526 | """ | ||
4852 | 527 | Verify that TypeError is raised when trying to serialize Unicode keys. | ||
4853 | 528 | """ | ||
4854 | 529 | a = amp32.AmpBox(**{u'key': 'value'}) | ||
4855 | 530 | self.assertRaises(TypeError, a.serialize) | ||
4856 | 531 | |||
4857 | 532 | def test_serializeUnicodeValueRaises(self): | ||
4858 | 533 | """ | ||
4859 | 534 | Verify that TypeError is raised when trying to serialize Unicode | ||
4860 | 535 | values. | ||
4861 | 536 | """ | ||
4862 | 537 | a = amp32.AmpBox(key=u'value') | ||
4863 | 538 | self.assertRaises(TypeError, a.serialize) | ||
4864 | 539 | |||
4865 | 540 | |||
4866 | 541 | class ParsingTest(AMP32TestCase): | ||
4867 | 542 | |||
4868 | 543 | def test_booleanValues(self): | ||
4869 | 544 | """ | ||
4870 | 545 | Verify that the Boolean parser parses 'True' and 'False', but nothing | ||
4871 | 546 | else. | ||
4872 | 547 | """ | ||
4873 | 548 | b = amp32.Boolean() | ||
4874 | 549 | self.assertEqual(b.fromString("True"), True) | ||
4875 | 550 | self.assertEqual(b.fromString("False"), False) | ||
4876 | 551 | self.assertRaises(TypeError, b.fromString, "ninja") | ||
4877 | 552 | self.assertRaises(TypeError, b.fromString, "true") | ||
4878 | 553 | self.assertRaises(TypeError, b.fromString, "TRUE") | ||
4879 | 554 | self.assertEqual(b.toString(True), 'True') | ||
4880 | 555 | self.assertEqual(b.toString(False), 'False') | ||
4881 | 556 | |||
4882 | 557 | def test_pathValueRoundTrip(self): | ||
4883 | 558 | """ | ||
4884 | 559 | Verify the 'Path' argument can parse and emit a file path. | ||
4885 | 560 | """ | ||
4886 | 561 | with TempDirectory() as tempdir: | ||
4887 | 562 | fp = filepath.FilePath(tempdir.path) | ||
4888 | 563 | p = amp32.Path() | ||
4889 | 564 | s = p.toString(fp) | ||
4890 | 565 | v = p.fromString(s) | ||
4891 | 566 | self.assertIsNot(fp, v) # sanity check | ||
4892 | 567 | self.assertEqual(fp, v) | ||
4893 | 568 | |||
4894 | 569 | def test_sillyEmptyThing(self): | ||
4895 | 570 | """ | ||
4896 | 571 | Test that empty boxes raise an error; they aren't supposed to be sent | ||
4897 | 572 | on purpose. | ||
4898 | 573 | """ | ||
4899 | 574 | a = amp32.AMP() | ||
4900 | 575 | return self.assertRaises( | ||
4901 | 576 | amp32.NoEmptyBoxes, a.ampBoxReceived, amp32.Box()) | ||
4902 | 577 | |||
4903 | 578 | def test_ParsingRoundTrip(self): | ||
4904 | 579 | """ | ||
4905 | 580 | Verify that various kinds of data make it through the encode/parse | ||
4906 | 581 | round-trip unharmed. | ||
4907 | 582 | """ | ||
4908 | 583 | c, s, p = connectedServerAndClient( | ||
4909 | 584 | ClientClass=LiteralAmp, ServerClass=LiteralAmp) | ||
4910 | 585 | |||
4911 | 586 | SIMPLE = ('simple', 'test') | ||
4912 | 587 | CE = ('ceq', ': ') | ||
4913 | 588 | CR = ('crtest', 'test\r') | ||
4914 | 589 | LF = ('lftest', 'hello\n') | ||
4915 | 590 | NEWLINE = ('newline', 'test\r\none\r\ntwo') | ||
4916 | 591 | NEWLINE2 = ('newline2', 'test\r\none\r\n two') | ||
4917 | 592 | BODYTEST = ('body', 'blah\r\n\r\ntesttest') | ||
4918 | 593 | |||
4919 | 594 | testData = [ | ||
4920 | 595 | [SIMPLE], | ||
4921 | 596 | [SIMPLE, BODYTEST], | ||
4922 | 597 | [SIMPLE, CE], | ||
4923 | 598 | [SIMPLE, CR], | ||
4924 | 599 | [SIMPLE, CE, CR, LF], | ||
4925 | 600 | [CE, CR, LF], | ||
4926 | 601 | [SIMPLE, NEWLINE, CE, NEWLINE2], | ||
4927 | 602 | [BODYTEST, SIMPLE, NEWLINE] | ||
4928 | 603 | ] | ||
4929 | 604 | |||
4930 | 605 | for test in testData: | ||
4931 | 606 | jb = amp32.Box() | ||
4932 | 607 | jb.update(dict(test)) | ||
4933 | 608 | jb._sendTo(c) | ||
4934 | 609 | p.flush() | ||
4935 | 610 | self.assertEqual(s.boxes[-1], jb) | ||
4936 | 611 | |||
4937 | 612 | |||
4938 | 613 | class FakeLocator(object): | ||
4939 | 614 | """ | ||
4940 | 615 | This is a fake implementation of the interface implied by | ||
4941 | 616 | L{CommandLocator}. | ||
4942 | 617 | """ | ||
4943 | 618 | |||
4944 | 619 | def __init__(self): | ||
4945 | 620 | """ | ||
4946 | 621 | Remember the given keyword arguments as a set of responders. | ||
4947 | 622 | """ | ||
4948 | 623 | self.commands = {} | ||
4949 | 624 | |||
4950 | 625 | def locateResponder(self, commandName): | ||
4951 | 626 | """ | ||
4952 | 627 | Look up and return a function passed as a keyword argument of the given | ||
4953 | 628 | name to the constructor. | ||
4954 | 629 | """ | ||
4955 | 630 | return self.commands[commandName] | ||
4956 | 631 | |||
4957 | 632 | |||
4958 | 633 | class FakeSender: | ||
4959 | 634 | """ | ||
4960 | 635 | This is a fake implementation of the 'box sender' interface implied by | ||
4961 | 636 | L{AMP}. | ||
4962 | 637 | """ | ||
4963 | 638 | |||
4964 | 639 | def __init__(self): | ||
4965 | 640 | """ | ||
4966 | 641 | Create a fake sender and initialize the list of received boxes and | ||
4967 | 642 | unhandled errors. | ||
4968 | 643 | """ | ||
4969 | 644 | self.sentBoxes = [] | ||
4970 | 645 | self.unhandledErrors = [] | ||
4971 | 646 | self.expectedErrors = 0 | ||
4972 | 647 | |||
4973 | 648 | def expectError(self): | ||
4974 | 649 | """ | ||
4975 | 650 | Expect one error, so that the test doesn't fail. | ||
4976 | 651 | """ | ||
4977 | 652 | self.expectedErrors += 1 | ||
4978 | 653 | |||
4979 | 654 | def sendBox(self, box): | ||
4980 | 655 | """ | ||
4981 | 656 | Accept a box, but don't do anything. | ||
4982 | 657 | """ | ||
4983 | 658 | self.sentBoxes.append(box) | ||
4984 | 659 | |||
4985 | 660 | def unhandledError(self, failure): | ||
4986 | 661 | """ | ||
4987 | 662 | Deal with failures by instantly re-raising them for easier debugging. | ||
4988 | 663 | """ | ||
4989 | 664 | self.expectedErrors -= 1 | ||
4990 | 665 | if self.expectedErrors < 0: | ||
4991 | 666 | failure.raiseException() | ||
4992 | 667 | else: | ||
4993 | 668 | self.unhandledErrors.append(failure) | ||
4994 | 669 | |||
4995 | 670 | |||
4996 | 671 | class CommandDispatchTests(AMP32TestCase): | ||
4997 | 672 | """ | ||
4998 | 673 | The AMP CommandDispatcher class dispatches converts AMP boxes into commands | ||
4999 | 674 | and responses using Command.responder decorator. | ||
5000 | 675 |
Hmm, do we really want to copy the code wholesale instead of changing AMT upstream to be fixed, parametrizable or monkey-patchable?