Merge lp:~blake-rouse/maas/bootresource-simplestreams-endpoint into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2705
Proposed branch: lp:~blake-rouse/maas/bootresource-simplestreams-endpoint
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 745 lines (+700/-0)
4 files modified
src/maasserver/bootresources.py (+265/-0)
src/maasserver/middleware.py (+3/-0)
src/maasserver/tests/test_bootresources.py (+421/-0)
src/maasserver/urls.py (+11/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/bootresource-simplestreams-endpoint
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+230375@code.launchpad.net

Commit message

Added simplestreams endpoint on the region controller at path images-stream. Cluster controllers will connect to this endpoint to download the boot resource image data.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good... but I don't understand a lot of it.

The branch is a bit too big. I don't mean just line count, I mean that there's a lot going on. Things like TransactionWrapper and its tests could probably have been landed separately, and SimpleStreamsHandler could have been landed/reviewed in stages.

Pretty much all tests could do with some comments to explain what's going on. Remember that I'm you in about a month, when you've lost all the context you've currently got in-brain.

Basically it _looks_ fine, but add some comments, please :) Also, I have a suggestion for a simpler TransactionWrapper.

review: Needs Information
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I will add some comments to the tests, fix the TransactionWrapper, and try to use django url routing.

Thanks for the review.

Revision history for this message
Gavin Panella (allenap) wrote :

Tip top, thanks for the replies. I'm around tomorrow so ping me whenever you need a follow-up.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Having an issue with the TransactionWrapper implementation you suggested. See inline comment.

Revision history for this message
Gavin Panella (allenap) :
Revision history for this message
Gavin Panella (allenap) wrote :

Changes look good.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (20.6 KiB)

The attempt to merge lp:~blake-rouse/maas/bootresource-simplestreams-endpoint into lp:maas failed. Below is the output from the failed tests.

Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:5 http://security.ubuntu.com trusty-security/main Sources [39.6 kB]
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.3 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [129 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [46.2 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [108 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [74.9 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [293 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [181 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Fetched 1,005 kB in 0s (1,920 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi p...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/maasserver/bootresources.py'
--- src/maasserver/bootresources.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/bootresources.py 2014-08-15 01:28:48 +0000
@@ -0,0 +1,265 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Boot Resources."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "get_simplestream_endpoint",
17 "simplestreams_file_handler",
18 "simplestreams_stream_handler",
19 "SIMPLESTREAMS_URL_REGEXP",
20]
21
22from django.db import (
23 close_old_connections,
24 transaction,
25 )
26from django.http import (
27 Http404,
28 HttpResponse,
29 StreamingHttpResponse,
30 )
31from django.shortcuts import get_object_or_404
32from maasserver.enum import BOOT_RESOURCE_TYPE
33from maasserver.models import (
34 BootResource,
35 BootResourceFile,
36 BootResourceSet,
37 )
38from maasserver.utils import absolute_reverse
39from simplestreams import util as sutil
40
41# Used by maasserver.middleware.AccessMiddleware to allow
42# anonymous access to the simplestreams endpoint.
43SIMPLESTREAMS_URL_REGEXP = '^/images-stream/'
44
45
46def get_simplestream_endpoint():
47 """Returns the simplestreams endpoint for the Region."""
48 return {
49 'url': absolute_reverse(
50 'simplestreams_stream_handler', kwargs={'filename': 'index.json'}),
51 'selections': [],
52 }
53
54
55class TransactionWrapper:
56 """Wraps `LargeObjectFile` in transaction, so `StreamingHttpResponse`
57 can be used. Once the stream is done, then the transaction is
58 closed.
59 """
60
61 def __init__(self, largeobject):
62 self.largeobject = largeobject
63 self._atomic = None
64 self._stream = None
65
66 def __iter__(self):
67 return self
68
69 def next(self):
70 if self._atomic is None:
71 self._atomic = transaction.atomic()
72 self._atomic.__enter__()
73 if self._stream is None:
74 self._stream = self.largeobject.open('rb')
75 data = self._stream.read(self.largeobject.block_size)
76 if len(data) == 0:
77 raise StopIteration
78 return data
79
80 def close(self):
81 if self._stream is not None:
82 self._stream.close()
83 self._stream = None
84 if self._atomic is not None:
85 self._atomic.__exit__()
86 self._atomic = None
87 close_old_connections()
88
89
90class SimpleStreamsHandler:
91 """Simplestreams endpoint, that the clusters talk to.
92
93 This is not called from piston, as piston uses emitters which
94 breaks the ability to return streaming content.
95
96 Anyone can access this endpoint. No credentials are required.
97 """
98
99 def get_json_response(self, content):
100 """Return `HttpResponse` for JSON content."""
101 response = HttpResponse(content.encode('utf-8'))
102 response['Content-Type'] = "application/json"
103 return response
104
105 def get_boot_resource_identifiers(self, resource):
106 """Return tuple (os, arch, subarch, series) for the given resource."""
107 arch, subarch = resource.split_arch()
108 if resource.rtype == BOOT_RESOURCE_TYPE.UPLOADED:
109 os = 'custom'
110 series = resource.name
111 else:
112 os, series = resource.name.split('/')
113 return (os, arch, subarch, series)
114
115 def get_product_name(self, resource):
116 """Return product name for the given resource."""
117 return 'maas:boot:%s:%s:%s:%s' % self.get_boot_resource_identifiers(
118 resource)
119
120 def gen_complete_boot_resources(self):
121 """Return generator of `BootResource` that contains a complete set."""
122 resources = BootResource.objects.all()
123 for resource in resources:
124 # Only add resources that have a complete set.
125 if resource.get_latest_complete_set() is None:
126 continue
127 yield resource
128
129 def gen_products_names(self):
130 """Return generator of avaliable products on the endpoint."""
131 for resource in self.gen_complete_boot_resources():
132 yield self.get_product_name(resource)
133
134 def get_product_index(self):
135 """Returns the streams product index `index.json`."""
136 products = list(self.gen_products_names())
137 updated = sutil.timestamp()
138 index = {
139 'index': {
140 'maas:v2:download': {
141 'datatype': "image-downloads",
142 'path': "streams/v1/maas:v2:download.json",
143 'updated': updated,
144 'products': products,
145 'format': "products:1.0",
146 },
147 },
148 'updated': updated,
149 'format': "index:1.0"
150 }
151 data = sutil.dump_data(index) + "\n"
152 return self.get_json_response(data)
153
154 def get_product_item(self, resource, resource_set, rfile):
155 """Returns the item description for the `rfile`."""
156 os, arch, subarch, series = self.get_boot_resource_identifiers(
157 resource)
158 path = '%s/%s/%s/%s/%s/%s' % (
159 os, arch, subarch, series, resource_set.version, rfile.filename)
160 item = {
161 'path': path,
162 'ftype': rfile.filetype,
163 'sha256': rfile.largefile.sha256,
164 'size': rfile.largefile.total_size,
165 }
166 item.update(rfile.extra)
167 return item
168
169 def get_product_data(self, resource):
170 """Returns the product data for this resource."""
171 os, arch, subarch, series = self.get_boot_resource_identifiers(
172 resource)
173 versions = {}
174 label = None
175 for resource_set in resource.sets.order_by('id').reverse():
176 if not resource_set.complete:
177 continue
178 # Set the label to the latest complete set label. In most cases the
179 # label will be the same for all sets. Only time it will differ is
180 # when daily has been enabled for a resource, that was previously
181 # only release. Only the latest version of the resource will be
182 # downloaded.
183 if label is None:
184 label = resource_set.label
185 items = {
186 rfile.filename: self.get_product_item(
187 resource, resource_set, rfile)
188 for rfile in resource_set.files.all()
189 }
190 versions[resource_set.version] = {
191 'items': items
192 }
193 product = {
194 'versions': versions,
195 'subarch': subarch,
196 'label': label,
197 'version': series,
198 'arch': arch,
199 'release': series,
200 'krel': series,
201 'os': os,
202 }
203 product.update(resource.extra)
204 return product
205
206 def get_product_download(self):
207 """Returns the streams download index `download.json`."""
208 products = {}
209 for resource in self.gen_complete_boot_resources():
210 name = self.get_product_name(resource)
211 products[name] = self.get_product_data(resource)
212 updated = sutil.timestamp()
213 index = {
214 'datatype': "image-downloads",
215 'updated': updated,
216 'content_id': "maas:v2:download",
217 'products': products,
218 'format': "products:1.0"
219 }
220 data = sutil.dump_data(index) + "\n"
221 return self.get_json_response(data)
222
223 def streams_handler(self, request, filename):
224 """Handles requests into the "streams/" content."""
225 if filename == 'index.json':
226 return self.get_product_index()
227 elif filename == 'maas:v2:download.json':
228 return self.get_product_download()
229 raise Http404()
230
231 def files_handler(
232 self, request, os, arch, subarch, series, version, filename):
233 """Handles requests for getting the boot resource data."""
234 if os == "custom":
235 name = series
236 else:
237 name = '%s/%s' % (os, series)
238 arch = '%s/%s' % (arch, subarch)
239 resource = get_object_or_404(
240 BootResource, name=name, architecture=arch)
241 try:
242 resource_set = resource.sets.get(version=version)
243 except BootResourceSet.DoesNotExist:
244 raise Http404()
245 try:
246 rfile = resource_set.files.get(filename=filename)
247 except BootResourceFile.DoesNotExist:
248 raise Http404()
249 response = StreamingHttpResponse(
250 TransactionWrapper(rfile.largefile.content),
251 content_type='application/octet-stream')
252 response['Content-Length'] = rfile.largefile.total_size
253 return response
254
255
256def simplestreams_stream_handler(request, filename):
257 handler = SimpleStreamsHandler()
258 return handler.streams_handler(request, filename)
259
260
261def simplestreams_file_handler(
262 request, os, arch, subarch, series, version, filename):
263 handler = SimpleStreamsHandler()
264 return handler.files_handler(
265 request, os, arch, subarch, series, version, filename)
0266
=== modified file 'src/maasserver/middleware.py'
--- src/maasserver/middleware.py 2014-02-11 12:19:25 +0000
+++ src/maasserver/middleware.py 2014-08-15 01:28:48 +0000
@@ -52,6 +52,7 @@
52 build_request_repr = repr52 build_request_repr = repr
53from django.utils.http import urlquote_plus53from django.utils.http import urlquote_plus
54from maasserver import logger54from maasserver import logger
55from maasserver.bootresources import SIMPLESTREAMS_URL_REGEXP
55from maasserver.exceptions import (56from maasserver.exceptions import (
56 ExternalComponentException,57 ExternalComponentException,
57 MAASAPIException,58 MAASAPIException,
@@ -97,6 +98,8 @@
97 reverse('metadata'),98 reverse('metadata'),
98 # RPC information is for use by clusters; no login.99 # RPC information is for use by clusters; no login.
99 reverse('rpc-info'),100 reverse('rpc-info'),
101 # Boot resources simple streams endpoint; no login.
102 SIMPLESTREAMS_URL_REGEXP,
100 # API calls are protected by piston.103 # API calls are protected by piston.
101 settings.API_URL_REGEXP,104 settings.API_URL_REGEXP,
102 ]105 ]
103106
=== added file 'src/maasserver/tests/test_bootresources.py'
--- src/maasserver/tests/test_bootresources.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_bootresources.py 2014-08-15 01:28:48 +0000
@@ -0,0 +1,421 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test maasserver.bootresources."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17import httplib
18import json
19from random import randint
20
21from django.core.urlresolvers import reverse
22from django.db import transaction
23from django.http import StreamingHttpResponse
24from django.test.client import Client
25from maasserver.bootresources import get_simplestream_endpoint
26from maasserver.enum import (
27 BOOT_RESOURCE_FILE_TYPE,
28 BOOT_RESOURCE_TYPE,
29 )
30from maasserver.testing.factory import factory
31from maasserver.testing.testcase import MAASServerTestCase
32from maasserver.utils import absolute_reverse
33from maastesting.testcase import MAASTestCase
34from testtools.matchers import ContainsAll
35
36
37class TestHelpers(MAASServerTestCase):
38 """Tests for `maasserver.bootresources` helpers."""
39
40 def test_get_simplestreams_endpoint(self):
41 endpoint = get_simplestream_endpoint()
42 self.assertEqual(
43 absolute_reverse(
44 'simplestreams_stream_handler',
45 kwargs={'filename': 'index.json'}),
46 endpoint['url'])
47 self.assertEqual([], endpoint['selections'])
48
49
50class TestSimpleStreamsHandler(MAASServerTestCase):
51 """Tests for `maasserver.bootresources.SimpleStreamsHandler`."""
52
53 def reverse_stream_handler(self, filename):
54 return reverse(
55 'simplestreams_stream_handler', kwargs={'filename': filename})
56
57 def reverse_file_handler(
58 self, os, arch, subarch, series, version, filename):
59 return reverse(
60 'simplestreams_file_handler', kwargs={
61 'os': os,
62 'arch': arch,
63 'subarch': subarch,
64 'series': series,
65 'version': version,
66 'filename': filename,
67 })
68
69 def get_stream_client(self, filename):
70 return self.client.get(self.reverse_stream_handler(filename))
71
72 def get_file_client(self, os, arch, subarch, series, version, filename):
73 return self.client.get(
74 self.reverse_file_handler(
75 os, arch, subarch, series, version, filename))
76
77 def get_product_name_for_resource(self, resource):
78 arch, subarch = resource.architecture.split('/')
79 if resource.rtype == BOOT_RESOURCE_TYPE.UPLOADED:
80 os = 'custom'
81 series = resource.name
82 else:
83 os, series = resource.name.split('/')
84 return 'maas:boot:%s:%s:%s:%s' % (os, arch, subarch, series)
85
86 def make_usable_product_boot_resource(self):
87 resource = factory.make_usable_boot_resource()
88 return self.get_product_name_for_resource(resource), resource
89
90 def test_streams_other_than_allowed_returns_404(self):
91 allowed_paths = [
92 'index.json',
93 'maas:v2:download.json',
94 ]
95 invalid_paths = [
96 '%s.json' % factory.make_name('path')
97 for _ in range(3)
98 ]
99 for path in allowed_paths:
100 response = self.get_stream_client(path)
101 self.assertEqual(httplib.OK, response.status_code)
102 for path in invalid_paths:
103 response = self.get_stream_client(path)
104 self.assertEqual(httplib.NOT_FOUND, response.status_code)
105
106 def test_streams_product_index_contains_keys(self):
107 response = self.get_stream_client('index.json')
108 output = json.loads(response.content)
109 self.assertThat(output, ContainsAll(['index', 'updated', 'format']))
110
111 def test_streams_product_index_format_is_index_1(self):
112 response = self.get_stream_client('index.json')
113 output = json.loads(response.content)
114 self.assertEqual('index:1.0', output['format'])
115
116 def test_streams_product_index_index_has_maas_v2_download(self):
117 response = self.get_stream_client('index.json')
118 output = json.loads(response.content)
119 self.assertThat(output['index'], ContainsAll(['maas:v2:download']))
120
121 def test_streams_product_index_maas_v2_download_contains_keys(self):
122 response = self.get_stream_client('index.json')
123 output = json.loads(response.content)
124 self.assertThat(
125 output['index']['maas:v2:download'],
126 ContainsAll([
127 'datatype', 'path', 'updated', 'products', 'format']))
128
129 def test_streams_product_index_maas_v2_download_has_valid_values(self):
130 response = self.get_stream_client('index.json')
131 output = json.loads(response.content)
132 self.assertEqual(
133 'image-downloads',
134 output['index']['maas:v2:download']['datatype'])
135 self.assertEqual(
136 'streams/v1/maas:v2:download.json',
137 output['index']['maas:v2:download']['path'])
138 self.assertEqual(
139 'products:1.0',
140 output['index']['maas:v2:download']['format'])
141
142 def test_streams_product_index_empty_products(self):
143 response = self.get_stream_client('index.json')
144 output = json.loads(response.content)
145 self.assertEqual(
146 [],
147 output['index']['maas:v2:download']['products'])
148
149 def test_streams_product_index_empty_with_incomplete_resource(self):
150 resource = factory.make_boot_resource()
151 factory.make_boot_resource_set(resource)
152 response = self.get_stream_client('index.json')
153 output = json.loads(response.content)
154 self.assertEqual(
155 [],
156 output['index']['maas:v2:download']['products'])
157
158 def test_streams_product_index_with_resources(self):
159 products = []
160 for _ in range(3):
161 product, _ = self.make_usable_product_boot_resource()
162 products.append(product)
163 response = self.get_stream_client('index.json')
164 output = json.loads(response.content)
165 # Product listing should be the same as all of the completed
166 # boot resources in the database.
167 self.assertItemsEqual(
168 products,
169 output['index']['maas:v2:download']['products'])
170
171 def test_streams_product_download_contains_keys(self):
172 response = self.get_stream_client('maas:v2:download.json')
173 output = json.loads(response.content)
174 self.assertThat(output, ContainsAll([
175 'datatype', 'updated', 'content_id', 'products', 'format']))
176
177 def test_streams_product_download_has_valid_values(self):
178 response = self.get_stream_client('maas:v2:download.json')
179 output = json.loads(response.content)
180 self.assertEqual('image-downloads', output['datatype'])
181 self.assertEqual('maas:v2:download', output['content_id'])
182 self.assertEqual('products:1.0', output['format'])
183
184 def test_streams_product_download_empty_products(self):
185 response = self.get_stream_client('maas:v2:download.json')
186 output = json.loads(response.content)
187 self.assertEqual(
188 {},
189 output['products'])
190
191 def test_streams_product_download_empty_with_incomplete_resource(self):
192 resource = factory.make_boot_resource()
193 factory.make_boot_resource_set(resource)
194 response = self.get_stream_client('maas:v2:download.json')
195 output = json.loads(response.content)
196 self.assertEqual(
197 {},
198 output['products'])
199
200 def test_streams_product_download_has_valid_product_keys(self):
201 products = []
202 for _ in range(3):
203 product, _ = self.make_usable_product_boot_resource()
204 products.append(product)
205 response = self.get_stream_client('maas:v2:download.json')
206 output = json.loads(response.content)
207 # Product listing should be the same as all of the completed
208 # boot resources in the database.
209 self.assertThat(
210 output['products'],
211 ContainsAll(products))
212
213 def test_streams_product_download_product_contains_keys(self):
214 product, _ = self.make_usable_product_boot_resource()
215 response = self.get_stream_client('maas:v2:download.json')
216 output = json.loads(response.content)
217 self.assertThat(
218 output['products'][product],
219 ContainsAll([
220 'versions', 'subarch', 'label', 'version',
221 'arch', 'release', 'krel', 'os']))
222
223 def test_streams_product_download_product_has_valid_values(self):
224 product, resource = self.make_usable_product_boot_resource()
225 _, _, os, arch, subarch, series = product.split(':')
226 label = resource.get_latest_complete_set().label
227 response = self.get_stream_client('maas:v2:download.json')
228 output = json.loads(response.content)
229 output_product = output['products'][product]
230 self.assertEqual(subarch, output_product['subarch'])
231 self.assertEqual(label, output_product['label'])
232 self.assertEqual(series, output_product['version'])
233 self.assertEqual(arch, output_product['arch'])
234 self.assertEqual(series, output_product['release'])
235 self.assertEqual(series, output_product['krel'])
236 self.assertEqual(os, output_product['os'])
237 for key, value in resource.extra.items():
238 self.assertEqual(value, output_product[key])
239
240 def test_streams_product_download_product_uses_latest_complete_label(self):
241 product, resource = self.make_usable_product_boot_resource()
242 # Incomplete resource_set
243 factory.make_boot_resource_set(resource)
244 newest_set = factory.make_boot_resource_set(resource)
245 factory.make_boot_resource_file_with_content(newest_set)
246 response = self.get_stream_client('maas:v2:download.json')
247 output = json.loads(response.content)
248 output_product = output['products'][product]
249 self.assertEqual(newest_set.label, output_product['label'])
250
251 def test_streams_product_download_product_contains_multiple_versions(self):
252 resource = factory.make_boot_resource()
253 resource_sets = [
254 factory.make_boot_resource_set(resource)
255 for _ in range(3)
256 ]
257 versions = []
258 for resource_set in resource_sets:
259 factory.make_boot_resource_file_with_content(resource_set)
260 versions.append(resource_set.version)
261 product = self.get_product_name_for_resource(resource)
262 response = self.get_stream_client('maas:v2:download.json')
263 output = json.loads(response.content)
264 self.assertThat(
265 output['products'][product]['versions'],
266 ContainsAll(versions))
267
268 def test_streams_product_download_product_version_contains_items(self):
269 product, resource = self.make_usable_product_boot_resource()
270 resource_set = resource.get_latest_complete_set()
271 items = [
272 rfile.filename
273 for rfile in resource_set.files.all()
274 ]
275 response = self.get_stream_client('maas:v2:download.json')
276 output = json.loads(response.content)
277 version = output['products'][product]['versions'][resource_set.version]
278 self.assertThat(
279 version['items'],
280 ContainsAll(items))
281
282 def test_streams_product_download_product_item_contains_keys(self):
283 product, resource = self.make_usable_product_boot_resource()
284 resource_set = resource.get_latest_complete_set()
285 resource_file = resource_set.files.order_by('?')[0]
286 response = self.get_stream_client('maas:v2:download.json')
287 output = json.loads(response.content)
288 version = output['products'][product]['versions'][resource_set.version]
289 self.assertThat(
290 version['items'][resource_file.filename],
291 ContainsAll(['path', 'ftype', 'sha256', 'size']))
292
293 def test_streams_product_download_product_item_has_valid_values(self):
294 product, resource = self.make_usable_product_boot_resource()
295 _, _, os, arch, subarch, series = product.split(':')
296 resource_set = resource.get_latest_complete_set()
297 resource_file = resource_set.files.order_by('?')[0]
298 path = '%s/%s/%s/%s/%s/%s' % (
299 os, arch, subarch, series, resource_set.version,
300 resource_file.filename)
301 response = self.get_stream_client('maas:v2:download.json')
302 output = json.loads(response.content)
303 version = output['products'][product]['versions'][resource_set.version]
304 item = version['items'][resource_file.filename]
305 self.assertEqual(path, item['path'])
306 self.assertEqual(resource_file.filetype, item['ftype'])
307 self.assertEqual(resource_file.largefile.sha256, item['sha256'])
308 self.assertEqual(resource_file.largefile.total_size, item['size'])
309 for key, value in resource_file.extra.items():
310 self.assertEqual(value, item[key])
311
312 def test_download_invalid_boot_resource_returns_404(self):
313 os = factory.make_name('os')
314 series = factory.make_name('series')
315 arch = factory.make_name('arch')
316 subarch = factory.make_name('subarch')
317 version = factory.make_name('version')
318 filename = factory.make_name('filename')
319 response = self.get_file_client(
320 os, arch, subarch, series, version, filename)
321 self.assertEqual(httplib.NOT_FOUND, response.status_code)
322
323 def test_download_invalid_version_returns_404(self):
324 product, resource = self.make_usable_product_boot_resource()
325 _, _, os, arch, subarch, series = product.split(':')
326 version = factory.make_name('version')
327 filename = factory.make_name('filename')
328 response = self.get_file_client(
329 os, arch, subarch, series, version, filename)
330 self.assertEqual(httplib.NOT_FOUND, response.status_code)
331
332 def test_download_invalid_filename_returns_404(self):
333 product, resource = self.make_usable_product_boot_resource()
334 _, _, os, arch, subarch, series = product.split(':')
335 resource_set = resource.get_latest_complete_set()
336 version = resource_set.version
337 filename = factory.make_name('filename')
338 response = self.get_file_client(
339 os, arch, subarch, series, version, filename)
340 self.assertEqual(httplib.NOT_FOUND, response.status_code)
341
342 def test_download_valid_path_returns_200(self):
343 product, resource = self.make_usable_product_boot_resource()
344 _, _, os, arch, subarch, series = product.split(':')
345 resource_set = resource.get_latest_complete_set()
346 version = resource_set.version
347 resource_file = resource_set.files.order_by('?')[0]
348 filename = resource_file.filename
349 response = self.get_file_client(
350 os, arch, subarch, series, version, filename)
351 self.assertEqual(httplib.OK, response.status_code)
352
353 def test_download_returns_streaming_response(self):
354 product, resource = self.make_usable_product_boot_resource()
355 _, _, os, arch, subarch, series = product.split(':')
356 resource_set = resource.get_latest_complete_set()
357 version = resource_set.version
358 resource_file = resource_set.files.order_by('?')[0]
359 filename = resource_file.filename
360 with resource_file.largefile.content.open('rb') as stream:
361 content = stream.read()
362 response = self.get_file_client(
363 os, arch, subarch, series, version, filename)
364 self.assertIsInstance(response, StreamingHttpResponse)
365 self.assertEqual(content, b''.join(response.streaming_content))
366
367
368class TestTransactionWrapper(MAASTestCase):
369 """Tests the use of StreamingHttpResponse(TransactionWrapper(stream)).
370
371 We do not run this inside of `MAASServerTestCase` as that wraps a
372 transaction around each test. This removes that behavior so we can
373 test that the transaction is remaining open for all of the content.
374 """
375
376 def test_download(self):
377 # Do the setup inside of a transaction, as we are running in a test
378 # that doesn't enable transactions per test.
379 with transaction.atomic():
380 os = factory.make_name('os')
381 series = factory.make_name('series')
382 arch = factory.make_name('arch')
383 subarch = factory.make_name('subarch')
384 name = '%s/%s' % (os, series)
385 architecture = '%s/%s' % (arch, subarch)
386 version = factory.make_name('version')
387 filetype = factory.pick_enum(BOOT_RESOURCE_FILE_TYPE)
388 # We set the filename to the same value as filetype, as in most
389 # cases this will always be true. The simplestreams content from
390 # maas.ubuntu.com, is formatted this way.
391 filename = filetype
392 size = randint(1024, 2048)
393 content = factory.make_string(size=size)
394 resource = factory.make_boot_resource(
395 rtype=BOOT_RESOURCE_TYPE.SYNCED, name=name,
396 architecture=architecture)
397 resource_set = factory.make_boot_resource_set(
398 resource, version=version)
399 largefile = factory.make_large_file(content=content, size=size)
400 factory.make_boot_resource_file(
401 resource_set, largefile, filename=filename, filetype=filetype)
402
403 # Outside of the transaction, we run the actual test. The client will
404 # run inside of its own transaction, but once the streaming response
405 # is returned that transaction will be closed.
406 client = Client()
407 response = client.get(
408 reverse(
409 'simplestreams_file_handler', kwargs={
410 'os': os,
411 'arch': arch,
412 'subarch': subarch,
413 'series': series,
414 'version': version,
415 'filename': filename,
416 }))
417
418 # If TransactionWrapper does not work, then a ProgramError will be
419 # thrown. If it works then content will match.
420 self.assertEqual(content, b''.join(response.streaming_content))
421 self.assertTrue(largefile.content.closed)
0422
=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py 2014-08-13 21:49:35 +0000
+++ src/maasserver/urls.py 2014-08-15 01:28:48 +0000
@@ -21,6 +21,10 @@
21 url,21 url,
22 )22 )
23from django.contrib.auth.decorators import user_passes_test23from django.contrib.auth.decorators import user_passes_test
24from maasserver.bootresources import (
25 simplestreams_file_handler,
26 simplestreams_stream_handler,
27 )
24from maasserver.enum import NODEGROUP_STATUS28from maasserver.enum import NODEGROUP_STATUS
25from maasserver.models import Node29from maasserver.models import Node
26from maasserver.views import TextTemplateView30from maasserver.views import TextTemplateView
@@ -104,6 +108,13 @@
104 'maasserver.views',108 'maasserver.views',
105 url(r'^accounts/login/$', login, name='login'),109 url(r'^accounts/login/$', login, name='login'),
106 url(110 url(
111 r'^images-stream/streams/v1/(?P<filename>.*)$',
112 simplestreams_stream_handler, name='simplestreams_stream_handler'),
113 url(
114 r'^images-stream/(?P<os>.*)/(?P<arch>.*)/(?P<subarch>.*)/'
115 '(?P<series>.*)/(?P<version>.*)/(?P<filename>.*)$',
116 simplestreams_file_handler, name='simplestreams_file_handler'),
117 url(
107 r'^robots\.txt$', TextTemplateView.as_view(118 r'^robots\.txt$', TextTemplateView.as_view(
108 template_name='maasserver/robots.txt'),119 template_name='maasserver/robots.txt'),
109 name='robots'),120 name='robots'),