Merge lp:~allenap/maas/api-uri-bug-1059645-for-1.2 into lp:maas/1.2
- api-uri-bug-1059645-for-1.2
- Merge into 1.2
Proposed by
Gavin Panella
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1281 |
Proposed branch: | lp:~allenap/maas/api-uri-bug-1059645-for-1.2 |
Merge into: | lp:maas/1.2 |
Diff against target: |
332 lines (+163/-16) 6 files modified
src/maasserver/api.py (+20/-4) src/maasserver/apidoc.py (+3/-8) src/maasserver/tests/test_api.py (+55/-0) src/maasserver/tests/test_apidoc.py (+4/-4) src/maasserver/utils/__init__.py (+18/-0) src/maasserver/utils/tests/test_utils.py (+63/-0) |
To merge this branch: | bzr merge lp:~allenap/maas/api-uri-bug-1059645-for-1.2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+132668@code.launchpad.net |
Commit message
Backport of lp:maas revision 1324:
In the API description, make handler URIs absolute with respect to the request.
Previously, DEFAULT_MAAS_URL was always used.
Description of the change
To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api.py' | |||
2 | --- src/maasserver/api.py 2012-10-30 15:15:30 +0000 | |||
3 | +++ src/maasserver/api.py 2012-11-02 09:52:23 +0000 | |||
4 | @@ -164,6 +164,7 @@ | |||
5 | 164 | from maasserver.server_address import get_maas_facing_server_address | 164 | from maasserver.server_address import get_maas_facing_server_address |
6 | 165 | from maasserver.utils import ( | 165 | from maasserver.utils import ( |
7 | 166 | absolute_reverse, | 166 | absolute_reverse, |
8 | 167 | build_absolute_uri, | ||
9 | 167 | map_enum, | 168 | map_enum, |
10 | 168 | strip_domain, | 169 | strip_domain, |
11 | 169 | ) | 170 | ) |
12 | @@ -967,6 +968,10 @@ | |||
13 | 967 | 968 | ||
14 | 968 | get = operation(idempotent=True, exported_as='get')(get_file) | 969 | get = operation(idempotent=True, exported_as='get')(get_file) |
15 | 969 | 970 | ||
16 | 971 | @classmethod | ||
17 | 972 | def resource_uri(cls, *args, **kwargs): | ||
18 | 973 | return ('files_handler', []) | ||
19 | 974 | |||
20 | 970 | 975 | ||
21 | 971 | class FilesHandler(OperationsHandler): | 976 | class FilesHandler(OperationsHandler): |
22 | 972 | """File management operations.""" | 977 | """File management operations.""" |
23 | @@ -1841,12 +1846,23 @@ | |||
24 | 1841 | Returns a JSON object describing the whole MAAS API. | 1846 | Returns a JSON object describing the whole MAAS API. |
25 | 1842 | """ | 1847 | """ |
26 | 1843 | from maasserver import urls_api as urlconf | 1848 | from maasserver import urls_api as urlconf |
27 | 1849 | resources = [ | ||
28 | 1850 | describe_resource(resource) | ||
29 | 1851 | for resource in find_api_resources(urlconf) | ||
30 | 1852 | ] | ||
31 | 1853 | # Make all URIs absolute. Clients - maas-cli in particular - expect that | ||
32 | 1854 | # all handler URIs are absolute, not just paths. The handler URIs returned | ||
33 | 1855 | # by describe_resource() are relative paths. | ||
34 | 1856 | absolute = partial(build_absolute_uri, request) | ||
35 | 1857 | for resource in resources: | ||
36 | 1858 | for handler_type in "anon", "auth": | ||
37 | 1859 | handler = resource[handler_type] | ||
38 | 1860 | if handler is not None: | ||
39 | 1861 | handler["uri"] = absolute(handler["path"]) | ||
40 | 1862 | # Package it all up. | ||
41 | 1844 | description = { | 1863 | description = { |
42 | 1845 | "doc": "MAAS API", | 1864 | "doc": "MAAS API", |
47 | 1846 | "resources": [ | 1865 | "resources": resources, |
44 | 1847 | describe_resource(resource) | ||
45 | 1848 | for resource in find_api_resources(urlconf) | ||
46 | 1849 | ], | ||
48 | 1850 | } | 1866 | } |
49 | 1851 | # For backward compatibility, add "handlers" as an alias for all not-None | 1867 | # For backward compatibility, add "handlers" as an alias for all not-None |
50 | 1852 | # anon and auth handlers in "resources". | 1868 | # anon and auth handlers in "resources". |
51 | 1853 | 1869 | ||
52 | === modified file 'src/maasserver/apidoc.py' | |||
53 | --- src/maasserver/apidoc.py 2012-10-06 22:42:45 +0000 | |||
54 | +++ src/maasserver/apidoc.py 2012-11-02 09:52:23 +0000 | |||
55 | @@ -19,9 +19,7 @@ | |||
56 | 19 | 19 | ||
57 | 20 | from inspect import getdoc | 20 | from inspect import getdoc |
58 | 21 | from itertools import izip_longest | 21 | from itertools import izip_longest |
59 | 22 | from urlparse import urljoin | ||
60 | 23 | 22 | ||
61 | 24 | from django.conf import settings | ||
62 | 25 | from django.core.urlresolvers import ( | 23 | from django.core.urlresolvers import ( |
63 | 26 | get_resolver, | 24 | get_resolver, |
64 | 27 | RegexURLPattern, | 25 | RegexURLPattern, |
65 | @@ -137,11 +135,8 @@ | |||
66 | 137 | if isinstance(handler, BaseHandler): | 135 | if isinstance(handler, BaseHandler): |
67 | 138 | handler = type(handler) | 136 | handler = type(handler) |
68 | 139 | 137 | ||
74 | 140 | uri_template = generate_doc(handler).resource_uri_template | 138 | path = generate_doc(handler).resource_uri_template |
75 | 141 | if uri_template is None: | 139 | path = "" if path is None else path |
71 | 142 | uri_template = settings.DEFAULT_MAAS_URL | ||
72 | 143 | else: | ||
73 | 144 | uri_template = urljoin(settings.DEFAULT_MAAS_URL, uri_template) | ||
76 | 145 | 140 | ||
77 | 146 | resource_uri = getattr(handler, "resource_uri", lambda: ()) | 141 | resource_uri = getattr(handler, "resource_uri", lambda: ()) |
78 | 147 | view_name, uri_params, uri_kw = merge(resource_uri(), (None, (), {})) | 142 | view_name, uri_params, uri_kw = merge(resource_uri(), (None, (), {})) |
79 | @@ -154,7 +149,7 @@ | |||
80 | 154 | "doc": getdoc(handler), | 149 | "doc": getdoc(handler), |
81 | 155 | "name": handler.__name__, | 150 | "name": handler.__name__, |
82 | 156 | "params": uri_params, | 151 | "params": uri_params, |
84 | 157 | "uri": uri_template, | 152 | "path": path, |
85 | 158 | } | 153 | } |
86 | 159 | 154 | ||
87 | 160 | 155 | ||
88 | 161 | 156 | ||
89 | === modified file 'src/maasserver/tests/test_api.py' | |||
90 | --- src/maasserver/tests/test_api.py 2012-10-30 15:15:30 +0000 | |||
91 | +++ src/maasserver/tests/test_api.py 2012-11-02 09:52:23 +0000 | |||
92 | @@ -27,10 +27,12 @@ | |||
93 | 27 | import httplib | 27 | import httplib |
94 | 28 | from itertools import izip | 28 | from itertools import izip |
95 | 29 | import json | 29 | import json |
96 | 30 | from operator import itemgetter | ||
97 | 30 | import os | 31 | import os |
98 | 31 | import random | 32 | import random |
99 | 32 | import shutil | 33 | import shutil |
100 | 33 | import sys | 34 | import sys |
101 | 35 | from urlparse import urlparse | ||
102 | 34 | 36 | ||
103 | 35 | from apiclient.maas_client import MAASClient | 37 | from apiclient.maas_client import MAASClient |
104 | 36 | from celery.app import app_or_default | 38 | from celery.app import app_or_default |
105 | @@ -38,9 +40,11 @@ | |||
106 | 38 | from django.contrib.auth.models import AnonymousUser | 40 | from django.contrib.auth.models import AnonymousUser |
107 | 39 | from django.core.urlresolvers import reverse | 41 | from django.core.urlresolvers import reverse |
108 | 40 | from django.http import QueryDict | 42 | from django.http import QueryDict |
109 | 43 | from django.test.client import RequestFactory | ||
110 | 41 | from fixtures import Fixture | 44 | from fixtures import Fixture |
111 | 42 | from maasserver import api | 45 | from maasserver import api |
112 | 43 | from maasserver.api import ( | 46 | from maasserver.api import ( |
113 | 47 | describe, | ||
114 | 44 | DISPLAYED_NODEGROUP_FIELDS, | 48 | DISPLAYED_NODEGROUP_FIELDS, |
115 | 45 | extract_constraints, | 49 | extract_constraints, |
116 | 46 | extract_oauth_key, | 50 | extract_oauth_key, |
117 | @@ -130,10 +134,15 @@ | |||
118 | 130 | from provisioningserver.pxe import tftppath | 134 | from provisioningserver.pxe import tftppath |
119 | 131 | from provisioningserver.testing.boot_images import make_boot_image_params | 135 | from provisioningserver.testing.boot_images import make_boot_image_params |
120 | 132 | from testresources import FixtureResource | 136 | from testresources import FixtureResource |
121 | 137 | from testscenarios import multiply_scenarios | ||
122 | 133 | from testtools.matchers import ( | 138 | from testtools.matchers import ( |
123 | 139 | AfterPreprocessing, | ||
124 | 134 | AllMatch, | 140 | AllMatch, |
125 | 135 | Contains, | 141 | Contains, |
126 | 136 | Equals, | 142 | Equals, |
127 | 143 | Is, | ||
128 | 144 | MatchesAll, | ||
129 | 145 | MatchesAny, | ||
130 | 137 | MatchesListwise, | 146 | MatchesListwise, |
131 | 138 | MatchesStructure, | 147 | MatchesStructure, |
132 | 139 | StartsWith, | 148 | StartsWith, |
133 | @@ -4192,3 +4201,49 @@ | |||
134 | 4192 | self.assertSetEqual( | 4201 | self.assertSetEqual( |
135 | 4193 | {"doc", "handlers", "resources"}, set(description)) | 4202 | {"doc", "handlers", "resources"}, set(description)) |
136 | 4194 | self.assertIsInstance(description["handlers"], list) | 4203 | self.assertIsInstance(description["handlers"], list) |
137 | 4204 | |||
138 | 4205 | |||
139 | 4206 | class TestDescribeAbsoluteURIs(AnonAPITestCase): | ||
140 | 4207 | """Tests for the `describe` view's URI manipulation.""" | ||
141 | 4208 | |||
142 | 4209 | scenarios_schemes = ( | ||
143 | 4210 | ("http", dict(scheme="http")), | ||
144 | 4211 | ("https", dict(scheme="https")), | ||
145 | 4212 | ) | ||
146 | 4213 | |||
147 | 4214 | scenarios_paths = ( | ||
148 | 4215 | ("script-at-root", dict(script_name="", path_info="")), | ||
149 | 4216 | ("script-below-root-1", dict(script_name="/foo/bar", path_info="")), | ||
150 | 4217 | ("script-below-root-2", dict(script_name="/foo", path_info="/bar")), | ||
151 | 4218 | ) | ||
152 | 4219 | |||
153 | 4220 | scenarios = multiply_scenarios( | ||
154 | 4221 | scenarios_schemes, scenarios_paths) | ||
155 | 4222 | |||
156 | 4223 | def test_handler_uris_are_absolute(self): | ||
157 | 4224 | server = factory.make_name("server").lower() | ||
158 | 4225 | extra = { | ||
159 | 4226 | "PATH_INFO": self.path_info, | ||
160 | 4227 | "SCRIPT_NAME": self.script_name, | ||
161 | 4228 | "SERVER_NAME": server, | ||
162 | 4229 | "wsgi.url_scheme": self.scheme, | ||
163 | 4230 | } | ||
164 | 4231 | request = RequestFactory().get( | ||
165 | 4232 | "/%s/describe" % factory.make_name("path"), **extra) | ||
166 | 4233 | response = describe(request) | ||
167 | 4234 | self.assertEqual(httplib.OK, response.status_code, response.content) | ||
168 | 4235 | description = json.loads(response.content) | ||
169 | 4236 | expected_uri = AfterPreprocessing( | ||
170 | 4237 | urlparse, MatchesStructure( | ||
171 | 4238 | scheme=Equals(self.scheme), hostname=Equals(server), | ||
172 | 4239 | # The path is always the script name followed by "api/" | ||
173 | 4240 | # because all API calls are within the "api" tree. | ||
174 | 4241 | path=StartsWith(self.script_name + "/api/"))) | ||
175 | 4242 | expected_handler = MatchesAny( | ||
176 | 4243 | Is(None), AfterPreprocessing(itemgetter("uri"), expected_uri)) | ||
177 | 4244 | expected_resource = MatchesAll( | ||
178 | 4245 | AfterPreprocessing(itemgetter("anon"), expected_handler), | ||
179 | 4246 | AfterPreprocessing(itemgetter("auth"), expected_handler)) | ||
180 | 4247 | resources = description["resources"] | ||
181 | 4248 | self.assertNotEqual([], resources) | ||
182 | 4249 | self.assertThat(resources, AllMatch(expected_resource)) | ||
183 | 4195 | 4250 | ||
184 | === modified file 'src/maasserver/tests/test_apidoc.py' | |||
185 | --- src/maasserver/tests/test_apidoc.py 2012-10-08 10:23:16 +0000 | |||
186 | +++ src/maasserver/tests/test_apidoc.py 2012-11-02 09:52:23 +0000 | |||
187 | @@ -203,7 +203,7 @@ | |||
188 | 203 | observed = describe_handler(ExampleHandler) | 203 | observed = describe_handler(ExampleHandler) |
189 | 204 | # The description contains several entries. | 204 | # The description contains several entries. |
190 | 205 | self.assertSetEqual( | 205 | self.assertSetEqual( |
192 | 206 | {"actions", "doc", "name", "params", "uri"}, | 206 | {"actions", "doc", "name", "params", "path"}, |
193 | 207 | set(observed)) | 207 | set(observed)) |
194 | 208 | self.assertEqual(ExampleHandler.__doc__, observed["doc"]) | 208 | self.assertEqual(ExampleHandler.__doc__, observed["doc"]) |
195 | 209 | self.assertEqual(ExampleHandler.__name__, observed["name"]) | 209 | self.assertEqual(ExampleHandler.__name__, observed["name"]) |
196 | @@ -231,11 +231,11 @@ | |||
197 | 231 | } | 231 | } |
198 | 232 | self.assertSetEqual(expected_actions, observed_actions) | 232 | self.assertSetEqual(expected_actions, observed_actions) |
199 | 233 | self.assertSetEqual({"system_id"}, set(description["params"])) | 233 | self.assertSetEqual({"system_id"}, set(description["params"])) |
201 | 234 | # The URI is a URI Template <http://tools.ietf.org/html/rfc6570>, the | 234 | # The path is a URI Template <http://tools.ietf.org/html/rfc6570>, the |
202 | 235 | # components of which correspond to the parameters declared. | 235 | # components of which correspond to the parameters declared. |
203 | 236 | self.assertEqual( | 236 | self.assertEqual( |
206 | 237 | "http://example.com/api/1.0/nodes/{system_id}/", | 237 | "/api/1.0/nodes/{system_id}/", |
207 | 238 | description["uri"]) | 238 | description["path"]) |
208 | 239 | 239 | ||
209 | 240 | def test_describe_resource_anonymous_resource(self): | 240 | def test_describe_resource_anonymous_resource(self): |
210 | 241 | # When the resource does not require authentication, any configured | 241 | # When the resource does not require authentication, any configured |
211 | 242 | 242 | ||
212 | === modified file 'src/maasserver/utils/__init__.py' | |||
213 | --- src/maasserver/utils/__init__.py 2012-10-30 15:15:30 +0000 | |||
214 | +++ src/maasserver/utils/__init__.py 2012-11-02 09:52:23 +0000 | |||
215 | @@ -12,6 +12,7 @@ | |||
216 | 12 | __metaclass__ = type | 12 | __metaclass__ = type |
217 | 13 | __all__ = [ | 13 | __all__ = [ |
218 | 14 | 'absolute_reverse', | 14 | 'absolute_reverse', |
219 | 15 | 'build_absolute_uri', | ||
220 | 15 | 'get_db_state', | 16 | 'get_db_state', |
221 | 16 | 'ignore_unused', | 17 | 'ignore_unused', |
222 | 17 | 'map_enum', | 18 | 'map_enum', |
223 | @@ -85,6 +86,23 @@ | |||
224 | 85 | return url | 86 | return url |
225 | 86 | 87 | ||
226 | 87 | 88 | ||
227 | 89 | def build_absolute_uri(request, path): | ||
228 | 90 | """Given a full app-relative path, returns an absolute URI. | ||
229 | 91 | |||
230 | 92 | The path ordinarily starts with a forward-slash... imagine that you've | ||
231 | 93 | magically chroot'ed to your Django application; the path is the absolute | ||
232 | 94 | path to the view within that environment. | ||
233 | 95 | |||
234 | 96 | The URI returned uses the request to figure out how to make an absolute | ||
235 | 97 | URL. This means that the URI returned will use the same IP address or | ||
236 | 98 | alias that the request came in on. | ||
237 | 99 | """ | ||
238 | 100 | script_name = request.META["SCRIPT_NAME"] | ||
239 | 101 | return "%s://%s%s%s" % ( | ||
240 | 102 | "https" if request.is_secure() else "http", | ||
241 | 103 | request.get_host(), script_name, path) | ||
242 | 104 | |||
243 | 105 | |||
244 | 88 | def strip_domain(hostname): | 106 | def strip_domain(hostname): |
245 | 89 | """Return `hostname` with the domain part removed.""" | 107 | """Return `hostname` with the domain part removed.""" |
246 | 90 | return hostname.split('.', 1)[0] | 108 | return hostname.split('.', 1)[0] |
247 | 91 | 109 | ||
248 | === modified file 'src/maasserver/utils/tests/test_utils.py' | |||
249 | --- src/maasserver/utils/tests/test_utils.py 2012-10-30 15:15:30 +0000 | |||
250 | +++ src/maasserver/utils/tests/test_utils.py 2012-11-02 09:52:23 +0000 | |||
251 | @@ -16,11 +16,13 @@ | |||
252 | 16 | 16 | ||
253 | 17 | from django.conf import settings | 17 | from django.conf import settings |
254 | 18 | from django.core.urlresolvers import reverse | 18 | from django.core.urlresolvers import reverse |
255 | 19 | from django.http import HttpRequest | ||
256 | 19 | from maasserver.enum import NODE_STATUS_CHOICES | 20 | from maasserver.enum import NODE_STATUS_CHOICES |
257 | 20 | from maasserver.testing.factory import factory | 21 | from maasserver.testing.factory import factory |
258 | 21 | from maasserver.testing.testcase import TestCase as DjangoTestCase | 22 | from maasserver.testing.testcase import TestCase as DjangoTestCase |
259 | 22 | from maasserver.utils import ( | 23 | from maasserver.utils import ( |
260 | 23 | absolute_reverse, | 24 | absolute_reverse, |
261 | 25 | build_absolute_uri, | ||
262 | 24 | get_db_state, | 26 | get_db_state, |
263 | 25 | map_enum, | 27 | map_enum, |
264 | 26 | strip_domain, | 28 | strip_domain, |
265 | @@ -107,6 +109,67 @@ | |||
266 | 107 | self.assertEqual(status, get_db_state(node, 'status')) | 109 | self.assertEqual(status, get_db_state(node, 'status')) |
267 | 108 | 110 | ||
268 | 109 | 111 | ||
269 | 112 | class TestBuildAbsoluteURI(TestCase): | ||
270 | 113 | """Tests for `build_absolute_uri`.""" | ||
271 | 114 | |||
272 | 115 | def make_request(self, host="example.com", port=80, script_name="", | ||
273 | 116 | is_secure=False): | ||
274 | 117 | """Return a :class:`HttpRequest` with the given parameters.""" | ||
275 | 118 | request = HttpRequest() | ||
276 | 119 | request.META["SERVER_NAME"] = host | ||
277 | 120 | request.META["SERVER_PORT"] = port | ||
278 | 121 | request.META["SCRIPT_NAME"] = script_name | ||
279 | 122 | request.is_secure = lambda: is_secure | ||
280 | 123 | return request | ||
281 | 124 | |||
282 | 125 | def test_simple(self): | ||
283 | 126 | request = self.make_request() | ||
284 | 127 | self.assertEqual( | ||
285 | 128 | "http://example.com/fred", | ||
286 | 129 | build_absolute_uri(request, "/fred")) | ||
287 | 130 | |||
288 | 131 | def test_different_port(self): | ||
289 | 132 | request = self.make_request(port=1234) | ||
290 | 133 | self.assertEqual( | ||
291 | 134 | "http://example.com:1234/fred", | ||
292 | 135 | build_absolute_uri(request, "/fred")) | ||
293 | 136 | |||
294 | 137 | def test_script_name_is_prefixed(self): | ||
295 | 138 | # The script name is always prefixed to the given path. | ||
296 | 139 | request = self.make_request(script_name="/foo/bar") | ||
297 | 140 | self.assertEqual( | ||
298 | 141 | "http://example.com/foo/bar/fred", | ||
299 | 142 | build_absolute_uri(request, "/fred")) | ||
300 | 143 | |||
301 | 144 | def test_secure(self): | ||
302 | 145 | request = self.make_request(port=443, is_secure=True) | ||
303 | 146 | self.assertEqual( | ||
304 | 147 | "https://example.com/fred", | ||
305 | 148 | build_absolute_uri(request, "/fred")) | ||
306 | 149 | |||
307 | 150 | def test_different_port_and_secure(self): | ||
308 | 151 | request = self.make_request(port=9443, is_secure=True) | ||
309 | 152 | self.assertEqual( | ||
310 | 153 | "https://example.com:9443/fred", | ||
311 | 154 | build_absolute_uri(request, "/fred")) | ||
312 | 155 | |||
313 | 156 | def test_no_leading_forward_slash(self): | ||
314 | 157 | # No attempt is made to ensure that the given path is separated from | ||
315 | 158 | # the to-be-prefixed path. | ||
316 | 159 | request = self.make_request(script_name="/foo") | ||
317 | 160 | self.assertEqual( | ||
318 | 161 | "http://example.com/foobar", | ||
319 | 162 | build_absolute_uri(request, "bar")) | ||
320 | 163 | |||
321 | 164 | def test_preserve_two_leading_slashes(self): | ||
322 | 165 | # Whilst this shouldn't ordinarily happen, two leading slashes in the | ||
323 | 166 | # path should be preserved, and not treated specially. | ||
324 | 167 | request = self.make_request(script_name="//foo") | ||
325 | 168 | self.assertEqual( | ||
326 | 169 | "http://example.com//foo/fred", | ||
327 | 170 | build_absolute_uri(request, "/fred")) | ||
328 | 171 | |||
329 | 172 | |||
330 | 110 | class TestStripDomain(TestCase): | 173 | class TestStripDomain(TestCase): |
331 | 111 | 174 | ||
332 | 112 | def test_strip_domain(self): | 175 | def test_strip_domain(self): |