Merge lp:~allenap/maas/api-uri-bug-1059645-for-1.2 into lp:maas/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
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.

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
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-10-30 15:15:30 +0000
+++ src/maasserver/api.py 2012-11-02 09:52:23 +0000
@@ -164,6 +164,7 @@
164from maasserver.server_address import get_maas_facing_server_address164from maasserver.server_address import get_maas_facing_server_address
165from maasserver.utils import (165from maasserver.utils import (
166 absolute_reverse,166 absolute_reverse,
167 build_absolute_uri,
167 map_enum,168 map_enum,
168 strip_domain,169 strip_domain,
169 )170 )
@@ -967,6 +968,10 @@
967968
968 get = operation(idempotent=True, exported_as='get')(get_file)969 get = operation(idempotent=True, exported_as='get')(get_file)
969970
971 @classmethod
972 def resource_uri(cls, *args, **kwargs):
973 return ('files_handler', [])
974
970975
971class FilesHandler(OperationsHandler):976class FilesHandler(OperationsHandler):
972 """File management operations."""977 """File management operations."""
@@ -1841,12 +1846,23 @@
1841 Returns a JSON object describing the whole MAAS API.1846 Returns a JSON object describing the whole MAAS API.
1842 """1847 """
1843 from maasserver import urls_api as urlconf1848 from maasserver import urls_api as urlconf
1849 resources = [
1850 describe_resource(resource)
1851 for resource in find_api_resources(urlconf)
1852 ]
1853 # Make all URIs absolute. Clients - maas-cli in particular - expect that
1854 # all handler URIs are absolute, not just paths. The handler URIs returned
1855 # by describe_resource() are relative paths.
1856 absolute = partial(build_absolute_uri, request)
1857 for resource in resources:
1858 for handler_type in "anon", "auth":
1859 handler = resource[handler_type]
1860 if handler is not None:
1861 handler["uri"] = absolute(handler["path"])
1862 # Package it all up.
1844 description = {1863 description = {
1845 "doc": "MAAS API",1864 "doc": "MAAS API",
1846 "resources": [1865 "resources": resources,
1847 describe_resource(resource)
1848 for resource in find_api_resources(urlconf)
1849 ],
1850 }1866 }
1851 # For backward compatibility, add "handlers" as an alias for all not-None1867 # For backward compatibility, add "handlers" as an alias for all not-None
1852 # anon and auth handlers in "resources".1868 # anon and auth handlers in "resources".
18531869
=== modified file 'src/maasserver/apidoc.py'
--- src/maasserver/apidoc.py 2012-10-06 22:42:45 +0000
+++ src/maasserver/apidoc.py 2012-11-02 09:52:23 +0000
@@ -19,9 +19,7 @@
1919
20from inspect import getdoc20from inspect import getdoc
21from itertools import izip_longest21from itertools import izip_longest
22from urlparse import urljoin
2322
24from django.conf import settings
25from django.core.urlresolvers import (23from django.core.urlresolvers import (
26 get_resolver,24 get_resolver,
27 RegexURLPattern,25 RegexURLPattern,
@@ -137,11 +135,8 @@
137 if isinstance(handler, BaseHandler):135 if isinstance(handler, BaseHandler):
138 handler = type(handler)136 handler = type(handler)
139137
140 uri_template = generate_doc(handler).resource_uri_template138 path = generate_doc(handler).resource_uri_template
141 if uri_template is None:139 path = "" if path is None else path
142 uri_template = settings.DEFAULT_MAAS_URL
143 else:
144 uri_template = urljoin(settings.DEFAULT_MAAS_URL, uri_template)
145140
146 resource_uri = getattr(handler, "resource_uri", lambda: ())141 resource_uri = getattr(handler, "resource_uri", lambda: ())
147 view_name, uri_params, uri_kw = merge(resource_uri(), (None, (), {}))142 view_name, uri_params, uri_kw = merge(resource_uri(), (None, (), {}))
@@ -154,7 +149,7 @@
154 "doc": getdoc(handler),149 "doc": getdoc(handler),
155 "name": handler.__name__,150 "name": handler.__name__,
156 "params": uri_params,151 "params": uri_params,
157 "uri": uri_template,152 "path": path,
158 }153 }
159154
160155
161156
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-10-30 15:15:30 +0000
+++ src/maasserver/tests/test_api.py 2012-11-02 09:52:23 +0000
@@ -27,10 +27,12 @@
27import httplib27import httplib
28from itertools import izip28from itertools import izip
29import json29import json
30from operator import itemgetter
30import os31import os
31import random32import random
32import shutil33import shutil
33import sys34import sys
35from urlparse import urlparse
3436
35from apiclient.maas_client import MAASClient37from apiclient.maas_client import MAASClient
36from celery.app import app_or_default38from celery.app import app_or_default
@@ -38,9 +40,11 @@
38from django.contrib.auth.models import AnonymousUser40from django.contrib.auth.models import AnonymousUser
39from django.core.urlresolvers import reverse41from django.core.urlresolvers import reverse
40from django.http import QueryDict42from django.http import QueryDict
43from django.test.client import RequestFactory
41from fixtures import Fixture44from fixtures import Fixture
42from maasserver import api45from maasserver import api
43from maasserver.api import (46from maasserver.api import (
47 describe,
44 DISPLAYED_NODEGROUP_FIELDS,48 DISPLAYED_NODEGROUP_FIELDS,
45 extract_constraints,49 extract_constraints,
46 extract_oauth_key,50 extract_oauth_key,
@@ -130,10 +134,15 @@
130from provisioningserver.pxe import tftppath134from provisioningserver.pxe import tftppath
131from provisioningserver.testing.boot_images import make_boot_image_params135from provisioningserver.testing.boot_images import make_boot_image_params
132from testresources import FixtureResource136from testresources import FixtureResource
137from testscenarios import multiply_scenarios
133from testtools.matchers import (138from testtools.matchers import (
139 AfterPreprocessing,
134 AllMatch,140 AllMatch,
135 Contains,141 Contains,
136 Equals,142 Equals,
143 Is,
144 MatchesAll,
145 MatchesAny,
137 MatchesListwise,146 MatchesListwise,
138 MatchesStructure,147 MatchesStructure,
139 StartsWith,148 StartsWith,
@@ -4192,3 +4201,49 @@
4192 self.assertSetEqual(4201 self.assertSetEqual(
4193 {"doc", "handlers", "resources"}, set(description))4202 {"doc", "handlers", "resources"}, set(description))
4194 self.assertIsInstance(description["handlers"], list)4203 self.assertIsInstance(description["handlers"], list)
4204
4205
4206class TestDescribeAbsoluteURIs(AnonAPITestCase):
4207 """Tests for the `describe` view's URI manipulation."""
4208
4209 scenarios_schemes = (
4210 ("http", dict(scheme="http")),
4211 ("https", dict(scheme="https")),
4212 )
4213
4214 scenarios_paths = (
4215 ("script-at-root", dict(script_name="", path_info="")),
4216 ("script-below-root-1", dict(script_name="/foo/bar", path_info="")),
4217 ("script-below-root-2", dict(script_name="/foo", path_info="/bar")),
4218 )
4219
4220 scenarios = multiply_scenarios(
4221 scenarios_schemes, scenarios_paths)
4222
4223 def test_handler_uris_are_absolute(self):
4224 server = factory.make_name("server").lower()
4225 extra = {
4226 "PATH_INFO": self.path_info,
4227 "SCRIPT_NAME": self.script_name,
4228 "SERVER_NAME": server,
4229 "wsgi.url_scheme": self.scheme,
4230 }
4231 request = RequestFactory().get(
4232 "/%s/describe" % factory.make_name("path"), **extra)
4233 response = describe(request)
4234 self.assertEqual(httplib.OK, response.status_code, response.content)
4235 description = json.loads(response.content)
4236 expected_uri = AfterPreprocessing(
4237 urlparse, MatchesStructure(
4238 scheme=Equals(self.scheme), hostname=Equals(server),
4239 # The path is always the script name followed by "api/"
4240 # because all API calls are within the "api" tree.
4241 path=StartsWith(self.script_name + "/api/")))
4242 expected_handler = MatchesAny(
4243 Is(None), AfterPreprocessing(itemgetter("uri"), expected_uri))
4244 expected_resource = MatchesAll(
4245 AfterPreprocessing(itemgetter("anon"), expected_handler),
4246 AfterPreprocessing(itemgetter("auth"), expected_handler))
4247 resources = description["resources"]
4248 self.assertNotEqual([], resources)
4249 self.assertThat(resources, AllMatch(expected_resource))
41954250
=== modified file 'src/maasserver/tests/test_apidoc.py'
--- src/maasserver/tests/test_apidoc.py 2012-10-08 10:23:16 +0000
+++ src/maasserver/tests/test_apidoc.py 2012-11-02 09:52:23 +0000
@@ -203,7 +203,7 @@
203 observed = describe_handler(ExampleHandler)203 observed = describe_handler(ExampleHandler)
204 # The description contains several entries.204 # The description contains several entries.
205 self.assertSetEqual(205 self.assertSetEqual(
206 {"actions", "doc", "name", "params", "uri"},206 {"actions", "doc", "name", "params", "path"},
207 set(observed))207 set(observed))
208 self.assertEqual(ExampleHandler.__doc__, observed["doc"])208 self.assertEqual(ExampleHandler.__doc__, observed["doc"])
209 self.assertEqual(ExampleHandler.__name__, observed["name"])209 self.assertEqual(ExampleHandler.__name__, observed["name"])
@@ -231,11 +231,11 @@
231 }231 }
232 self.assertSetEqual(expected_actions, observed_actions)232 self.assertSetEqual(expected_actions, observed_actions)
233 self.assertSetEqual({"system_id"}, set(description["params"]))233 self.assertSetEqual({"system_id"}, set(description["params"]))
234 # The URI is a URI Template <http://tools.ietf.org/html/rfc6570>, the234 # The path is a URI Template <http://tools.ietf.org/html/rfc6570>, the
235 # components of which correspond to the parameters declared.235 # components of which correspond to the parameters declared.
236 self.assertEqual(236 self.assertEqual(
237 "http://example.com/api/1.0/nodes/{system_id}/",237 "/api/1.0/nodes/{system_id}/",
238 description["uri"])238 description["path"])
239239
240 def test_describe_resource_anonymous_resource(self):240 def test_describe_resource_anonymous_resource(self):
241 # When the resource does not require authentication, any configured241 # When the resource does not require authentication, any configured
242242
=== modified file 'src/maasserver/utils/__init__.py'
--- src/maasserver/utils/__init__.py 2012-10-30 15:15:30 +0000
+++ src/maasserver/utils/__init__.py 2012-11-02 09:52:23 +0000
@@ -12,6 +12,7 @@
12__metaclass__ = type12__metaclass__ = type
13__all__ = [13__all__ = [
14 'absolute_reverse',14 'absolute_reverse',
15 'build_absolute_uri',
15 'get_db_state',16 'get_db_state',
16 'ignore_unused',17 'ignore_unused',
17 'map_enum',18 'map_enum',
@@ -85,6 +86,23 @@
85 return url86 return url
8687
8788
89def build_absolute_uri(request, path):
90 """Given a full app-relative path, returns an absolute URI.
91
92 The path ordinarily starts with a forward-slash... imagine that you've
93 magically chroot'ed to your Django application; the path is the absolute
94 path to the view within that environment.
95
96 The URI returned uses the request to figure out how to make an absolute
97 URL. This means that the URI returned will use the same IP address or
98 alias that the request came in on.
99 """
100 script_name = request.META["SCRIPT_NAME"]
101 return "%s://%s%s%s" % (
102 "https" if request.is_secure() else "http",
103 request.get_host(), script_name, path)
104
105
88def strip_domain(hostname):106def strip_domain(hostname):
89 """Return `hostname` with the domain part removed."""107 """Return `hostname` with the domain part removed."""
90 return hostname.split('.', 1)[0]108 return hostname.split('.', 1)[0]
91109
=== modified file 'src/maasserver/utils/tests/test_utils.py'
--- src/maasserver/utils/tests/test_utils.py 2012-10-30 15:15:30 +0000
+++ src/maasserver/utils/tests/test_utils.py 2012-11-02 09:52:23 +0000
@@ -16,11 +16,13 @@
1616
17from django.conf import settings17from django.conf import settings
18from django.core.urlresolvers import reverse18from django.core.urlresolvers import reverse
19from django.http import HttpRequest
19from maasserver.enum import NODE_STATUS_CHOICES20from maasserver.enum import NODE_STATUS_CHOICES
20from maasserver.testing.factory import factory21from maasserver.testing.factory import factory
21from maasserver.testing.testcase import TestCase as DjangoTestCase22from maasserver.testing.testcase import TestCase as DjangoTestCase
22from maasserver.utils import (23from maasserver.utils import (
23 absolute_reverse,24 absolute_reverse,
25 build_absolute_uri,
24 get_db_state,26 get_db_state,
25 map_enum,27 map_enum,
26 strip_domain,28 strip_domain,
@@ -107,6 +109,67 @@
107 self.assertEqual(status, get_db_state(node, 'status'))109 self.assertEqual(status, get_db_state(node, 'status'))
108110
109111
112class TestBuildAbsoluteURI(TestCase):
113 """Tests for `build_absolute_uri`."""
114
115 def make_request(self, host="example.com", port=80, script_name="",
116 is_secure=False):
117 """Return a :class:`HttpRequest` with the given parameters."""
118 request = HttpRequest()
119 request.META["SERVER_NAME"] = host
120 request.META["SERVER_PORT"] = port
121 request.META["SCRIPT_NAME"] = script_name
122 request.is_secure = lambda: is_secure
123 return request
124
125 def test_simple(self):
126 request = self.make_request()
127 self.assertEqual(
128 "http://example.com/fred",
129 build_absolute_uri(request, "/fred"))
130
131 def test_different_port(self):
132 request = self.make_request(port=1234)
133 self.assertEqual(
134 "http://example.com:1234/fred",
135 build_absolute_uri(request, "/fred"))
136
137 def test_script_name_is_prefixed(self):
138 # The script name is always prefixed to the given path.
139 request = self.make_request(script_name="/foo/bar")
140 self.assertEqual(
141 "http://example.com/foo/bar/fred",
142 build_absolute_uri(request, "/fred"))
143
144 def test_secure(self):
145 request = self.make_request(port=443, is_secure=True)
146 self.assertEqual(
147 "https://example.com/fred",
148 build_absolute_uri(request, "/fred"))
149
150 def test_different_port_and_secure(self):
151 request = self.make_request(port=9443, is_secure=True)
152 self.assertEqual(
153 "https://example.com:9443/fred",
154 build_absolute_uri(request, "/fred"))
155
156 def test_no_leading_forward_slash(self):
157 # No attempt is made to ensure that the given path is separated from
158 # the to-be-prefixed path.
159 request = self.make_request(script_name="/foo")
160 self.assertEqual(
161 "http://example.com/foobar",
162 build_absolute_uri(request, "bar"))
163
164 def test_preserve_two_leading_slashes(self):
165 # Whilst this shouldn't ordinarily happen, two leading slashes in the
166 # path should be preserved, and not treated specially.
167 request = self.make_request(script_name="//foo")
168 self.assertEqual(
169 "http://example.com//foo/fred",
170 build_absolute_uri(request, "/fred"))
171
172
110class TestStripDomain(TestCase):173class TestStripDomain(TestCase):
111174
112 def test_strip_domain(self):175 def test_strip_domain(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: