Merge lp:~jtv/maas/1.5-revert-bug-1311433 into lp:~maas-committers/maas/trunk
- 1.5-revert-bug-1311433
- Merge into trunk
Proposed by
Jeroen T. Vermeulen
Status: | Superseded |
---|---|
Proposed branch: | lp:~jtv/maas/1.5-revert-bug-1311433 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1315 lines (+716/-221) (has conflicts) 20 files modified
docs/conf.py (+53/-16) docs/hardware-enablement-kernels.rst (+96/-0) docs/installing-ubuntu.rst (+88/-0) etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py (+8/-6) src/maascli/api.py (+1/-0) src/maasserver/api.py (+3/-3) src/maasserver/migrations/0072_remove_ipmi_autodetect.py (+9/-0) src/maasserver/models/node.py (+2/-42) src/maasserver/models/tests/test_node.py (+2/-117) src/maasserver/support/pertenant/tests/test_migration.py (+5/-9) src/maasserver/testing/factory.py (+2/-2) src/maasserver/tests/test_api_node.py (+4/-2) src/maasserver/views/nodes.py (+5/-0) src/maasserver/views/tests/test_clusters.py (+5/-5) src/maasserver/views/tests/test_general.py (+2/-1) src/maasserver/views/tests/test_nodes.py (+13/-4) src/provisioningserver/custom_hardware/seamicro.py (+9/-0) src/provisioningserver/import_images/boot_resources.py (+138/-0) src/provisioningserver/import_images/tests/test_boot_resources.py (+256/-14) src/provisioningserver/tests/test_tasks.py (+15/-0) Text conflict in docs/conf.py Text conflict in docs/hardware-enablement-kernels.rst Text conflict in docs/installing-ubuntu.rst Text conflict in src/maasserver/migrations/0072_remove_ipmi_autodetect.py Text conflict in src/maasserver/views/nodes.py Text conflict in src/maasserver/views/tests/test_nodes.py Text conflict in src/provisioningserver/custom_hardware/seamicro.py Text conflict in src/provisioningserver/import_images/boot_resources.py Text conflict in src/provisioningserver/import_images/tests/test_boot_resources.py Text conflict in src/provisioningserver/tests/test_tasks.py |
To merge this branch: | bzr merge lp:~jtv/maas/1.5-revert-bug-1311433 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Maintainers | Pending | ||
Review via email: mp+219068@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-05-09.
Commit message
Revert 1.5 r2258: workaround for bug 1311433 (now properly fixed in Django).
Description of the change
Reverting this in the branch will save the distro people an unnecessary SRU.
Jeroen
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'docs/conf.py' |
2 | --- docs/conf.py 2014-04-17 09:13:06 +0000 |
3 | +++ docs/conf.py 2014-05-09 20:33:59 +0000 |
4 | @@ -61,6 +61,23 @@ |
5 | |
6 | # -- General configuration ----------------------------------------------------- |
7 | |
8 | +# Add a widget to switch between different versions of the documentation to |
9 | +# each generated page. |
10 | +add_version_switcher = False |
11 | + |
12 | +# In order for the version widget to be able to redirect correctly to the |
13 | +# other versions of the documentation, each version of the documentation |
14 | +# has to be accessible at the following addresses: |
15 | +# /<doc_prefix>/ -> documentation for trunk. |
16 | +# /<doc_prefix>1.4/ -> documentation for 1.4. |
17 | +# etc. |
18 | +doc_prefix = 'docs' |
19 | + |
20 | +# Path of the JSON document, relative to homepage of the documentation for trunk |
21 | +# (i.e. '/<doc_prefix>/'), with the list of the versions to include in the |
22 | +# version switcher widget. |
23 | +versions_path = '_static/versions.js' |
24 | + |
25 | # If your documentation needs a minimal Sphinx version, state it here. |
26 | #needs_sphinx = '1.0' |
27 | |
28 | @@ -280,19 +297,39 @@ |
29 | |
30 | # Example configuration for intersphinx: refer to the Python standard library. |
31 | intersphinx_mapping = {'http://docs.python.org/': None} |
32 | - |
33 | -# Gather information about the branch and the build date. |
34 | -from subprocess import check_output |
35 | -bzr_last_revision_number = check_output(['bzr', 'revno']) |
36 | -bzr_last_revision_date = check_output(['bzr', 'version-info', '--template={date}', '--custom']) |
37 | -bzr_build_date = check_output(['bzr', 'version-info', '--template={build_date}', '--custom']) |
38 | - |
39 | -# Populate html_context with the variables used in the templates. |
40 | -html_context = { |
41 | - 'add_version_switcher': 'true' if add_version_switcher else 'false', |
42 | - 'versions_json_path': '/'.join(['', doc_prefix, versions_path]), |
43 | - 'doc_prefix': doc_prefix, |
44 | - 'bzr_last_revision_date': bzr_last_revision_date, |
45 | - 'bzr_last_revision_number': bzr_last_revision_number, |
46 | - 'bzr_build_date': bzr_build_date, |
47 | -} |
48 | +<<<<<<< TREE |
49 | + |
50 | +# Gather information about the branch and the build date. |
51 | +from subprocess import check_output |
52 | +bzr_last_revision_number = check_output(['bzr', 'revno']) |
53 | +bzr_last_revision_date = check_output(['bzr', 'version-info', '--template={date}', '--custom']) |
54 | +bzr_build_date = check_output(['bzr', 'version-info', '--template={build_date}', '--custom']) |
55 | + |
56 | +# Populate html_context with the variables used in the templates. |
57 | +html_context = { |
58 | + 'add_version_switcher': 'true' if add_version_switcher else 'false', |
59 | + 'versions_json_path': '/'.join(['', doc_prefix, versions_path]), |
60 | + 'doc_prefix': doc_prefix, |
61 | + 'bzr_last_revision_date': bzr_last_revision_date, |
62 | + 'bzr_last_revision_number': bzr_last_revision_number, |
63 | + 'bzr_build_date': bzr_build_date, |
64 | +} |
65 | +======= |
66 | + |
67 | +# Gather information about the branch and the build date. |
68 | +from subprocess import check_output |
69 | +bzr_last_revision_number = check_output(['bzr', 'revno']) |
70 | +bzr_last_revision_date = check_output(['bzr', 'version-info', '--template={date}', '--custom']) |
71 | +bzr_build_date = check_output(['bzr', 'version-info', '--template={build_date}', '--custom']) |
72 | + |
73 | +# Populate html_context with the variables used in the templates. |
74 | +html_context = { |
75 | + 'add_version_switcher': 'true' if add_version_switcher else 'false', |
76 | + 'versions_json_path': '/'.join(['', doc_prefix, versions_path]), |
77 | + 'doc_prefix': doc_prefix, |
78 | + 'bzr_last_revision_date': bzr_last_revision_date, |
79 | + 'bzr_last_revision_number': bzr_last_revision_number, |
80 | + 'bzr_build_date': bzr_build_date, |
81 | +} |
82 | + |
83 | +>>>>>>> MERGE-SOURCE |
84 | |
85 | === modified file 'docs/hardware-enablement-kernels.rst' |
86 | --- docs/hardware-enablement-kernels.rst 2014-04-14 21:42:00 +0000 |
87 | +++ docs/hardware-enablement-kernels.rst 2014-05-09 20:33:59 +0000 |
88 | @@ -1,3 +1,4 @@ |
89 | +<<<<<<< TREE |
90 | .. -*- mode: rst -*- |
91 | |
92 | .. _hardware-enablement-kernels: |
93 | @@ -91,3 +92,98 @@ |
94 | the Node's page and clicking ``Edit node``. Under the Architecture field, |
95 | you will be able to select any HWE kernels that have been imported onto |
96 | that node's cluster controller. |
97 | +======= |
98 | +.. -*- mode: rst -*- |
99 | + |
100 | +.. _hardware-enablement-kernels: |
101 | + |
102 | +================================= |
103 | +Using hardware-enablement kernels |
104 | +================================= |
105 | + |
106 | +.. note:: |
107 | + |
108 | + This feature is available in MAAS versions 1.5 and above. |
109 | + |
110 | +MAAS allows you to use hardware enablement kernels when booting nodes |
111 | +that require them. |
112 | + |
113 | +What are hardware-enablement kernels? |
114 | +------------------------------------- |
115 | + |
116 | +Brand new hardware gets released all the time. We want that hardware to |
117 | +work well wih Ubuntu and MAAS, even if it was released after the latest |
118 | +release of MAAS or Ubuntu. Hardware Enablement (HWE) is all about making |
119 | +keeping pace with the new hardware. |
120 | + |
121 | +Ubuntu's solution to this is to offer newer kernels for older releases. |
122 | +There are at least two kernels on offer for Ubuntu releases: the |
123 | +"generic" kernel -- i.e. the kernel released with the current series -- |
124 | +and the Hardware Enablement kernel, which is the most recent kernel |
125 | +release. |
126 | + |
127 | +There are separate HWE kernels for each release of Ubuntu, referred to |
128 | +as ``hwe-<release letter>``. So, the 14.04 / Trusty Tahr HWE kernel is |
129 | +called ``hwe-t``, the 12.10 / Quantal Quetzal HWE kernel is called |
130 | +``hwe-q`` and so on. This allows you to use newer kernels with older |
131 | +releases, for example running Precise with a Saucy (hwe-s) kernel. |
132 | + |
133 | +For more information see the `LTS Enablement Stack`_ page on the Ubuntu |
134 | +wiki. |
135 | + |
136 | +.. _LTS Enablement Stack: |
137 | + https://wiki.ubuntu.com/Kernel/LTSEnablementStack |
138 | + |
139 | +Importing hardware-enablement kernels |
140 | +------------------------------------- |
141 | + |
142 | +Hardware-enablement kernels need to be imported to a cluster controller |
143 | +before that cluster's nodes can use them. |
144 | + |
145 | +In order to import HWE kernels to a cluster controller you need to edit |
146 | +the controller's ``/etc/maas/bootresources.yaml`` file, and update the |
147 | +subarches that you want to import, like this:: |
148 | + |
149 | + boot: |
150 | + storage: "/var/lib/maas/boot-resources/" |
151 | + |
152 | + sources: |
153 | + - path: "http://maas.ubuntu.com/images/ephemeral-v2/releases/" |
154 | + keyring: "/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg" |
155 | + selections: |
156 | + - release: "precise" |
157 | + arches: ["i386", "amd64"] |
158 | + subarches: ["generic", "hwe-q", "hwe-r", "hwe-s", "hwe-t"] |
159 | + labels: ["release"] |
160 | + |
161 | +Once you've updated ``bootresources.yaml``, you can tell the cluster to |
162 | +re-import its boot images using the ``maas`` command (You will need to |
163 | +:ref:`be logged in to the API first <api-key>`):: |
164 | + |
165 | + $ maas <profile-name> node-group import-boot-images \ |
166 | + <cluster-controller-uuid> |
167 | + |
168 | +You can also tell the cluster controller to re-import its boot images by |
169 | +clicking the ``Import boot images`` button in the ``Clusters`` page of |
170 | +the MAAS web UI. |
171 | + |
172 | +Using hardware-enablement kernels in MAAS |
173 | +----------------------------------------- |
174 | + |
175 | +A MAAS administrator can choose to use HWE kernels on a per-node basis |
176 | +in MAAS. |
177 | + |
178 | +The quickest way to do this is using the MAAS command, like this:: |
179 | + |
180 | + $ maas <profile-name> node update <system-id> |
181 | + architecture=amd64/hwe-t |
182 | + |
183 | +If you specify an architecture that doesn't exist (e.g. |
184 | +``amd64/hwe-z``), the ``maas`` command will return an error. |
185 | + |
186 | + |
187 | +It's also possible to use HWE kernels from the MAAS web UI, by visiting |
188 | +the Node's page and clicking ``Edit node``. Under the Architecture field, |
189 | +you will be able to select any HWE kernels that have been imported onto |
190 | +that node's cluster controller. |
191 | +>>>>>>> MERGE-SOURCE |
192 | |
193 | === modified file 'docs/installing-ubuntu.rst' |
194 | --- docs/installing-ubuntu.rst 2014-05-05 16:20:18 +0000 |
195 | +++ docs/installing-ubuntu.rst 2014-05-09 20:33:59 +0000 |
196 | @@ -1,3 +1,4 @@ |
197 | +<<<<<<< TREE |
198 | ===================================== |
199 | Installing Ubuntu and deploying nodes |
200 | ===================================== |
201 | @@ -80,3 +81,90 @@ |
202 | on Launchpad. |
203 | |
204 | .. _curtin project: https://launchpad.net/curtin |
205 | +======= |
206 | +===================================== |
207 | +Installing Ubuntu and deploying nodes |
208 | +===================================== |
209 | + |
210 | +Once a node has been accepted into MAAS and is ready for use, users can |
211 | +deploy services to that node. |
212 | + |
213 | +Prior to deployment, MAAS is responsible for: |
214 | + |
215 | +1. Powering up the node. |
216 | +2. Installing Ubuntu on the node. |
217 | +3. Installing the user's SSH keys on the node. |
218 | + |
219 | +Once these steps have been completed, the node is ready to have services |
220 | +deployed to it, either manually or by using a tool like Juju_. |
221 | + |
222 | +There are two ways to install Ubuntu on a node: |
223 | + |
224 | +1. :ref:`The default installer <default-installer>`. |
225 | +2. :ref:`The fast installer <fast-installer>`. |
226 | + |
227 | +.. _Juju: http://juju.ubuntu.com |
228 | + |
229 | +.. _default-installer: |
230 | + |
231 | +The default installer |
232 | +---------------------- |
233 | + |
234 | +The default installer installs Ubuntu on a node in exactly the same way |
235 | +as you would install it manually: using the `Debian Installer`_. |
236 | +Installation is handled by the Debian installer. Answers to the |
237 | +questions asked by the installer are provided in a 'preseed' file. For |
238 | +more information on preseed files, see the :ref:`Additional |
239 | +Configuration <preseed>` page. |
240 | + |
241 | +As the name suggests, the default installer is enabled by default for |
242 | +all new nodes. To enable the default installer for a node that's been |
243 | +configured to use the fast installer, visit the node's page as an |
244 | +administrator and click the ``Use the default installer`` button. |
245 | + |
246 | +.. image:: media/node-page-use-default-installer.png |
247 | + |
248 | +To set multiple nodes to use the default installer, select the ``Mark |
249 | +nodes as using the default installer`` option from the bulk action menu |
250 | +on the ``Nodes`` page in the MAAS web UI. |
251 | + |
252 | +Because it installs Ubuntu from scratch, downloading packages as |
253 | +required, the default installer is slower than the :ref:`fast installer |
254 | +<fast-installer>`. |
255 | + |
256 | +.. _Debian Installer: http://www.debian.org/devel/debian-installer/ |
257 | + |
258 | +.. _fast-installer: |
259 | + |
260 | +The fast installer |
261 | +------------------ |
262 | + |
263 | +The fast installer is, as the name suggests, a means of installing |
264 | +Ubuntu on a node more quickly than would be possible using the |
265 | +:ref:`default installer <default-installer>`. |
266 | + |
267 | +To enable the fast installer for a node, visit the node's page as an |
268 | +administrator and click the ``Use the fast installer`` button. |
269 | + |
270 | +.. image:: media/node-page-use-fast-installer.png |
271 | + |
272 | +To set multiple nodes to use the fast installer, select the ``Mark nodes |
273 | +as using the fase installer`` option from the bulk action menu on the |
274 | +``Nodes`` page in the MAAS web UI. |
275 | + |
276 | +The fast installer copies a pre-built Ubuntu image to the node, with all |
277 | +the packages installed that would be normally found in an Ubuntu |
278 | +installation. |
279 | + |
280 | +The fast installer is much quicker than the default installer, but has |
281 | +the disadvantage that it's less easy to configure a node at install |
282 | +time, since the fast installer doesn't use a :ref:`preseed file |
283 | +<preseed>`. In addition, the packages that are initially installed on a |
284 | +fast-installed node need updating manually, since they are part of the |
285 | +installation image and not downloaded fresh from an apt repository. |
286 | + |
287 | +For more information about the fast installer, see the `curtin project`_ |
288 | +on Launchpad. |
289 | + |
290 | +.. _curtin project: https://launchpad.net/curtin |
291 | +>>>>>>> MERGE-SOURCE |
292 | |
293 | === modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py' |
294 | --- etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py 2014-04-04 06:46:05 +0000 |
295 | +++ etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py 2014-05-09 20:33:59 +0000 |
296 | @@ -165,10 +165,12 @@ |
297 | |
298 | for key, expected_value in user_settings.iteritems(): |
299 | # Password isn't included in checkout. |
300 | - if key != 'Password': |
301 | - value = bmc_user_get(user_number, key) |
302 | - if value != expected_value: |
303 | - bad_values[key] = value |
304 | + if key == 'Password': |
305 | + continue |
306 | + |
307 | + actual_value = bmc_user_get(user_number, key) |
308 | + if expected_value != actual_value: |
309 | + bad_values[key] = actual_value |
310 | |
311 | if len(bad_values) == 0: |
312 | return |
313 | @@ -177,8 +179,8 @@ |
314 | "for '%s', expected '%s', actual '%s';" % ( |
315 | key, user_settings[key], actual_value) |
316 | for key, actual_value in bad_values.iteritems() |
317 | - ]).rstrip(';') |
318 | - message = "IPMI user setting verification failures: %s." % (errors_string) |
319 | + ]).rstrip(';') |
320 | + message = 'IPMI user setting verification failures: %s.' % (errors_string) |
321 | raise IPMIError(message) |
322 | |
323 | |
324 | |
325 | === modified file 'etc/maas/templates/power/amt.template' |
326 | === modified file 'src/maascli/api.py' |
327 | --- src/maascli/api.py 2014-04-04 06:46:05 +0000 |
328 | +++ src/maascli/api.py 2014-05-09 20:33:59 +0000 |
329 | @@ -21,6 +21,7 @@ |
330 | from email.message import Message |
331 | from functools import partial |
332 | import httplib |
333 | +from itertools import chain |
334 | import json |
335 | from operator import itemgetter |
336 | import re |
337 | |
338 | === modified file 'src/maasserver/api.py' |
339 | --- src/maasserver/api.py 2014-05-07 02:44:10 +0000 |
340 | +++ src/maasserver/api.py 2014-05-09 20:33:59 +0000 |
341 | @@ -2310,10 +2310,10 @@ |
342 | line(doc.handler.__doc__) |
343 | line() |
344 | line() |
345 | - for (http_method, op), function in sorted(exports): |
346 | + for (http_method, operation), function in sorted(exports): |
347 | line("``%s %s``" % (http_method, uri_template), end="") |
348 | - if op is not None: |
349 | - line(" ``op=%s``" % op) |
350 | + if operation is not None: |
351 | + line(" ``op=%s``" % operation) |
352 | line() |
353 | docstring = getdoc(function) |
354 | if docstring is not None: |
355 | |
356 | === modified file 'src/maasserver/forms.py' |
357 | === modified file 'src/maasserver/migrations/0072_remove_ipmi_autodetect.py' |
358 | --- src/maasserver/migrations/0072_remove_ipmi_autodetect.py 2014-04-10 11:13:41 +0000 |
359 | +++ src/maasserver/migrations/0072_remove_ipmi_autodetect.py 2014-05-09 20:33:59 +0000 |
360 | @@ -1,7 +1,16 @@ |
361 | +<<<<<<< TREE |
362 | from django.db.utils import ProgrammingError |
363 | from maasserver import logger |
364 | +======= |
365 | +>>>>>>> MERGE-SOURCE |
366 | from provisioningserver.power_schema import IPMI_DRIVER |
367 | +<<<<<<< TREE |
368 | +======= |
369 | +# -*- coding: utf-8 -*- |
370 | +>>>>>>> MERGE-SOURCE |
371 | from south.v2 import DataMigration |
372 | +from django.db.utils import ProgrammingError |
373 | +from maasserver import logger |
374 | |
375 | |
376 | class Migration(DataMigration): |
377 | |
378 | === modified file 'src/maasserver/models/node.py' |
379 | --- src/maasserver/models/node.py 2014-05-07 02:44:10 +0000 |
380 | +++ src/maasserver/models/node.py 2014-05-09 20:33:59 +0000 |
381 | @@ -23,7 +23,6 @@ |
382 | repeat, |
383 | ) |
384 | import random |
385 | -import re |
386 | from string import whitespace |
387 | from uuid import uuid1 |
388 | |
389 | @@ -153,43 +152,6 @@ |
390 | """Raised when a node has an unknown power type.""" |
391 | |
392 | |
393 | -def validate_hostname(hostname): |
394 | - """Validator for hostnames. |
395 | - |
396 | - :param hostname: Input value for a host name. May include domain. |
397 | - :raise ValidationError: If the hostname is not valid according to RFCs 952 |
398 | - and 1123. |
399 | - """ |
400 | - # Valid characters within a hostname label: ASCII letters, ASCII digits, |
401 | - # hyphens, and underscores. Not all are always valid. |
402 | - # Technically we could write all of this as a single regex, but it's not |
403 | - # very good for code maintenance. |
404 | - label_chars = re.compile('[a-zA-Z0-9_-]*$') |
405 | - |
406 | - if len(hostname) > 255: |
407 | - raise ValidationError( |
408 | - "Hostname is too long. Maximum allowed is 255 characters.") |
409 | - # A hostname consists of "labels" separated by dots. |
410 | - labels = hostname.split('.') |
411 | - if '_' in labels[0]: |
412 | - # The host label cannot contain underscores; the rest of the name can. |
413 | - raise ValidationError( |
414 | - "Host label cannot contain underscore: %r." % labels[0]) |
415 | - for label in labels: |
416 | - if len(label) == 0: |
417 | - raise ValidationError("Hostname contains empty name.") |
418 | - if len(label) > 63: |
419 | - raise ValidationError( |
420 | - "Name is too long: %r. Maximum allowed is 63 characters." |
421 | - % label) |
422 | - if label.startswith('-') or label.endswith('-'): |
423 | - raise ValidationError( |
424 | - "Name cannot start or end with hyphen: %r." % label) |
425 | - if not label_chars.match(label): |
426 | - raise ValidationError( |
427 | - "Name contains disallowed characters: %r." % label) |
428 | - |
429 | - |
430 | class NodeManager(Manager): |
431 | """A utility to manage the collection of Nodes.""" |
432 | |
433 | @@ -446,7 +408,7 @@ |
434 | |
435 | :ivar system_id: The unique identifier for this `Node`. |
436 | (e.g. 'node-41eba45e-4cfa-11e1-a052-00225f89f211'). |
437 | - :ivar hostname: This `Node`'s hostname. Must conform to RFCs 952 and 1123. |
438 | + :ivar hostname: This `Node`'s hostname. |
439 | :ivar status: This `Node`'s status. See the vocabulary |
440 | :class:`NODE_STATUS`. |
441 | :ivar owner: This `Node`'s owner if it's in use, None otherwise. |
442 | @@ -466,9 +428,7 @@ |
443 | max_length=41, unique=True, default=generate_node_system_id, |
444 | editable=False) |
445 | |
446 | - hostname = CharField( |
447 | - max_length=255, default='', blank=True, unique=True, |
448 | - validators=[validate_hostname]) |
449 | + hostname = CharField(max_length=255, default='', blank=True, unique=True) |
450 | |
451 | status = IntegerField( |
452 | max_length=10, choices=NODE_STATUS_CHOICES, editable=False, |
453 | |
454 | === modified file 'src/maasserver/models/tests/test_node.py' |
455 | --- src/maasserver/models/tests/test_node.py 2014-05-07 02:44:10 +0000 |
456 | +++ src/maasserver/models/tests/test_node.py 2014-05-09 20:33:59 +0000 |
457 | @@ -40,7 +40,6 @@ |
458 | from maasserver.models.node import ( |
459 | generate_hostname, |
460 | NODE_TRANSITIONS, |
461 | - validate_hostname, |
462 | ) |
463 | from maasserver.models.user import create_auth_token |
464 | from maasserver.testing import reload_object |
465 | @@ -84,114 +83,6 @@ |
466 | self.assertEqual(sizes, [len(hostname) for hostname in hostnames]) |
467 | |
468 | |
469 | -class TestHostnameValidator(MAASTestCase): |
470 | - """Tests for the validation of hostnames. |
471 | - |
472 | - Specifications based on: |
473 | - http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names |
474 | - |
475 | - This does not support Internationalized Domain Names. To do so, we'd have |
476 | - to accept and store unicode, but use the Punycode-encoded version. The |
477 | - validator would have to validate both versions: the unicode input for |
478 | - invalid characters, and the encoded version for length. |
479 | - """ |
480 | - def make_maximum_hostname(self): |
481 | - """Create a hostname of the maximum permitted length. |
482 | - |
483 | - The maximum permitted length is 255 characters. The last label in the |
484 | - hostname will not be of the maximum length, so tests can still append a |
485 | - character to it without creating an invalid label. |
486 | - |
487 | - The hostname is not randomised, so do not count on it being unique. |
488 | - """ |
489 | - # A hostname may contain any number of labels, separated by dots. |
490 | - # Each of the labels has a maximum length of 63 characters, so this has |
491 | - # to be built up from multiple labels. |
492 | - ten_chars = ('a' * 9) + '.' |
493 | - hostname = ten_chars * 25 + ('b' * 5) |
494 | - self.assertEqual(255, len(hostname)) |
495 | - return hostname |
496 | - |
497 | - def assertAccepts(self, hostname): |
498 | - """Assertion: the validator accepts `hostname`.""" |
499 | - try: |
500 | - validate_hostname(hostname) |
501 | - except ValidationError as e: |
502 | - raise AssertionError(unicode(e)) |
503 | - |
504 | - def assertRejects(self, hostname): |
505 | - """Assertion: the validator rejects `hostname`.""" |
506 | - self.assertRaises(ValidationError, validate_hostname, hostname) |
507 | - |
508 | - def test_accepts_ascii_letters(self): |
509 | - self.assertAccepts('abcde') |
510 | - |
511 | - def test_accepts_dots(self): |
512 | - self.assertAccepts('abc.def') |
513 | - |
514 | - def test_rejects_adjacent_dots(self): |
515 | - self.assertRejects('abc..def') |
516 | - |
517 | - def test_rejects_leading_dot(self): |
518 | - self.assertRejects('.abc') |
519 | - |
520 | - def test_rejects_trailing_dot(self): |
521 | - self.assertRejects('abc.') |
522 | - |
523 | - def test_accepts_ascii_digits(self): |
524 | - self.assertAccepts('abc123') |
525 | - |
526 | - def test_accepts_leading_digits(self): |
527 | - # Leading digits used to be forbidden, but are now allowed. |
528 | - self.assertAccepts('123abc') |
529 | - |
530 | - def test_rejects_whitespace(self): |
531 | - self.assertRejects('a b') |
532 | - self.assertRejects('a\nb') |
533 | - self.assertRejects('a\tb') |
534 | - |
535 | - def test_rejects_other_ascii_characters(self): |
536 | - self.assertRejects('a?b') |
537 | - self.assertRejects('a!b') |
538 | - self.assertRejects('a,b') |
539 | - self.assertRejects('a:b') |
540 | - self.assertRejects('a;b') |
541 | - self.assertRejects('a+b') |
542 | - self.assertRejects('a=b') |
543 | - |
544 | - def test_accepts_underscore_in_domain(self): |
545 | - self.assertAccepts('host.local_domain') |
546 | - |
547 | - def test_rejects_underscore_in_host(self): |
548 | - self.assertRejects('host_name.local') |
549 | - |
550 | - def test_accepts_hyphen(self): |
551 | - self.assertAccepts('a-b') |
552 | - |
553 | - def test_rejects_hyphen_at_start_of_label(self): |
554 | - self.assertRejects('-ab') |
555 | - |
556 | - def test_rejects_hyphen_at_end_of_label(self): |
557 | - self.assertRejects('ab-') |
558 | - |
559 | - def test_accepts_maximum_valid_length(self): |
560 | - self.assertAccepts(self.make_maximum_hostname()) |
561 | - |
562 | - def test_rejects_oversized_hostname(self): |
563 | - self.assertRejects(self.make_maximum_hostname() + 'x') |
564 | - |
565 | - def test_accepts_maximum_label_length(self): |
566 | - self.assertAccepts('a' * 63) |
567 | - |
568 | - def test_rejects_oversized_label(self): |
569 | - self.assertRejects('b' * 64) |
570 | - |
571 | - def test_rejects_nonascii_letter(self): |
572 | - # The \u03be is the Greek letter xi. Perfectly good letter, just not |
573 | - # ASCII. |
574 | - self.assertRejects('\u03be') |
575 | - |
576 | - |
577 | class NodeTest(MAASServerTestCase): |
578 | |
579 | def test_system_id(self): |
580 | @@ -203,12 +94,6 @@ |
581 | self.assertEqual(len(node.system_id), 41) |
582 | self.assertTrue(node.system_id.startswith('node-')) |
583 | |
584 | - def test_hostname_is_validated(self): |
585 | - bad_hostname = '-_?!@*-' |
586 | - self.assertRaises( |
587 | - ValidationError, |
588 | - factory.make_node, hostname=bad_hostname) |
589 | - |
590 | def test_work_queue_returns_nodegroup_uuid(self): |
591 | nodegroup = factory.make_node_group() |
592 | node = factory.make_node(nodegroup=nodegroup) |
593 | @@ -332,14 +217,14 @@ |
594 | Config.objects.set_config("enlistment_domain", '') |
595 | existing_node = factory.make_node(hostname='hostname') |
596 | |
597 | - hostnames = [existing_node.hostname, "new-hostname"] |
598 | + hostnames = [existing_node.hostname, "new_hostname"] |
599 | self.patch( |
600 | node_module, "generate_hostname", |
601 | lambda size: hostnames.pop(0)) |
602 | |
603 | node = factory.make_node() |
604 | node.set_random_hostname() |
605 | - self.assertEqual('new-hostname', node.hostname) |
606 | + self.assertEqual('new_hostname', node.hostname) |
607 | |
608 | def test_get_effective_power_type_raises_if_not_set(self): |
609 | node = factory.make_node(power_type='') |
610 | |
611 | === modified file 'src/maasserver/support/pertenant/tests/test_migration.py' |
612 | --- src/maasserver/support/pertenant/tests/test_migration.py 2014-04-04 06:46:05 +0000 |
613 | +++ src/maasserver/support/pertenant/tests/test_migration.py 2014-05-09 20:33:59 +0000 |
614 | @@ -156,18 +156,14 @@ |
615 | self.assertEqual(get_legacy_user(), get_destination_user()) |
616 | |
617 | def test_get_destination_user_with_user_from_juju_state(self): |
618 | - user = factory.make_user() |
619 | - # Also create another user. |
620 | - factory.make_user() |
621 | - node = factory.make_node(owner=user) |
622 | + user1, user2 = factory.make_user(), factory.make_user() |
623 | + node = factory.make_node(owner=user1) |
624 | make_provider_state_file(node) |
625 | - self.assertEqual(user, get_destination_user()) |
626 | + self.assertEqual(user1, get_destination_user()) |
627 | |
628 | def test_get_destination_user_with_orphaned_juju_state(self): |
629 | - user = factory.make_user() |
630 | - # Also create another user. |
631 | - factory.make_user() |
632 | - node = factory.make_node(owner=user) |
633 | + user1, user2 = factory.make_user(), factory.make_user() |
634 | + node = factory.make_node(owner=user1) |
635 | make_provider_state_file(node) |
636 | node.delete() # Orphan the state. |
637 | self.assertEqual(get_legacy_user(), get_destination_user()) |
638 | |
639 | === modified file 'src/maasserver/testing/factory.py' |
640 | --- src/maasserver/testing/factory.py 2014-05-06 21:44:04 +0000 |
641 | +++ src/maasserver/testing/factory.py 2014-05-09 20:33:59 +0000 |
642 | @@ -648,8 +648,8 @@ |
643 | else: |
644 | vlan_tags = [None] * number |
645 | return [ |
646 | - self.make_network(vlan_tag=tag, **kwargs) |
647 | - for tag in vlan_tags |
648 | + self.make_network(vlan_tag=vlan_tag, **kwargs) |
649 | + for vlan_tag in vlan_tags |
650 | ] |
651 | |
652 | def make_boot_source(self, cluster=None, url=None, |
653 | |
654 | === modified file 'src/maasserver/tests/test_api_node.py' |
655 | --- src/maasserver/tests/test_api_node.py 2014-04-25 18:03:38 +0000 |
656 | +++ src/maasserver/tests/test_api_node.py 2014-05-09 20:33:59 +0000 |
657 | @@ -503,12 +503,14 @@ |
658 | hostname='diane', owner=self.logged_in_user, |
659 | architecture=make_usable_architecture(self)) |
660 | response = self.client_put( |
661 | - self.get_node_uri(node), {'hostname': '.'}) |
662 | + self.get_node_uri(node), {'hostname': 'too long' * 100}) |
663 | parsed_result = json.loads(response.content) |
664 | |
665 | self.assertEqual(httplib.BAD_REQUEST, response.status_code) |
666 | self.assertEqual( |
667 | - {'hostname': ["Hostname contains empty name."]}, |
668 | + {'hostname': |
669 | + ['Ensure this value has at most 255 characters ' |
670 | + '(it has 800).']}, |
671 | parsed_result) |
672 | |
673 | def test_PUT_refuses_to_update_invisible_node(self): |
674 | |
675 | === modified file 'src/maasserver/tests/test_api_nodegroup.py' |
676 | === modified file 'src/maasserver/tests/test_forms.py' |
677 | === modified file 'src/maasserver/views/nodes.py' |
678 | --- src/maasserver/views/nodes.py 2014-04-25 18:03:38 +0000 |
679 | +++ src/maasserver/views/nodes.py 2014-05-09 20:33:59 +0000 |
680 | @@ -24,8 +24,12 @@ |
681 | 'prefetch_nodes_listing', |
682 | ] |
683 | |
684 | +<<<<<<< TREE |
685 | from cgi import escape |
686 | from textwrap import dedent |
687 | +======= |
688 | +from cgi import escape |
689 | +>>>>>>> MERGE-SOURCE |
690 | from urllib import urlencode |
691 | |
692 | from django.conf import settings as django_settings |
693 | @@ -90,6 +94,7 @@ |
694 | ) |
695 | from metadataserver.models import NodeCommissionResult |
696 | from provisioningserver.tags import merge_details_cleanly |
697 | +from textwrap import dedent |
698 | |
699 | |
700 | def get_longpoll_context(): |
701 | |
702 | === modified file 'src/maasserver/views/tests/test_clusters.py' |
703 | --- src/maasserver/views/tests/test_clusters.py 2014-04-04 06:46:05 +0000 |
704 | +++ src/maasserver/views/tests/test_clusters.py 2014-05-09 20:33:59 +0000 |
705 | @@ -262,11 +262,11 @@ |
706 | self.client_log_in(as_admin=True) |
707 | nodegroup = factory.make_node_group() |
708 | interfaces = set() |
709 | - for _ in range(3): |
710 | - interfaces.add( |
711 | - factory.make_node_group_interface( |
712 | - nodegroup=nodegroup, |
713 | - management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)) |
714 | + for i in range(3): |
715 | + interface = factory.make_node_group_interface( |
716 | + nodegroup=nodegroup, |
717 | + management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
718 | + interfaces.add(interface) |
719 | links = get_content_links( |
720 | self.client.get(reverse('cluster-edit', args=[nodegroup.uuid]))) |
721 | interface_edit_links = [ |
722 | |
723 | === modified file 'src/maasserver/views/tests/test_general.py' |
724 | --- src/maasserver/views/tests/test_general.py 2014-04-04 06:46:05 +0000 |
725 | +++ src/maasserver/views/tests/test_general.py 2014-05-09 20:33:59 +0000 |
726 | @@ -418,7 +418,8 @@ |
727 | # to display all the errors. |
728 | component = factory.make_name('component') |
729 | error_message = factory.make_name('error') |
730 | - errors.append(Fault(fault, error_message)) |
731 | + error = Fault(fault, error_message) |
732 | + errors.append(error) |
733 | register_persistent_error(component, error_message) |
734 | links = [ |
735 | reverse('index'), |
736 | |
737 | === modified file 'src/maasserver/views/tests/test_nodes.py' |
738 | --- src/maasserver/views/tests/test_nodes.py 2014-04-25 18:03:38 +0000 |
739 | +++ src/maasserver/views/tests/test_nodes.py 2014-05-09 20:33:59 +0000 |
740 | @@ -16,6 +16,7 @@ |
741 | |
742 | from cgi import escape |
743 | import httplib |
744 | +from cgi import escape |
745 | from operator import attrgetter |
746 | import os |
747 | from random import randint |
748 | @@ -30,6 +31,7 @@ |
749 | from lxml.etree import XPath |
750 | from lxml.html import fromstring |
751 | import maasserver.api |
752 | +from maasserver.third_party_drivers import get_third_party_driver |
753 | from maasserver.enum import ( |
754 | NODE_STATUS, |
755 | NODEGROUP_STATUS, |
756 | @@ -68,10 +70,17 @@ |
757 | from maasserver.views import nodes as nodes_views |
758 | from maasserver.views.nodes import message_from_form_stats |
759 | from maastesting.djangotestcase import count_queries |
760 | -from metadataserver.models.commissioningscript import ( |
761 | - LIST_MODALIASES_OUTPUT_NAME, |
762 | - LLDP_OUTPUT_NAME, |
763 | - ) |
764 | +<<<<<<< TREE |
765 | +from metadataserver.models.commissioningscript import ( |
766 | + LIST_MODALIASES_OUTPUT_NAME, |
767 | + LLDP_OUTPUT_NAME, |
768 | + ) |
769 | +======= |
770 | +from metadataserver.models.commissioningscript import ( |
771 | + LLDP_OUTPUT_NAME, |
772 | + LIST_MODALIASES_OUTPUT_NAME, |
773 | + ) |
774 | +>>>>>>> MERGE-SOURCE |
775 | from testtools.matchers import ContainsAll |
776 | |
777 | |
778 | |
779 | === modified file 'src/provisioningserver/custom_hardware/seamicro.py' |
780 | --- src/provisioningserver/custom_hardware/seamicro.py 2014-04-08 20:58:05 +0000 |
781 | +++ src/provisioningserver/custom_hardware/seamicro.py 2014-05-09 20:33:59 +0000 |
782 | @@ -24,8 +24,17 @@ |
783 | import urlparse |
784 | |
785 | import provisioningserver.custom_hardware.utils as utils |
786 | +<<<<<<< TREE |
787 | from seamicroclient import exceptions as seamicro_exceptions |
788 | from seamicroclient.v2 import client as seamicro_client |
789 | +======= |
790 | +from seamicroclient.v2 import ( |
791 | + client as seamicro_client, |
792 | + ) |
793 | +from seamicroclient import ( |
794 | + exceptions as seamicro_exceptions, |
795 | + ) |
796 | +>>>>>>> MERGE-SOURCE |
797 | |
798 | |
799 | logger = logging.getLogger(__name__) |
800 | |
801 | === modified file 'src/provisioningserver/import_images/boot_resources.py' |
802 | --- src/provisioningserver/import_images/boot_resources.py 2014-04-28 14:19:28 +0000 |
803 | +++ src/provisioningserver/import_images/boot_resources.py 2014-05-09 20:33:59 +0000 |
804 | @@ -79,6 +79,132 @@ |
805 | return entry |
806 | |
807 | |
808 | +<<<<<<< TREE |
809 | +======= |
810 | +def mirror_info_for_path(path, unsigned_policy=None, keyring=None): |
811 | + if unsigned_policy is None: |
812 | + unsigned_policy = lambda content, path, keyring: content |
813 | + (mirror, rpath) = path_from_mirror_url(path, None) |
814 | + policy = policy_read_signed |
815 | + if rpath.endswith(".json"): |
816 | + policy = unsigned_policy |
817 | + if keyring: |
818 | + policy = functools.partial(policy, keyring=keyring) |
819 | + return(mirror, rpath, policy) |
820 | + |
821 | + |
822 | +class RepoDumper(BasicMirrorWriter): |
823 | + |
824 | + def dump(self, path, keyring=None): |
825 | + self._boot = create_empty_hierarchy() |
826 | + (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring) |
827 | + reader = UrlMirrorReader(mirror, policy=policy) |
828 | + super(RepoDumper, self).sync(reader, rpath) |
829 | + return self._boot |
830 | + |
831 | + def load_products(self, path=None, content_id=None): |
832 | + return |
833 | + |
834 | + def item_cleanup(self, item): |
835 | + keys_to_keep = ['content_id', 'product_name', 'version_name', 'path'] |
836 | + compact_item = {key: item[key] for key in keys_to_keep} |
837 | + return compact_item |
838 | + |
839 | + def insert_item(self, data, src, target, pedigree, contentsource): |
840 | + item = products_exdata(src, pedigree) |
841 | + arch, subarches = item['arch'], item['subarches'] |
842 | + release = item['release'] |
843 | + label = item['label'] |
844 | + compact_item = self.item_cleanup(item) |
845 | + for subarch in subarches.split(','): |
846 | + if not self._boot[arch][subarch][release][label]: |
847 | + self._boot[arch][subarch][release][label] = compact_item |
848 | + |
849 | + |
850 | +class RepoWriter(BasicMirrorWriter): |
851 | + |
852 | + def __init__(self, root_path, cache_path, info): |
853 | + self._root_path = os.path.abspath(root_path) |
854 | + self._info = info |
855 | + self._cache = FileStore(os.path.abspath(cache_path)) |
856 | + super(RepoWriter, self).__init__() |
857 | + |
858 | + def write(self, path, keyring=None): |
859 | + (mirror, rpath, policy) = mirror_info_for_path(path, keyring=keyring) |
860 | + reader = UrlMirrorReader(mirror, policy=policy) |
861 | + super(RepoWriter, self).sync(reader, rpath) |
862 | + |
863 | + def load_products(self, path=None, content_id=None): |
864 | + return |
865 | + |
866 | + def filter_version(self, data, src, target, pedigree): |
867 | + item = products_exdata(src, pedigree) |
868 | + content_id, product_name = item['content_id'], item['product_name'] |
869 | + version_name = item['version_name'] |
870 | + return ( |
871 | + content_id in self._info and |
872 | + product_name in self._info[content_id] and |
873 | + version_name in self._info[content_id][product_name] |
874 | + ) |
875 | + |
876 | + def insert_file(self, name, tag, checksums, size, contentsource): |
877 | + logger.info("Inserting file %s (tag=%s, size=%s).", name, tag, size) |
878 | + self._cache.insert( |
879 | + tag, contentsource, checksums, mutable=False, size=size) |
880 | + return [(self._cache._fullpath(tag), name)] |
881 | + |
882 | + def insert_root_image(self, tag, checksums, size, contentsource): |
883 | + root_image_tag = 'root-image-%s' % tag |
884 | + root_image_path = self._cache._fullpath(root_image_tag) |
885 | + root_tgz_tag = 'root-tgz-%s' % tag |
886 | + root_tgz_path = self._cache._fullpath(root_tgz_tag) |
887 | + if not os.path.isfile(root_image_path): |
888 | + logger.info("New root image: %s.", root_image_path) |
889 | + self._cache.insert( |
890 | + tag, contentsource, checksums, mutable=False, size=size) |
891 | + uncompressed = FdContentSource( |
892 | + GzipFile(self._cache._fullpath(tag))) |
893 | + self._cache.insert(root_image_tag, uncompressed, mutable=False) |
894 | + self._cache.remove(tag) |
895 | + if not os.path.isfile(root_tgz_path): |
896 | + logger.info("Converting root tarball: %s.", root_tgz_path) |
897 | + call_uec2roottar(root_image_path, root_tgz_path) |
898 | + return [(root_image_path, 'root-image'), (root_tgz_path, 'root-tgz')] |
899 | + |
900 | + def insert_item(self, data, src, target, pedigree, contentsource): |
901 | + item = products_exdata(src, pedigree) |
902 | + checksums = item_checksums(data) |
903 | + tag = checksums['sha256'] |
904 | + size = data['size'] |
905 | + ftype = item['ftype'] |
906 | + if ftype == 'root-image.gz': |
907 | + links = self.insert_root_image(tag, checksums, size, contentsource) |
908 | + else: |
909 | + links = self.insert_file( |
910 | + ftype, tag, checksums, size, contentsource) |
911 | + content_id = item['content_id'] |
912 | + prod_name = item['product_name'] |
913 | + version_name = item['version_name'] |
914 | + for subarch in self._info[content_id][prod_name][version_name]: |
915 | + dst_folder = os.path.join( |
916 | + self._root_path, item['arch'], subarch, item['release'], |
917 | + item['label']) |
918 | + if not os.path.exists(dst_folder): |
919 | + os.makedirs(dst_folder) |
920 | + for src, link_name in links: |
921 | + link_path = os.path.join(dst_folder, link_name) |
922 | + if os.path.isfile(link_path): |
923 | + os.remove(link_path) |
924 | + os.link(src, link_path) |
925 | + |
926 | + |
927 | +def available_boot_resources(root): |
928 | + for resource_path in glob.glob(os.path.join(root, '*/*/*/*')): |
929 | + arch, subarch, release, label = resource_path.split('/')[-4:] |
930 | + yield (arch, subarch, release, label) |
931 | + |
932 | + |
933 | +>>>>>>> MERGE-SOURCE |
934 | def install_boot_loaders(destination): |
935 | """Install the all the required file from each bootloader method. |
936 | :param destination: Directory where the loaders should be stored. |
937 | @@ -195,7 +321,19 @@ |
938 | return |
939 | |
940 | storage = config['boot']['storage'] |
941 | +<<<<<<< TREE |
942 | meta_file_content = image_descriptions.dump_json() |
943 | +======= |
944 | + |
945 | + boot = create_empty_hierarchy() |
946 | + dumper = RepoDumper() |
947 | + |
948 | + for source in config['boot']['sources']: |
949 | + repo_boot = dumper.dump(source['path'], keyring=source['keyring']) |
950 | + boot_merge(boot, repo_boot, source['selections']) |
951 | + |
952 | + meta_file_content = json.dumps(boot, sort_keys=True) |
953 | +>>>>>>> MERGE-SOURCE |
954 | if meta_contains(storage, meta_file_content): |
955 | # The current maas.meta already contains the new config. No need to |
956 | # rewrite anything. |
957 | |
958 | === modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py' |
959 | --- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-04-28 14:19:28 +0000 |
960 | +++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-05-09 20:33:59 +0000 |
961 | @@ -15,8 +15,6 @@ |
962 | __all__ = [] |
963 | |
964 | import errno |
965 | -import hashlib |
966 | -import json |
967 | import os |
968 | from random import randint |
969 | from subprocess import ( |
970 | @@ -25,12 +23,15 @@ |
971 | ) |
972 | |
973 | from maastesting.factory import factory |
974 | +<<<<<<< TREE |
975 | from maastesting.matchers import MockAnyCall |
976 | +======= |
977 | +>>>>>>> MERGE-SOURCE |
978 | from maastesting.testcase import MAASTestCase |
979 | -import mock |
980 | -from provisioningserver.boot.uefi import UEFIBootMethod |
981 | +from mock import MagicMock |
982 | from provisioningserver.config import BootConfig |
983 | from provisioningserver.import_images import boot_resources |
984 | +<<<<<<< TREE |
985 | from provisioningserver.import_images.boot_image_mapping import ( |
986 | BootImageMapping, |
987 | ) |
988 | @@ -44,8 +45,11 @@ |
989 | FileExists, |
990 | ) |
991 | import yaml |
992 | - |
993 | - |
994 | +======= |
995 | +>>>>>>> MERGE-SOURCE |
996 | + |
997 | + |
998 | +<<<<<<< TREE |
999 | class TestTgtEntry(MAASTestCase): |
1000 | """Tests for `tgt_entry`.""" |
1001 | |
1002 | @@ -95,8 +99,241 @@ |
1003 | return summer.hexdigest() |
1004 | |
1005 | |
1006 | +======= |
1007 | +def make_image_spec(): |
1008 | + """Return an `ImageSpec` with random values.""" |
1009 | + return boot_resources.ImageSpec( |
1010 | + factory.make_name('arch'), |
1011 | + factory.make_name('subarch'), |
1012 | + factory.make_name('release'), |
1013 | + factory.make_name('label'), |
1014 | + ) |
1015 | + |
1016 | + |
1017 | +class TestIterateBootResources(MAASTestCase): |
1018 | + """Tests for `iterate_boot_resources`.""" |
1019 | + |
1020 | + def test_empty_hierarchy_yields_nothing(self): |
1021 | + self.assertItemsEqual( |
1022 | + [], |
1023 | + boot_resources.iterate_boot_resources( |
1024 | + boot_resources.create_empty_hierarchy())) |
1025 | + |
1026 | + def test_finds_boot_resource(self): |
1027 | + image_spec = make_image_spec() |
1028 | + arch, subarch, release, label = image_spec |
1029 | + self.assertItemsEqual( |
1030 | + [image_spec], |
1031 | + boot_resources.iterate_boot_resources( |
1032 | + {arch: {subarch: {release: {label: factory.make_name()}}}})) |
1033 | + |
1034 | + |
1035 | +class TestValuePassesFilterList(MAASTestCase): |
1036 | + """Tests for `value_passes_filter_list`.""" |
1037 | + |
1038 | + def test_nothing_passes_empty_list(self): |
1039 | + self.assertFalse( |
1040 | + boot_resources.value_passes_filter_list( |
1041 | + [], factory.make_name('value'))) |
1042 | + |
1043 | + def test_unmatched_value_does_not_pass(self): |
1044 | + self.assertFalse( |
1045 | + boot_resources.value_passes_filter_list( |
1046 | + [factory.make_name('filter')], factory.make_name('value'))) |
1047 | + |
1048 | + def test_matched_value_passes(self): |
1049 | + value = factory.make_name('value') |
1050 | + self.assertTrue( |
1051 | + boot_resources.value_passes_filter_list([value], value)) |
1052 | + |
1053 | + def test_value_passes_if_matched_anywhere_in_filter(self): |
1054 | + value = factory.make_name('value') |
1055 | + self.assertTrue( |
1056 | + boot_resources.value_passes_filter_list( |
1057 | + [ |
1058 | + factory.make_name('filter'), |
1059 | + value, |
1060 | + factory.make_name('filter'), |
1061 | + ], |
1062 | + value)) |
1063 | + |
1064 | + def test_any_value_passes_asterisk(self): |
1065 | + self.assertTrue( |
1066 | + boot_resources.value_passes_filter_list( |
1067 | + ['*'], factory.make_name('value'))) |
1068 | + |
1069 | + |
1070 | +class TestValuePassesFilter(MAASTestCase): |
1071 | + """Tests for `value_passes_filter`.""" |
1072 | + |
1073 | + def test_unmatched_value_does_not_pass(self): |
1074 | + self.assertFalse( |
1075 | + boot_resources.value_passes_filter( |
1076 | + factory.make_name('filter'), factory.make_name('value'))) |
1077 | + |
1078 | + def test_matching_value_passes(self): |
1079 | + value = factory.make_name('value') |
1080 | + self.assertTrue(boot_resources.value_passes_filter(value, value)) |
1081 | + |
1082 | + def test_any_value_matches_asterisk(self): |
1083 | + self.assertTrue( |
1084 | + boot_resources.value_passes_filter( |
1085 | + '*', factory.make_name('value'))) |
1086 | + |
1087 | + |
1088 | +class TestImagePassesFilter(MAASTestCase): |
1089 | + """Tests for `image_passes_filter`.""" |
1090 | + |
1091 | + def make_filter_from_image(self, image_spec=None): |
1092 | + """Create a filter dict that matches the given `ImageSpec`. |
1093 | + |
1094 | + If `image_spec` is not given, creates a random value. |
1095 | + """ |
1096 | + if image_spec is None: |
1097 | + image_spec = make_image_spec() |
1098 | + return { |
1099 | + 'arches': [image_spec.arch], |
1100 | + 'subarches': [image_spec.subarch], |
1101 | + 'release': image_spec.release, |
1102 | + 'labels': [image_spec.label], |
1103 | + } |
1104 | + |
1105 | + def test_any_image_passes_none_filter(self): |
1106 | + arch, subarch, release, label = make_image_spec() |
1107 | + self.assertTrue( |
1108 | + boot_resources.image_passes_filter( |
1109 | + None, arch, subarch, release, label)) |
1110 | + |
1111 | + def test_any_image_passes_empty_filter(self): |
1112 | + arch, subarch, release, label = make_image_spec() |
1113 | + self.assertTrue( |
1114 | + boot_resources.image_passes_filter( |
1115 | + [], arch, subarch, release, label)) |
1116 | + |
1117 | + def test_image_passes_matching_filter(self): |
1118 | + image = make_image_spec() |
1119 | + self.assertTrue( |
1120 | + boot_resources.image_passes_filter( |
1121 | + [self.make_filter_from_image(image)], |
1122 | + image.arch, image.subarch, image.release, image.label)) |
1123 | + |
1124 | + def test_image_does_not_pass_nonmatching_filter(self): |
1125 | + image = make_image_spec() |
1126 | + self.assertFalse( |
1127 | + boot_resources.image_passes_filter( |
1128 | + [self.make_filter_from_image()], |
1129 | + image.arch, image.subarch, image.release, image.label)) |
1130 | + |
1131 | + def test_image_passes_if_one_filter_matches(self): |
1132 | + image = make_image_spec() |
1133 | + self.assertTrue( |
1134 | + boot_resources.image_passes_filter( |
1135 | + [ |
1136 | + self.make_filter_from_image(), |
1137 | + self.make_filter_from_image(image), |
1138 | + self.make_filter_from_image(), |
1139 | + ], image.arch, image.subarch, image.release, image.label)) |
1140 | + |
1141 | + def test_filter_checks_release(self): |
1142 | + image = make_image_spec() |
1143 | + self.assertFalse( |
1144 | + boot_resources.image_passes_filter( |
1145 | + [ |
1146 | + self.make_filter_from_image(image._replace( |
1147 | + release=factory.make_name('other-release'))) |
1148 | + ], image.arch, image.subarch, image.release, image.label)) |
1149 | + |
1150 | + def test_filter_checks_arches(self): |
1151 | + image = make_image_spec() |
1152 | + self.assertFalse( |
1153 | + boot_resources.image_passes_filter( |
1154 | + [ |
1155 | + self.make_filter_from_image(image._replace( |
1156 | + arch=factory.make_name('other-arch'))) |
1157 | + ], image.arch, image.subarch, image.release, image.label)) |
1158 | + |
1159 | + def test_filter_checks_subarches(self): |
1160 | + image = make_image_spec() |
1161 | + self.assertFalse( |
1162 | + boot_resources.image_passes_filter( |
1163 | + [ |
1164 | + self.make_filter_from_image(image._replace( |
1165 | + subarch=factory.make_name('other-subarch'))) |
1166 | + ], image.arch, image.subarch, image.release, image.label)) |
1167 | + |
1168 | + def test_filter_checks_labels(self): |
1169 | + image = make_image_spec() |
1170 | + self.assertFalse( |
1171 | + boot_resources.image_passes_filter( |
1172 | + [ |
1173 | + self.make_filter_from_image(image._replace( |
1174 | + label=factory.make_name('other-label'))) |
1175 | + ], image.arch, image.subarch, image.release, image.label)) |
1176 | + |
1177 | + |
1178 | +class TestBootMerge(MAASTestCase): |
1179 | + """Tests for `boot_merge`.""" |
1180 | + |
1181 | + def make_resource(self, boot_dict=None, image_spec=None, resource=None): |
1182 | + """Add a boot resource to `boot_dict`, creating it if necessary.""" |
1183 | + if boot_dict is None: |
1184 | + boot_dict = {} |
1185 | + if image_spec is None: |
1186 | + image_spec = make_image_spec() |
1187 | + if resource is None: |
1188 | + resource = factory.make_name('boot-resource') |
1189 | + arch, subarch, release, label = image_spec |
1190 | + # Drill down into the dict; along the way, create any missing levels of |
1191 | + # nested dicts. |
1192 | + nested_dict = boot_dict |
1193 | + for level in (arch, subarch, release): |
1194 | + nested_dict.setdefault(level, {}) |
1195 | + nested_dict = nested_dict[level] |
1196 | + # At the bottom level, indexed by "label," insert "resource" as the |
1197 | + # value. |
1198 | + nested_dict[label] = resource |
1199 | + return boot_dict |
1200 | + |
1201 | + def test_integrates(self): |
1202 | + # End-to-end scenario for boot_merge: start with an empty boot |
1203 | + # resources dict, and receive one resource from Simplestreams. |
1204 | + total_resources = boot_resources.create_empty_hierarchy() |
1205 | + resources_from_repo = self.make_resource() |
1206 | + boot_resources.boot_merge(total_resources, resources_from_repo.copy()) |
1207 | + # Since we started with an empty dict, the result contains the same |
1208 | + # item that we got from Simplestreams, and nothing else. |
1209 | + self.assertEqual(resources_from_repo, total_resources) |
1210 | + |
1211 | + def test_obeys_filters(self): |
1212 | + filters = [ |
1213 | + { |
1214 | + 'arches': [factory.make_name('other-arch')], |
1215 | + 'subarches': [factory.make_name('other-subarch')], |
1216 | + 'release': factory.make_name('other-release'), |
1217 | + 'label': [factory.make_name('other-label')], |
1218 | + }, |
1219 | + ] |
1220 | + total_resources = boot_resources.create_empty_hierarchy() |
1221 | + resources_from_repo = self.make_resource() |
1222 | + boot_resources.boot_merge( |
1223 | + total_resources, resources_from_repo, filters=filters) |
1224 | + self.assertEqual({}, total_resources) |
1225 | + |
1226 | + def test_does_not_overwrite_existing_entry(self): |
1227 | + image = make_image_spec() |
1228 | + original_resources = self.make_resource( |
1229 | + resource="Original resource", image_spec=image) |
1230 | + total_resources = original_resources.copy() |
1231 | + resources_from_repo = self.make_resource( |
1232 | + resource="New resource", image_spec=image) |
1233 | + boot_resources.boot_merge(total_resources, resources_from_repo.copy()) |
1234 | + self.assertEqual(original_resources, total_resources) |
1235 | + |
1236 | + |
1237 | +>>>>>>> MERGE-SOURCE |
1238 | class TestMain(MAASTestCase): |
1239 | |
1240 | +<<<<<<< TREE |
1241 | def patch_logger(self): |
1242 | """Suppress log output from the import code.""" |
1243 | self.patch(boot_resources, 'logger') |
1244 | @@ -343,23 +580,28 @@ |
1245 | "No boot resources found. " |
1246 | "Check configuration and connectivity.")) |
1247 | |
1248 | +======= |
1249 | +>>>>>>> MERGE-SOURCE |
1250 | def test_raises_ioerror_when_no_config_file_found(self): |
1251 | - self.patch_logger() |
1252 | - no_config = os.path.join( |
1253 | - self.make_dir(), '%s.yaml' % factory.make_name('no-config')) |
1254 | + # Suppress log output. |
1255 | + self.logger = self.patch(boot_resources, 'logger') |
1256 | + filename = "/tmp/%s" % factory.make_name("config") |
1257 | + self.assertFalse(os.path.exists(filename)) |
1258 | + args = MagicMock() |
1259 | + args.config_file = filename |
1260 | self.assertRaises( |
1261 | boot_resources.NoConfigFile, |
1262 | - boot_resources.main, self.make_args(config_file=no_config)) |
1263 | + boot_resources.main, args) |
1264 | |
1265 | def test_raises_non_ENOENT_IOErrors(self): |
1266 | # main() will raise a NoConfigFile error when it encounters an |
1267 | # ENOENT IOError, but will otherwise just re-raise the original |
1268 | # IOError. |
1269 | + args = MagicMock() |
1270 | mock_load_from_cache = self.patch(BootConfig, 'load_from_cache') |
1271 | other_error = IOError(randint(errno.ENOENT + 1, 1000)) |
1272 | mock_load_from_cache.side_effect = other_error |
1273 | - self.patch_logger() |
1274 | - raised_error = self.assertRaises( |
1275 | - IOError, |
1276 | - boot_resources.main, self.make_args()) |
1277 | + # Suppress log output. |
1278 | + self.logger = self.patch(boot_resources, 'logger') |
1279 | + raised_error = self.assertRaises(IOError, boot_resources.main, args) |
1280 | self.assertEqual(other_error, raised_error) |
1281 | |
1282 | === modified file 'src/provisioningserver/power_schema.py' |
1283 | === modified file 'src/provisioningserver/tasks.py' |
1284 | === modified file 'src/provisioningserver/tests/test_tasks.py' |
1285 | --- src/provisioningserver/tests/test_tasks.py 2014-05-05 18:51:41 +0000 |
1286 | +++ src/provisioningserver/tests/test_tasks.py 2014-05-09 20:33:59 +0000 |
1287 | @@ -676,6 +676,7 @@ |
1288 | self.patch_boot_resources_function() |
1289 | mock_callback = Mock() |
1290 | import_boot_images(callback=mock_callback) |
1291 | +<<<<<<< TREE |
1292 | self.assertThat(mock_callback.delay, MockCalledOnceWith()) |
1293 | |
1294 | |
1295 | @@ -688,3 +689,17 @@ |
1296 | mock = self.patch(tasks, 'probe_and_enlist_ucsm') |
1297 | enlist_nodes_from_ucsm(url, username, password) |
1298 | self.assertThat(mock, MockCalledOnceWith(url, username, password)) |
1299 | +======= |
1300 | + self.assertEqual([call()], mock_callback.delay.mock_calls) |
1301 | + |
1302 | + |
1303 | +class TestAddUCSM(PservTestCase): |
1304 | + |
1305 | + def test_enlist_nodes_from_ucsm(self): |
1306 | + url = 'url' |
1307 | + username = 'username' |
1308 | + password = 'password' |
1309 | + mock = self.patch(tasks, 'probe_and_enlist_ucsm') |
1310 | + enlist_nodes_from_ucsm(url, username, password) |
1311 | + self.assertThat(mock, MockCalledOnceWith(url, username, password)) |
1312 | +>>>>>>> MERGE-SOURCE |
1313 | |
1314 | === modified file 'src/provisioningserver/utils/__init__.py' |
1315 | === modified file 'src/provisioningserver/utils/tests/test_utils.py' |