← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-api-versionning into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-api-versionning into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-api-versionning/+merge/94725

This branch adds proper versioning to the API (/api/nodes/ is now /api/1.0/nodes).

Drive-by fixes:
- refactor tests to factor out the API version part APIV1TestMixin.get_uri.  This will allow us to reuse the tests when we create another API version (all we will have to do is override the test case's 'get_uri' method).
- refactor API urls in a separate file (urls_api.py)
-- 
https://code.launchpad.net/~rvb/maas/maas-api-versionning/+merge/94725
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-api-versionning into lp:maas.
=== modified file 'src/maas/settings.py'
--- src/maas/settings.py	2012-02-22 14:04:11 +0000
+++ src/maas/settings.py	2012-02-27 07:50:23 +0000
@@ -33,7 +33,7 @@
 LOGOUT_URL = '/'
 LOGIN_REDIRECT_URL = '/'
 
-API_URL_REGEXP = '^/api/'
+API_URL_REGEXP = '^/api/1\.0/'
 METADATA_URL_REGEXP = '^/metadata/'
 
 

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2012-02-24 14:58:22 +0000
+++ src/maasserver/tests/test_api.py	2012-02-27 07:50:23 +0000
@@ -32,13 +32,24 @@
 from metadataserver.nodeinituser import get_node_init_user
 
 
-class AnonymousEnlistmentAPITest(TestCase):
+class APIV1TestMixin:
+
+    def get_uri(self, path):
+        """GET an API V1 uri.
+
+        :return: The API uri.
+        """
+        api_root = '/api/1.0/'
+        return api_root + path
+
+
+class AnonymousEnlistmentAPITest(APIV1TestMixin, TestCase):
     # Nodes can be enlisted anonymously.
 
     def test_POST_new_creates_node(self):
         # The API allows a Node to be created.
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'new',
                 'hostname': 'diane',
@@ -58,7 +69,7 @@
         # The API allows a Node to be created and associated with MAC
         # Addresses.
         self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'new',
                 'hostname': 'diane',
@@ -72,7 +83,7 @@
 
     def test_POST_returns_limited_fields(self):
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'new',
                 'hostname': 'diane',
@@ -87,7 +98,7 @@
         # If there is no operation ('op=operation_name') specified in the
         # request data, a 'Bad request' response is returned.
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'hostname': 'diane',
                 'mac_addresses': ['aa:bb:cc:dd:ee:ff', 'invalid'],
@@ -101,7 +112,7 @@
         # If the operation ('op=operation_name') specified in the
         # request data is unknown, a 'Bad request' response is returned.
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'invalid_operation',
                 'hostname': 'diane',
@@ -116,7 +127,7 @@
         # If the data provided to create a node with an invalid MAC
         # Address, a 'Bad request' response is returned.
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'new',
                 'hostname': 'diane',
@@ -132,28 +143,28 @@
             parsed_result['mac_addresses'])
 
 
-class NodeAnonAPITest(TestCase):
+class NodeAnonAPITest(APIV1TestMixin, TestCase):
 
     def test_anon_nodes_GET(self):
         # Anonymous requests to the API are denied.
-        response = self.client.get('/api/nodes/')
+        response = self.client.get(self.get_uri('nodes/'))
 
         self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
 
     def test_anon_api_doc(self):
         # The documentation is accessible to anon users.
-        response = self.client.get('/api/doc/')
+        response = self.client.get(self.get_uri('doc/'))
 
         self.assertEqual(httplib.OK, response.status_code)
 
     def test_node_init_user_cannot_access(self):
         token = NodeKey.objects.create_token(factory.make_node())
         client = OAuthAuthenticatedClient(get_node_init_user(), token)
-        response = client.get('/api/nodes/', {'op': 'list'})
+        response = client.get(self.get_uri('nodes/'), {'op': 'list'})
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
 
-class APITestCase(TestCase):
+class APITestCase(APIV1TestMixin, TestCase):
     """Extension to `TestCase`: log in first.
 
     :ivar logged_in_user: A user who is currently logged in and can access
@@ -179,12 +190,12 @@
     return [node.get('system_id') for node in parsed_result]
 
 
-class NodeAPILoggedInTest(LoggedInTestCase):
+class NodeAPILoggedInTest(APIV1TestMixin, LoggedInTestCase):
 
     def test_nodes_GET_logged_in(self):
         # A (Django) logged-in user can access the API.
         node = factory.make_node()
-        response = self.client.get('/api/nodes/', {'op': 'list'})
+        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
@@ -192,16 +203,16 @@
 
 
 class TestNodeAPI(APITestCase):
-    """Tests for /api/nodes/<node>/."""
+    """Tests for /api/1.0/nodes/<node>/."""
 
-    def get_uri(self, node):
+    def get_node_uri(self, node):
         """Get the API URI for `node`."""
-        return '/api/nodes/%s/' % node.system_id
+        return self.get_uri('nodes/%s/') % node.system_id
 
     def test_GET_returns_node(self):
         # The api allows for fetching a single Node (using system_id).
         node = factory.make_node(set_hostname=True)
-        response = self.client.get(self.get_uri(node))
+        response = self.client.get(self.get_node_uri(node))
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
@@ -214,58 +225,58 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
 
-        response = self.client.get(self.get_uri(other_node))
+        response = self.client.get(self.get_node_uri(other_node))
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_GET_refuses_to_access_nonexistent_node(self):
         # When fetching a Node, the api returns a 'Not Found' (404) error
         # if no node is found.
-        response = self.client.get('/api/nodes/invalid-uuid/')
+        response = self.client.get(self.get_uri('nodes/invalid-uuid/'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
     def test_POST_stop_checks_permission(self):
         node = factory.make_node()
-        response = self.client.post(self.get_uri(node), {'op': 'stop'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_POST_stop_returns_node(self):
         node = factory.make_node(owner=self.logged_in_user)
-        response = self.client.post(self.get_uri(node), {'op': 'stop'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
         self.assertEqual(httplib.OK, response.status_code)
         self.assertEqual(
             node.system_id, json.loads(response.content)['system_id'])
 
     def test_POST_stop_may_be_repeated(self):
         node = factory.make_node(owner=self.logged_in_user)
-        self.client.post(self.get_uri(node), {'op': 'stop'})
-        response = self.client.post(self.get_uri(node), {'op': 'stop'})
+        self.client.post(self.get_node_uri(node), {'op': 'stop'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
         self.assertEqual(httplib.OK, response.status_code)
 
     def test_POST_start_checks_permission(self):
         node = factory.make_node()
-        response = self.client.post(self.get_uri(node), {'op': 'start'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_POST_start_returns_node(self):
         node = factory.make_node(owner=self.logged_in_user)
-        response = self.client.post(self.get_uri(node), {'op': 'start'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
         self.assertEqual(httplib.OK, response.status_code)
         self.assertEqual(
             node.system_id, json.loads(response.content)['system_id'])
 
     def test_POST_start_may_be_repeated(self):
         node = factory.make_node(owner=self.logged_in_user)
-        self.client.post(self.get_uri(node), {'op': 'start'})
-        response = self.client.post(self.get_uri(node), {'op': 'start'})
+        self.client.post(self.get_node_uri(node), {'op': 'start'})
+        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
         self.assertEqual(httplib.OK, response.status_code)
 
     def test_PUT_updates_node(self):
         # The api allows to update a Node.
         node = factory.make_node(hostname='diane')
         response = self.client.put(
-            self.get_uri(node), {'hostname': 'francis'})
+            self.get_node_uri(node), {'hostname': 'francis'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
@@ -277,11 +288,12 @@
         # When a Node is returned by the API, the field 'resource_uri'
         # provides the URI for this Node.
         node = factory.make_node(hostname='diane')
-        response = self.client.put(self.get_uri(node), {'hostname': 'francis'})
+        response = self.client.put(
+            self.get_node_uri(node), {'hostname': 'francis'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(
-            '/api/nodes/%s/' % (parsed_result['system_id']),
+            self.get_uri('nodes/%s/') % (parsed_result['system_id']),
             parsed_result['resource_uri'])
 
     def test_PUT_rejects_invalid_data(self):
@@ -289,7 +301,7 @@
         # response is returned.
         node = factory.make_node(hostname='diane')
         response = self.client.put(
-            self.get_uri(node), {'hostname': 'too long' * 100})
+            self.get_node_uri(node), {'hostname': 'too long' * 100})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
@@ -305,14 +317,14 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
 
-        response = self.client.put(self.get_uri(other_node))
+        response = self.client.put(self.get_node_uri(other_node))
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_PUT_refuses_to_update_nonexistent_node(self):
         # When updating a Node, the api returns a 'Not Found' (404) error
         # if no node is found.
-        response = self.client.put('/api/nodes/no-node-here/')
+        response = self.client.put(self.get_uri('nodes/no-node-here/'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
@@ -320,7 +332,7 @@
         # The api allows to delete a Node.
         node = factory.make_node(set_hostname=True)
         system_id = node.system_id
-        response = self.client.delete(self.get_uri(node))
+        response = self.client.delete(self.get_node_uri(node))
 
         self.assertEqual(204, response.status_code)
         self.assertItemsEqual([], Node.objects.filter(system_id=system_id))
@@ -331,25 +343,25 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
 
-        response = self.client.delete(self.get_uri(other_node))
+        response = self.client.delete(self.get_node_uri(other_node))
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_DELETE_refuses_to_delete_nonexistent_node(self):
         # When deleting a Node, the api returns a 'Not Found' (404) error
         # if no node is found.
-        response = self.client.delete('/api/nodes/no-node-here/')
+        response = self.client.delete(self.get_uri('nodes/no-node-here/'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
 
 class TestNodesAPI(APITestCase):
-    """Tests for /api/nodes/."""
+    """Tests for /api/1.0/nodes/."""
 
     def test_POST_new_creates_node(self):
         # The API allows a Node to be created, even as a logged-in user.
         response = self.client.post(
-            '/api/nodes/',
+            self.get_uri('nodes/'),
             {
                 'op': 'new',
                 'hostname': 'diane',
@@ -365,7 +377,7 @@
         node2 = factory.make_node(
             set_hostname=True, status=NODE_STATUS.ALLOCATED,
             owner=self.logged_in_user)
-        response = self.client.get('/api/nodes/', {'op': 'list'})
+        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
@@ -376,13 +388,13 @@
     def test_GET_list_without_nodes_returns_empty_list(self):
         # If there are no nodes to list, the "list" op still works but
         # returns an empty list.
-        response = self.client.get('/api/nodes/', {'op': 'list'})
+        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
         self.assertItemsEqual([], json.loads(response.content))
 
     def test_GET_list_orders_by_id(self):
         # Nodes are returned in id order.
         nodes = [factory.make_node() for counter in range(3)]
-        response = self.client.get('/api/nodes/', {'op': 'list'})
+        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
         parsed_result = json.loads(response.content)
         self.assertSequenceEqual(
             [node.system_id for node in nodes],
@@ -393,7 +405,7 @@
         # nodes with matching ids will be returned.
         ids = [factory.make_node().system_id for counter in range(3)]
         matching_id = ids[0]
-        response = self.client.get('/api/nodes/', {
+        response = self.client.get(self.get_uri('nodes/'), {
             'op': 'list',
             'id': [matching_id],
         })
@@ -406,7 +418,7 @@
         # no nodes -- even if other (non-matching) nodes exist.
         existing_id = factory.make_node().system_id
         nonexistent_id = existing_id + factory.getRandomString()
-        response = self.client.get('/api/nodes/', {
+        response = self.client.get(self.get_uri('nodes/'), {
             'op': 'list',
             'id': [nonexistent_id],
         })
@@ -416,7 +428,7 @@
         # Even when ids are passed to "list," nodes are returned in id
         # order, not necessarily in the order of the id arguments.
         ids = [factory.make_node().system_id for counter in range(3)]
-        response = self.client.get('/api/nodes/', {
+        response = self.client.get(self.get_uri('nodes/'), {
             'op': 'list',
             'id': list(reversed(ids)),
         })
@@ -428,7 +440,7 @@
         # matching ones are returned.
         existing_id = factory.make_node().system_id
         nonexistent_id = existing_id + factory.getRandomString()
-        response = self.client.get('/api/nodes/', {
+        response = self.client.get(self.get_uri('nodes/'), {
             'op': 'list',
             'id': [existing_id, nonexistent_id],
         })
@@ -440,7 +452,7 @@
         # The "acquire" operation returns an available node.
         available_status = NODE_STATUS.READY
         node = factory.make_node(status=available_status, owner=None)
-        response = self.client.post('/api/nodes/', {'op': 'acquire'})
+        response = self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
         self.assertEqual(200, response.status_code)
         parsed_result = json.loads(response.content)
         self.assertEqual(node.system_id, parsed_result['system_id'])
@@ -449,14 +461,14 @@
         # The "acquire" operation allocates the node it returns.
         available_status = NODE_STATUS.READY
         node = factory.make_node(status=available_status, owner=None)
-        self.client.post('/api/nodes/', {'op': 'acquire'})
+        self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
         node = Node.objects.get(system_id=node.system_id)
         self.assertEqual(self.logged_in_user, node.owner)
 
     def test_POST_acquire_fails_if_no_node_present(self):
         # The "acquire" operation returns a Conflict error if no nodes
         # are available.
-        response = self.client.post('/api/nodes/', {'op': 'acquire'})
+        response = self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
         # Fails with Conflict error: resource can't satisfy request.
         self.assertEqual(httplib.CONFLICT, response.status_code)
 
@@ -471,7 +483,8 @@
 
     def test_macs_GET(self):
         # The api allows for fetching the list of the MAC Addresss for a node.
-        response = self.client.get('/api/nodes/%s/macs/' % self.node.system_id)
+        response = self.client.get(
+            self.get_uri('nodes/%s/macs/') % self.node.system_id)
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
@@ -487,14 +500,14 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
         response = self.client.get(
-            '/api/nodes/%s/macs/' % other_node.system_id)
+            self.get_uri('nodes/%s/macs/') % other_node.system_id)
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_macs_GET_not_found(self):
         # When fetching MAC Addresses, the api returns a 'Not Found' (404)
         # error if no node is found.
-        response = self.client.get('/api/nodes/invalid-id/macs/')
+        response = self.client.get(self.get_uri('nodes/invalid-id/macs/'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
@@ -502,7 +515,8 @@
         # When fetching a MAC Address, the api returns a 'Not Found' (404)
         # error if the MAC Address does not exist.
         response = self.client.get(
-            '/api/nodes/%s/macs/00-aa-22-cc-44-dd/' % self.node.system_id)
+            self.get_uri(
+                'nodes/%s/macs/00-aa-22-cc-44-dd/') % self.node.system_id)
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
@@ -512,7 +526,8 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
         response = self.client.get(
-            '/api/nodes/%s/macs/0-aa-22-cc-44-dd/' % other_node.system_id)
+            self.get_uri(
+                'nodes/%s/macs/0-aa-22-cc-44-dd/') % other_node.system_id)
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
@@ -520,7 +535,7 @@
         # When fetching a MAC Address, the api returns a 'Bad Request' (400)
         # error if the MAC Address is not valid.
         response = self.client.get(
-            '/api/nodes/%s/macs/invalid-mac/' % self.node.system_id)
+            self.get_uri('nodes/%s/macs/invalid-mac/') % self.node.system_id)
 
         self.assertEqual(400, response.status_code)
 
@@ -528,7 +543,7 @@
         # The api allows to add a MAC Address to an existing node.
         nb_macs = MACAddress.objects.filter(node=self.node).count()
         response = self.client.post(
-            '/api/nodes/%s/macs/' % self.node.system_id,
+            self.get_uri('nodes/%s/macs/') % self.node.system_id,
             {'mac_address': 'AA:BB:CC:DD:EE:FF'})
         parsed_result = json.loads(response.content)
 
@@ -542,7 +557,7 @@
         # A 'Bad Request' response is returned if one tries to add an invalid
         # MAC Address to a node.
         response = self.client.post(
-            '/api/nodes/%s/macs/' % self.node.system_id,
+            self.get_uri('nodes/%s/macs/') % self.node.system_id,
             {'mac_address': 'invalid-mac'})
         parsed_result = json.loads(response.content)
 
@@ -556,7 +571,7 @@
         # The api allows to delete a MAC Address.
         nb_macs = self.node.macaddress_set.count()
         response = self.client.delete(
-            '/api/nodes/%s/macs/%s/' % (
+            self.get_uri('nodes/%s/macs/%s/') % (
                 self.node.system_id, self.mac1.mac_address))
 
         self.assertEqual(204, response.status_code)
@@ -570,7 +585,7 @@
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
         response = self.client.delete(
-            '/api/nodes/%s/macs/%s/' % (
+            self.get_uri('nodes/%s/macs/%s/') % (
                 other_node.system_id, self.mac1.mac_address))
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
@@ -579,7 +594,7 @@
         # When deleting a MAC Address, the api returns a 'Not Found' (404)
         # error if no existing MAC Address is found.
         response = self.client.delete(
-            '/api/nodes/%s/macs/%s/' % (
+            self.get_uri('nodes/%s/macs/%s/') % (
                 self.node.system_id, '00-aa-22-cc-44-dd'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
@@ -588,7 +603,7 @@
         # When deleting a MAC Address, the api returns a 'Bad Request' (400)
         # error if the provided MAC Address is not valid.
         response = self.client.delete(
-            '/api/nodes/%s/macs/%s/' % (
+            self.get_uri('nodes/%s/macs/%s/') % (
                 self.node.system_id, 'invalid-mac'))
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
@@ -600,7 +615,7 @@
         # The api operation create_authorisation_token returns a json dict
         # with the consumer_key, the token_key and the token_secret in it.
         response = self.client.post(
-            '/api/account/', {'op': 'create_authorisation_token'})
+            self.get_uri('account/'), {'op': 'create_authorisation_token'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(
@@ -614,7 +629,7 @@
         # If the provided token_key does not exist (for the currently
         # logged-in user), the api returns a 'Not Found' (404) error.
         response = self.client.post(
-            '/api/account/',
+            self.get_uri('account/'),
             {'op': 'delete_authorisation_token', 'token_key': 'no-such-token'})
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
@@ -624,7 +639,7 @@
         # delete_authorisation_token. It it is not present in the request's
         # parameters, the api returns a 'Bad Request' (400) error.
         response = self.client.post(
-            '/api/account/', {'op': 'delete_authorisation_token'})
+            self.get_uri('account/'), {'op': 'delete_authorisation_token'})
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
@@ -661,12 +676,12 @@
     def make_API_POST_request(self, op=None, filename=None, fileObj=None):
         """Make an API POST request and return the response."""
         params = self._create_API_params(op, filename, fileObj)
-        return self.client.post("/api/files/", params)
+        return self.client.post(self.get_uri('files/'), params)
 
     def make_API_GET_request(self, op=None, filename=None, fileObj=None):
         """Make an API GET request and return the response."""
         params = self._create_API_params(op, filename, fileObj)
-        return self.client.get("/api/files/", params)
+        return self.client.get(self.get_uri('files/'), params)
 
     def test_add_file_succeeds(self):
         filepath = self.make_file()
@@ -699,7 +714,7 @@
 
         with open(filepath) as f, open(filepath2) as f2:
             response = self.client.post(
-                "/api/files/",
+                self.get_uri('files/'),
                 {
                     "op": "add",
                     "filename": "foo",

=== modified file 'src/maasserver/tests/test_middleware.py'
--- src/maasserver/tests/test_middleware.py	2012-02-24 17:37:01 +0000
+++ src/maasserver/tests/test_middleware.py	2012-02-27 07:50:23 +0000
@@ -116,7 +116,7 @@
 
     def test_handles_error_on_API(self):
         middleware = APIErrorsMiddleware()
-        non_api_request = fake_request("/api/hello")
+        non_api_request = fake_request("/api/1.0/hello")
         exception = MaaSAPINotFound("Have you looked under the couch?")
         response = middleware.process_exception(non_api_request, exception)
         self.assertEqual(

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-02-23 09:25:48 +0000
+++ src/maasserver/urls.py	2012-02-27 07:50:23 +0000
@@ -12,6 +12,7 @@
 __all__ = []
 
 from django.conf.urls.defaults import (
+    include,
     patterns,
     url,
     )
@@ -21,17 +22,6 @@
     direct_to_template,
     redirect_to,
     )
-from maas.api_auth import api_auth
-from maasserver.api import (
-    AccountHandler,
-    api_doc,
-    FilesHandler,
-    NodeHandler,
-    NodeMacHandler,
-    NodeMacsHandler,
-    NodesHandler,
-    RestrictedResource,
-    )
 from maasserver.models import Node
 from maasserver.views import (
     AccountsAdd,
@@ -92,35 +82,7 @@
 )
 
 
-# API.
-account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
-files_handler = RestrictedResource(FilesHandler, authentication=api_auth)
-node_handler = RestrictedResource(NodeHandler, authentication=api_auth)
-nodes_handler = RestrictedResource(NodesHandler, authentication=api_auth)
-node_mac_handler = RestrictedResource(NodeMacHandler, authentication=api_auth)
-node_macs_handler = RestrictedResource(
-    NodeMacsHandler, authentication=api_auth)
-
-
-# API URLs accessible to anonymous users.
-urlpatterns += patterns('',
-    url(r'^api/doc/$', api_doc, name='api-doc'),
-)
-
-
-# API URLs for logged-in users.
-urlpatterns += patterns('',
-    url(
-        r'^api/nodes/(?P<system_id>[\w\-]+)/macs/(?P<mac_address>.+)/$',
-        node_mac_handler, name='node_mac_handler'),
-    url(
-        r'^api/nodes/(?P<system_id>[\w\-]+)/macs/$', node_macs_handler,
-        name='node_macs_handler'),
-
-    url(
-        r'^api/nodes/(?P<system_id>[\w\-]+)/$', node_handler,
-        name='node_handler'),
-    url(r'^api/nodes/$', nodes_handler, name='nodes_handler'),
-    url(r'^api/files/$', files_handler, name='files_handler'),
-    url(r'^api/account/$', account_handler, name='account_handler'),
-)
+# API URLs.
+urlpatterns += patterns('',
+    (r'^api/1\.0/', include('maasserver.urls_api'))
+    )

=== added file 'src/maasserver/urls_api.py'
--- src/maasserver/urls_api.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/urls_api.py	2012-02-27 07:50:23 +0000
@@ -0,0 +1,59 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""URL API routing configuration."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from django.conf.urls.defaults import (
+    patterns,
+    url,
+    )
+from maas.api_auth import api_auth
+from maasserver.api import (
+    AccountHandler,
+    api_doc,
+    FilesHandler,
+    NodeHandler,
+    NodeMacHandler,
+    NodeMacsHandler,
+    NodesHandler,
+    RestrictedResource,
+    )
+account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
+files_handler = RestrictedResource(FilesHandler, authentication=api_auth)
+node_handler = RestrictedResource(NodeHandler, authentication=api_auth)
+nodes_handler = RestrictedResource(NodesHandler, authentication=api_auth)
+node_mac_handler = RestrictedResource(NodeMacHandler, authentication=api_auth)
+node_macs_handler = RestrictedResource(
+    NodeMacsHandler, authentication=api_auth)
+
+
+# API URLs accessible to anonymous users.
+urlpatterns = patterns('',
+    url(r'doc/$', api_doc, name='api-doc'),
+)
+
+
+# API URLs for logged-in users.
+urlpatterns += patterns('',
+    url(
+        r'nodes/(?P<system_id>[\w\-]+)/macs/(?P<mac_address>.+)/$',
+        node_mac_handler, name='node_mac_handler'),
+    url(
+        r'nodes/(?P<system_id>[\w\-]+)/macs/$', node_macs_handler,
+        name='node_macs_handler'),
+
+    url(
+        r'nodes/(?P<system_id>[\w\-]+)/$', node_handler,
+        name='node_handler'),
+    url(r'nodes/$', nodes_handler, name='nodes_handler'),
+    url(r'files/$', files_handler, name='files_handler'),
+    url(r'account/$', account_handler, name='account_handler'),
+)