← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/metadata-hostname into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/metadata-hostname into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jtv/maas/metadata-hostname/+merge/93457

This implements the “local-hostname” meta-data item as a lookup based on the Node as derived from the authentication key used for the request.

It's only one step along the way though.  A few other things that need doing:
 * Set up OAuth for the metadata service (probably by migrating this to Piston).
 * Check that the request really is made by the node-init user.
 * Creating node credentials and preseeding them through the provisioning server.

One step at a time.

Jeroen
-- 
https://code.launchpad.net/~jtv/maas/metadata-hostname/+merge/93457
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/metadata-hostname into lp:maas.
=== modified file 'src/maas/settings.py'
--- src/maas/settings.py	2012-02-12 18:32:26 +0000
+++ src/maas/settings.py	2012-02-16 16:57:31 +0000
@@ -33,7 +33,7 @@
 LOGOUT_URL = '/'
 LOGIN_REDIRECT_URL = '/'
 
-API_URL_REGEXP = '^/api/'
+API_URL_REGEXP = '^/(api|metadata)/'
 
 # We handle exceptions ourselves (in
 # maasserver.middleware.APIErrorsMiddleware)

=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py	2012-02-16 16:50:40 +0000
+++ src/maasserver/models.py	2012-02-16 16:57:31 +0000
@@ -38,6 +38,7 @@
 
 # Special users internal to MaaS.
 SYSTEM_USERS = [
+    # For nodes' access to the metadata API:
     nodeinituser.user_name,
     ]
 

=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py	2012-02-10 10:10:18 +0000
+++ src/metadataserver/api.py	2012-02-16 16:57:31 +0000
@@ -17,12 +17,41 @@
     ]
 
 from django.http import HttpResponse
-
-
-class UnknownMetadataVersion(RuntimeError):
+from maasserver.exceptions import (
+    MaasAPINotFound,
+    Unauthorized,
+    )
+from metadataserver.models import NodeKey
+
+
+class UnknownMetadataVersion(MaasAPINotFound):
     """Not a known metadata version."""
 
 
+class UnknownNode(MaasAPINotFound):
+    """Not a known node."""
+
+
+def extract_oauth_key(auth_data):
+    """Extract the oauth key from auth data in HTTP header."""
+    for entry in auth_data.split():
+        key_value = entry.split('=', 1)
+        if len(key_value) == 2:
+            key, value = key_value
+            if key == 'oauth_token':
+                return value
+    raise Unauthorized("No oauth token found for metadata request.")
+
+
+def get_node_for_request(request):
+    """Return the `Node` that `request` is authorized to query for."""
+    auth_header = request.META.get('HTTP_AUTHORIZATION')
+    if auth_header is None:
+        raise Unauthorized("No authorization header received.")
+    key = extract_oauth_key(auth_header)
+    return NodeKey.objects.get_node_for_key(key)
+
+
 def make_text_response(contents):
     """Create a response containing `contents` as plain text."""
     return HttpResponse(contents, mimetype='text/plain')
@@ -50,13 +79,32 @@
     return make_list_response(['meta-data', 'user-data'])
 
 
-def meta_data(request, version):
-    """View: meta-data listing for a given version."""
+def retrieve_unknown_item(node, item_path):
+    """Retrieve meta-data: unknown sub-item."""
+    raise MaasAPINotFound("No such metadata item: %s" % '/'.join(item_path))
+
+
+def retrieve_local_hostname(node, item_path):
+    """Retrieve meta-data: local-hostname."""
+    return make_text_response(node.hostname)
+
+
+def meta_data(request, version, item=None):
+    """View: meta-data listing for a given version, or meta-data item."""
     check_version(version)
-    items = [
-        'kernel-id',
-        ]
-    return make_list_response(items)
+    node = get_node_for_request(request)
+
+    # Functions to retrieve meta-data items.
+    retrievers = {
+        'local-hostname': retrieve_local_hostname,
+        }
+
+    if not item:
+        return make_list_response(sorted(retrievers.keys()))
+
+    item_path = item.split('/')
+    retriever = retrievers.get(item_path[0], retrieve_unknown_item)
+    return retriever(node, item_path[1:])
 
 
 def user_data(request, version):

=== modified file 'src/metadataserver/tests/test_api.py'
--- src/metadataserver/tests/test_api.py	2012-02-10 10:10:18 +0000
+++ src/metadataserver/tests/test_api.py	2012-02-16 16:57:31 +0000
@@ -11,6 +11,9 @@
 __metaclass__ = type
 __all__ = []
 
+import httplib
+
+from maasserver.testing.factory import factory
 from maastesting import TestCase
 from metadataserver.api import (
     check_version,
@@ -18,6 +21,7 @@
     make_text_response,
     UnknownMetadataVersion,
     )
+from metadataserver.models import NodeKey
 
 
 class TestHelpers(TestCase):
@@ -46,10 +50,16 @@
 class TestViews(TestCase):
     """Tests for the API views."""
 
-    def get(self, path):
+    def get(self, path, **headers):
         # Root of the metadata API service.
         metadata_root = "/metadata"
-        return self.client.get(metadata_root + path)
+        return self.client.get(metadata_root + path, **headers)
+
+    def make_node_and_auth_header(self):
+        node = factory.make_node()
+        consumer, token = NodeKey.objects.create_token(node)
+        header = 'oauth_token=%s' % token.key
+        return node, header
 
     def test_metadata_index_shows_latest(self):
         self.assertIn('latest', self.get('/').content)
@@ -66,9 +76,24 @@
         self.assertIn('user-data', items)
 
     def test_meta_data_view_returns_text_response(self):
-        self.assertEqual(
+        self.assertIn(
             'text/plain', self.get('/latest/meta-data/')['Content-Type'])
 
+    def test_meta_data_unknown_item_is_not_found(self):
+        node, header = self.make_node_and_auth_header()
+        response = self.get(
+            '/latest/meta-data/UNKNOWN-ITEM-HA-HA-HA',
+            HTTP_AUTHORIZATION=header)
+        self.assertEqual(httplib.NOT_FOUND, response.status_code)
+
+    def test_meta_data_local_hostname(self):
+        node, header = self.make_node_and_auth_header()
+        response = self.get(
+            '/latest/meta-data/local-hostname', HTTP_AUTHORIZATION=header)
+        self.assertEqual(httplib.OK, response.status_code)
+        self.assertIn('text/plain', response['Content-Type'])
+        self.assertEqual(node.hostname, response.content)
+
     def test_user_data_view_returns_binary_blob(self):
         response = self.get('/latest/user-data')
         self.assertEqual('application/octet-stream', response['Content-Type'])

=== modified file 'src/metadataserver/urls.py'
--- src/metadataserver/urls.py	2012-02-08 11:18:10 +0000
+++ src/metadataserver/urls.py	2012-02-16 16:57:31 +0000
@@ -28,7 +28,7 @@
 urlpatterns = patterns(
     '',
     url(
-        r'(?P<version>[^/]+)/meta-data/$', meta_data,
+        r'(?P<version>[^/]+)/meta-data/(?P<item>.*)$', meta_data,
         name='metadata_meta_data'),
     url(
         r'(?P<version>[^/]+)/user-data$', user_data,