Merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:~maas-committers/maas/trunk
- fix-commissioning-page-distro-list-bug-1312844
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Graham Binns |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2414 |
Proposed branch: | lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
709 lines (+328/-189) 9 files modified
src/maasserver/forms.py (+17/-97) src/maasserver/forms_settings.py (+46/-42) src/maasserver/models/bootsourceselection.py (+1/-10) src/maasserver/testing/factory.py (+17/-1) src/maasserver/tests/test_forms.py (+60/-0) src/maasserver/utils/os.py (+126/-0) src/maasserver/views/tests/test_settings.py (+6/-4) src/provisioningserver/boot/tests/test_tftppath.py (+1/-35) src/provisioningserver/testing/os.py (+54/-0) |
To merge this branch: | bzr merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Gavin Panella (community) | Approve | ||
Review via email: mp+222010@code.launchpad.net |
Commit message
The "deploy" section of the settings page will now no longer show operating systems and releases for which MAAS doesn't have boot resources. Previously, the page listed options that couldn't be used due to a lack of boot resources.
Description of the change
The logic that I've added / tweaked works as follows:
OSes and releases will appear if:
- There are boot images for the OS / release
- The OS and release are supported according the OperatingSystem
MAAS Lander (maas-lander) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Looks good, but I have several comments.
Also, there probably ought to be a test to demonstrate that DeployForm has fields that reflect the current state of the system, rather than that as import time.
Rapheël mentioned using callables for parts of Django's form machinery. Did you investigate that?
Gavin Panella (allenap) : | # |
Graham Binns (gmb) wrote : | # |
Thanks for the review and for sanity-checking me :)
On 4 June 2014 15:19, Gavin Panella <email address hidden> wrote:
> Looks good, but I have several comments.
>
> Also, there probably ought to be a test to demonstrate that DeployForm has fields that reflect the current state of the system, rather than that as import time.
Good point. I'll work on that.
> Rapheël mentioned using callables for parts of Django's form machinery. Did you investigate that?
Yeah, doesn't work (there's an open ticket for it in the Django Trac).
Well, it does for `initial` values, but not for `choices`. Because,
why would you want something that useful.
> Diff comments:
>
>> === modified file 'src/maasserver
>> --- src/maasserver/
>> +++ src/maasserver/
>> @@ -230,7 +230,10 @@
>> for nodegroup in NodeGroup.
>> osystems = osystems.union(
>> BootImage.
>> - osystems = [OperatingSyste
>> + osystems = [
>> + OperatingSystem
>> + if osystem in OperatingSystem
>
> I suspect this needs to be changed to query the cluster. All information on what OS/release combos could be supported are in the simplestream that describes boot images. The information on what OS/release combos that *can be supported right now* is in the list of boot resources downloaded. Both of those originate in the cluster, though the latter is also cached in BootImage.
>
> This doesn't need to be changed in this branch, but there ought to be a plan to address it.
Right. I'll file a bug once this is landed.
>> + ]
>> return sorted(osystems, key=lambda osystem: osystem.title)
>>
>>
>> @@ -1023,9 +1026,33 @@
>>
>> class DeployForm(
>> """Settings page, Deploy section."""
>> +
>> default_osystem = get_config_
>> default_
>>
>> + def __init__(self, *args, **kwargs):
>> + super(DeployForm, self)._
>> + # There's a race condition of some descrption that causes an
>> + # error when walking the BootImage table at import time (as
>> + # get_usable{
>> + # choices here is less expensive than trying to find the
>> + # underlying race, which might be a Django thing.
>> + usable_oses = list_all_
>> + os_choices = list_osystem_
>> + self.fields[
>> + self.fields[
>> + 'invalid_choice': compose_
>> + 'osystem', os_choices)
>> + }
>> +
>> + release_choices = list_release_
>> + list_all_
>> + self.fields[
>> + self.fields[
>> + 'invalid_
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 509 kB in 0s (1,897 kB/s)
Reading package lists...
sudo DEBIAN_
--
Julian Edwards (julian-edwards) wrote : | # |
On 05/06/14 00:19, Gavin Panella wrote:
> I suspect this needs to be changed to query the cluster. All information on what OS/release combos could be supported are in the simplestream that describes boot images. The information on what OS/release combos that *can be supported right now* is in the list of boot resources downloaded. Both of those originate in the cluster, though the latter is also cached in BootImage.
>
> This doesn't need to be changed in this branch, but there ought to be a plan to address it.
I have previously raised this with the original author (Blason!) and we
deferred it since the branch at the time was urgent.
So I would check with those guys to see if there's any plan in place. I
suspect not :)
But this is top of my tech-debt list for sure.
Raphaël Badin (rvb) wrote : | # |
I've added a couple of inline comments… please wait before landing this, I need to check something first…
Raphaël Badin (rvb) wrote : | # |
The OperatingSystem class has moved apparently, you need to do something like: http://
Raphaël Badin (rvb) wrote : | # |
There is a problem with moving the fields's attributes into DeployForm's init method.
These fields (default_osystem and default_
There is a (generic) test for this in src/maasserver/
The code path I'm describing here is:
- API:set_config
- src/maasserver/
- src/maasserver/
- src/maasserver/
Raphaël Badin (rvb) wrote : | # |
Looks good but I've got a couple of suggestions (see inline comments).
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 524 kB in 0s (1,814 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Ign http://
Get:2 http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 524 kB in 0s (1,808 kB/s)
Reading package lists...
sudo DEBIAN_
--
Graham Binns (gmb) wrote : | # |
On 11 June 2014 07:52, MaaS Lander <email address hidden> wrote:
> FAIL: maasserver.
I can't get this to fail locally, annoyingly… Will
run-once-
Raphaël Badin (rvb) wrote : | # |
I also think it would be worth having an end-to-end API test to make sure one of the config settings you're changing here can still be set using the API. Don't need to test them all since the testing at the form-level covers that.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 524 kB in 0s (1,913 kB/s)
Reading package lists...
sudo DEBIAN_
--
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~gmb/maas/fix-commissioning-page-distro-list-bug-1312844 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Ign http://
Ign http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 524 kB in 0s (1,742 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/maasserver/forms.py' |
2 | --- src/maasserver/forms.py 2014-06-11 04:38:41 +0000 |
3 | +++ src/maasserver/forms.py 2014-06-11 09:53:56 +0000 |
4 | @@ -112,6 +112,14 @@ |
5 | from maasserver.utils import strip_domain |
6 | from maasserver.utils.forms import compose_invalid_choice_text |
7 | from maasserver.utils.network import make_network |
8 | +from maasserver.utils.os import ( |
9 | + get_distro_series_inital, |
10 | + get_release_requires_key, |
11 | + list_all_usable_osystems, |
12 | + list_all_usable_releases, |
13 | + list_osystem_choices, |
14 | + list_release_choices, |
15 | + ) |
16 | from metadataserver.fields import Bin |
17 | from metadataserver.models import CommissioningScript |
18 | from provisioningserver.drivers.osystem import OperatingSystemRegistry |
19 | @@ -215,99 +223,6 @@ |
20 | return all_architectures[0] |
21 | |
22 | |
23 | -def list_all_usable_osystems(): |
24 | - """Return all operating systems that can be used for nodes. |
25 | - |
26 | - These are the operating systems for which any nodegroup has the boot images |
27 | - required to boot the node. |
28 | - """ |
29 | - # The Node edit form offers all usable operating systems as options for the |
30 | - # osystem field. Not all of these may be available in the node's |
31 | - # nodegroup, but to represent that accurately in the UI would depend on |
32 | - # the currently selected nodegroup. Narrowing the options down further |
33 | - # would have to happen browser-side. |
34 | - osystems = set() |
35 | - for nodegroup in NodeGroup.objects.all(): |
36 | - osystems = osystems.union( |
37 | - BootImage.objects.get_usable_osystems(nodegroup)) |
38 | - osystems = [OperatingSystemRegistry[osystem] for osystem in osystems] |
39 | - return sorted(osystems, key=lambda osystem: osystem.title) |
40 | - |
41 | - |
42 | -def list_osystem_choices(osystems): |
43 | - """Return Django "choices" list for `osystem`.""" |
44 | - choices = [('', 'Default OS')] |
45 | - choices += [ |
46 | - (osystem.name, osystem.title) |
47 | - for osystem in osystems |
48 | - ] |
49 | - return choices |
50 | - |
51 | - |
52 | -def list_all_usable_releases(osystems): |
53 | - """Return dictionary of usable `releases` for each opertaing system.""" |
54 | - distro_series = {} |
55 | - nodegroups = list(NodeGroup.objects.all()) |
56 | - for osystem in osystems: |
57 | - releases = set() |
58 | - for nodegroup in nodegroups: |
59 | - releases = releases.union( |
60 | - BootImage.objects.get_usable_releases(nodegroup, osystem.name)) |
61 | - distro_series[osystem.name] = sorted(releases) |
62 | - return distro_series |
63 | - |
64 | - |
65 | -def get_release_requires_key(osystem, release): |
66 | - """Return astrisk for any release that requires |
67 | - a license key. |
68 | - |
69 | - This is used by the JS, to display the licese_key field. |
70 | - """ |
71 | - if osystem.requires_license_key(release): |
72 | - return '*' |
73 | - return '' |
74 | - |
75 | - |
76 | -def list_release_choices(releases): |
77 | - """Return Django "choices" list for `releases`.""" |
78 | - choices = [('', 'Default OS Release')] |
79 | - for key, value in releases.items(): |
80 | - osystem = OperatingSystemRegistry[key] |
81 | - options = osystem.format_release_choices(value) |
82 | - requires_key = get_release_requires_key(osystem, '') |
83 | - choices += [( |
84 | - '%s/%s' % (osystem.name, requires_key), |
85 | - 'Latest %s Release' % osystem.title |
86 | - )] |
87 | - choices += [( |
88 | - '%s/%s%s' % ( |
89 | - osystem.name, |
90 | - name, |
91 | - get_release_requires_key(osystem, name) |
92 | - ), |
93 | - title |
94 | - ) |
95 | - for name, title in options |
96 | - ] |
97 | - return choices |
98 | - |
99 | - |
100 | -def get_distro_series_inital(instance): |
101 | - """Returns the distro_series initial value for the instance.""" |
102 | - osystem = instance.osystem |
103 | - series = instance.distro_series |
104 | - os_obj = OperatingSystemRegistry.get_item(osystem) |
105 | - if os_obj is not None: |
106 | - key_required = get_release_requires_key(os_obj, series) |
107 | - else: |
108 | - key_required = '' |
109 | - if osystem is not None and osystem != '': |
110 | - if series is None: |
111 | - series = '' |
112 | - return '%s/%s%s' % (osystem, series, key_required) |
113 | - return None |
114 | - |
115 | - |
116 | def clean_distro_series_field(form, field, os_field): |
117 | """Cleans the distro_series field in the form. Validating that |
118 | the selected operating system matches the distro_series. |
119 | @@ -1080,13 +995,18 @@ |
120 | |
121 | class DeployForm(ConfigForm): |
122 | """Settings page, Deploy section.""" |
123 | - default_osystem = get_config_field('default_osystem') |
124 | - default_distro_series = get_config_field('default_distro_series') |
125 | + |
126 | + def __init__(self, *args, **kwargs): |
127 | + Form.__init__(self, *args, **kwargs) |
128 | + self.fields['default_osystem'] = get_config_field('default_osystem') |
129 | + self.fields['default_distro_series'] = get_config_field( |
130 | + 'default_distro_series') |
131 | + self._load_initials() |
132 | |
133 | def _load_initials(self): |
134 | super(DeployForm, self)._load_initials() |
135 | - initial_os = self.initial['default_osystem'] |
136 | - initial_series = self.initial['default_distro_series'] |
137 | + initial_os = self.fields['default_osystem'].initial |
138 | + initial_series = self.fields['default_distro_series'].initial |
139 | self.initial['default_distro_series'] = '%s/%s' % ( |
140 | initial_os, |
141 | initial_series |
142 | |
143 | === modified file 'src/maasserver/forms_settings.py' |
144 | --- src/maasserver/forms_settings.py 2014-06-10 14:45:14 +0000 |
145 | +++ src/maasserver/forms_settings.py 2014-06-11 09:53:56 +0000 |
146 | @@ -23,43 +23,58 @@ |
147 | from socket import gethostname |
148 | |
149 | from django import forms |
150 | -from maasserver.models.config import DEFAULT_OS |
151 | +from maasserver.models.config import ( |
152 | + Config, |
153 | + DEFAULT_OS, |
154 | + ) |
155 | from maasserver.utils.forms import compose_invalid_choice_text |
156 | -from provisioningserver.drivers.osystem import OperatingSystemRegistry |
157 | +from maasserver.utils.os import ( |
158 | + list_all_usable_osystems, |
159 | + list_all_usable_releases, |
160 | + list_osystem_choices, |
161 | + list_release_choices, |
162 | + ) |
163 | |
164 | |
165 | INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)." |
166 | |
167 | |
168 | -def list_osystem_choices(): |
169 | - osystems = [osystem for _, osystem in OperatingSystemRegistry] |
170 | - osystems = sorted(osystems, key=lambda osystem: osystem.title) |
171 | - return [ |
172 | - (osystem.name, osystem.title) |
173 | - for osystem in osystems |
174 | - ] |
175 | - |
176 | - |
177 | -def list_release_choices(): |
178 | - osystems = [osystem for _, osystem in OperatingSystemRegistry] |
179 | - choices = [] |
180 | - for osystem in osystems: |
181 | - supported = sorted(osystem.get_supported_releases()) |
182 | - options = osystem.format_release_choices(supported) |
183 | - options = [ |
184 | - ('%s/%s' % (osystem.name, name), title) |
185 | - for name, title in options |
186 | - ] |
187 | - choices += options |
188 | - return choices |
189 | - |
190 | - |
191 | def list_commisioning_choices(): |
192 | releases = DEFAULT_OS.get_supported_commissioning_releases() |
193 | options = DEFAULT_OS.format_release_choices(releases) |
194 | return list(options) |
195 | |
196 | |
197 | +def make_default_osystem_field(*args, **kwargs): |
198 | + """Build and return the default_osystem field.""" |
199 | + usable_oses = list_all_usable_osystems() |
200 | + os_choices = list_osystem_choices(usable_oses) |
201 | + field = forms.ChoiceField( |
202 | + initial = Config.objects.get_config('default_osystem'), |
203 | + choices = os_choices, |
204 | + error_messages = { |
205 | + 'invalid_choice': compose_invalid_choice_text( |
206 | + 'osystem', os_choices) |
207 | + }, |
208 | + **kwargs) |
209 | + return field |
210 | + |
211 | + |
212 | +def make_default_distro_series_field(*args, **kwargs): |
213 | + usable_oses = list_all_usable_osystems() |
214 | + release_choices = list_release_choices( |
215 | + list_all_usable_releases(usable_oses)) |
216 | + field = forms.ChoiceField( |
217 | + initial = Config.objects.get_config('default_distro_series'), |
218 | + choices = release_choices, |
219 | + error_messages = { |
220 | + 'invalid_choice': compose_invalid_choice_text( |
221 | + 'release', release_choices) |
222 | + }, |
223 | + **kwargs) |
224 | + return field |
225 | + |
226 | + |
227 | CONFIG_ITEMS = { |
228 | 'check_compatibility': { |
229 | 'default': False, |
230 | @@ -162,32 +177,21 @@ |
231 | } |
232 | }, |
233 | 'default_osystem': { |
234 | - 'default': DEFAULT_OS.name, |
235 | - 'form': forms.ChoiceField, |
236 | + 'form': make_default_osystem_field, |
237 | 'form_kwargs': { |
238 | 'label': "Default operating system used for deployment", |
239 | - 'choices': list_osystem_choices(), |
240 | 'required': False, |
241 | - 'error_messages': { |
242 | - 'invalid_choice': compose_invalid_choice_text( |
243 | - 'osystem', |
244 | - list_osystem_choices())}, |
245 | + # This field's `choices` and `error_messages` are populated |
246 | + # at run-time to avoid a race condition. |
247 | } |
248 | }, |
249 | 'default_distro_series': { |
250 | - 'default': '%s/%s' % ( |
251 | - DEFAULT_OS.name, |
252 | - DEFAULT_OS.get_default_release() |
253 | - ), |
254 | - 'form': forms.ChoiceField, |
255 | + 'form': make_default_distro_series_field, |
256 | 'form_kwargs': { |
257 | 'label': "Default OS release used for deployment", |
258 | - 'choices': list_release_choices(), |
259 | 'required': False, |
260 | - 'error_messages': { |
261 | - 'invalid_choice': compose_invalid_choice_text( |
262 | - 'distro_series', |
263 | - list_release_choices())}, |
264 | + # This field's `choices` and `error_messages` are populated |
265 | + # at run-time to avoid a race condition. |
266 | } |
267 | }, |
268 | 'commissioning_distro_series': { |
269 | |
270 | === modified file 'src/maasserver/models/bootsourceselection.py' |
271 | --- src/maasserver/models/bootsourceselection.py 2014-06-10 14:45:14 +0000 |
272 | +++ src/maasserver/models/bootsourceselection.py 2014-06-11 09:53:56 +0000 |
273 | @@ -26,14 +26,6 @@ |
274 | from maasserver import DefaultMeta |
275 | from maasserver.models.cleansave import CleanSave |
276 | from maasserver.models.timestampedmodel import TimestampedModel |
277 | -from provisioningserver.drivers.osystem.ubuntu import UbuntuOS |
278 | - |
279 | - |
280 | -def list_release_choices(): |
281 | - """Return Django "choices" list for Ubuntu releases.""" |
282 | - osystem = UbuntuOS() |
283 | - releases = osystem.get_supported_releases() |
284 | - return osystem.format_release_choices(releases) |
285 | |
286 | |
287 | class BootSourceSelectionManager(Manager): |
288 | @@ -51,8 +43,7 @@ |
289 | boot_source = ForeignKey('maasserver.BootSource', blank=False) |
290 | |
291 | release = CharField( |
292 | - max_length=20, choices=list_release_choices(), blank=True, |
293 | - default='', |
294 | + max_length=20, blank=True, default='', |
295 | help_text="The Ubuntu release for which to import resources.") |
296 | |
297 | arches = djorm_pgarray.fields.ArrayField(dbtype="text") |
298 | |
299 | === modified file 'src/maasserver/testing/factory.py' |
300 | --- src/maasserver/testing/factory.py 2014-06-11 04:38:41 +0000 |
301 | +++ src/maasserver/testing/factory.py 2014-06-11 09:53:56 +0000 |
302 | @@ -64,7 +64,10 @@ |
303 | # XXX 2014-05-13 blake-rouse bug=1319143 |
304 | # Need to not import directly, use RPC to info from cluster. |
305 | from provisioningserver.drivers.osystem import OperatingSystemRegistry |
306 | -from provisioningserver.drivers.osystem.ubuntu import UbuntuOS |
307 | +from provisioningserver.drivers.osystem.ubuntu import ( |
308 | + UbuntuOS, |
309 | + ) |
310 | +from provisioningserver.testing.os import FakeOS |
311 | |
312 | # We have a limited number of public keys: |
313 | # src/maasserver/tests/data/test_rsa{0, 1, 2, 3, 4}.pub |
314 | @@ -170,6 +173,19 @@ |
315 | finally: |
316 | NODE_TRANSITIONS[None] = valid_initial_states |
317 | |
318 | + def make_operating_system(self, name=None, release=None): |
319 | + """Make an :class:`OperatingSystem`.""" |
320 | + if name is None: |
321 | + name = self.make_name('os') |
322 | + if release is None: |
323 | + release = self.make_name('release') |
324 | + self.make_boot_image(release=release, osystem=name) |
325 | + new_operating_system = FakeOS( |
326 | + name=name, purpose="install", releases=[release]) |
327 | + OperatingSystemRegistry.register_item( |
328 | + new_operating_system.name, new_operating_system) |
329 | + return new_operating_system |
330 | + |
331 | def make_node(self, mac=False, hostname=None, status=None, |
332 | architecture="i386/generic", updated=None, |
333 | created=None, nodegroup=None, routers=None, zone=None, |
334 | |
335 | === modified file 'src/maasserver/tests/test_forms.py' |
336 | --- src/maasserver/tests/test_forms.py 2014-06-11 04:38:41 +0000 |
337 | +++ src/maasserver/tests/test_forms.py 2014-06-11 09:53:56 +0000 |
338 | @@ -44,6 +44,7 @@ |
339 | CommissioningForm, |
340 | CommissioningScriptForm, |
341 | ConfigForm, |
342 | + DeployForm, |
343 | DownloadProgressForm, |
344 | EditUserForm, |
345 | get_action_form, |
346 | @@ -78,6 +79,7 @@ |
347 | ValidatorMultipleChoiceField, |
348 | ZoneForm, |
349 | ) |
350 | +from provisioningserver.drivers.osystem import ubuntu |
351 | from maasserver.models import ( |
352 | Config, |
353 | MACAddress, |
354 | @@ -240,6 +242,28 @@ |
355 | self.assertEqual( |
356 | [osystem], list_all_usable_osystems()) |
357 | |
358 | + def test_list_all_usable_osystems_omits_oses_without_boot_images(self): |
359 | + usable_os_name = factory.make_name('os') |
360 | + unusable_os_name = factory.make_name('os') |
361 | + self.make_usable_boot_images(osystem=usable_os_name) |
362 | + usable_os = make_usable_osystem(self, usable_os_name) |
363 | + unusable_os = make_usable_osystem(self, unusable_os_name) |
364 | + |
365 | + usable_os_list = list_all_usable_osystems() |
366 | + self.assertIn(usable_os, usable_os_list) |
367 | + self.assertNotIn(unusable_os, usable_os_list) |
368 | + |
369 | + def test_list_all_usable_osystems_omits_oses_not_supported(self): |
370 | + usable_os_name = factory.make_name('os') |
371 | + unusable_os_name = factory.make_name('os') |
372 | + self.make_usable_boot_images(osystem=usable_os_name) |
373 | + self.make_usable_boot_images(osystem=unusable_os_name) |
374 | + usable_os = make_usable_osystem(self, usable_os_name) |
375 | + |
376 | + usable_os_list = list_all_usable_osystems() |
377 | + self.assertIn(usable_os, usable_os_list) |
378 | + self.assertNotIn(unusable_os_name, [os.name for os in usable_os_list]) |
379 | + |
380 | def test_list_all_usable_releases_combines_nodegroups(self): |
381 | expected = {} |
382 | osystems = [] |
383 | @@ -2146,3 +2170,39 @@ |
384 | self.assertTrue(form.is_valid(), form._errors) |
385 | boot_source_selection = form.save() |
386 | self.assertAttributes(boot_source_selection, params) |
387 | + |
388 | + |
389 | +class TestDeployForm(MAASServerTestCase): |
390 | + """Tests for `DeployForm`.""" |
391 | + |
392 | + def test_uses_live_data(self): |
393 | + # The DeployForm uses the database rather than just relying on |
394 | + # hard-coded stuff. |
395 | + os_name = factory.make_name('os') |
396 | + release_name = factory.make_name('release') |
397 | + factory.make_operating_system( |
398 | + name=os_name, release=release_name) |
399 | + release_name = "%s/%s" % (os_name, release_name) |
400 | + deploy_form = DeployForm() |
401 | + os_names = [ |
402 | + name for name, title in deploy_form.fields['default_osystem'].choices] |
403 | + release_names = [ |
404 | + name for name, title in deploy_form.fields['default_distro_series'].choices] |
405 | + self.assertIn(os_name, os_names) |
406 | + self.assertIn(release_name, release_names) |
407 | + |
408 | + def test_accepts_new_values(self): |
409 | + os_name = 'ubuntu' |
410 | + release_name = factory.make_name('release') |
411 | + # XXX 2014-06-10 gmb: |
412 | + # We have to poke the release into the DISTRO_SERIES_CHOICES |
413 | + # dict, because otherwise it won't show up in the availble |
414 | + # releases on the DeployForm. |
415 | + ubuntu.DISTRO_SERIES_CHOICES[release_name] = release_name.title() |
416 | + factory.make_operating_system(name=os_name, release=release_name) |
417 | + params = { |
418 | + 'default_osystem': os_name, |
419 | + 'default_distro_series': "%s/%s" % (os_name, release_name), |
420 | + } |
421 | + form = DeployForm(data=params) |
422 | + self.assertTrue(form.is_valid()) |
423 | |
424 | === added file 'src/maasserver/utils/os.py' |
425 | --- src/maasserver/utils/os.py 1970-01-01 00:00:00 +0000 |
426 | +++ src/maasserver/utils/os.py 2014-06-11 09:53:56 +0000 |
427 | @@ -0,0 +1,126 @@ |
428 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
429 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
430 | +"""Utilities for working with operating systems.""" |
431 | + |
432 | +from __future__ import ( |
433 | + absolute_import, |
434 | + print_function, |
435 | + unicode_literals, |
436 | + ) |
437 | + |
438 | +str = None |
439 | + |
440 | +__metaclass__ = type |
441 | +__all__ = [ |
442 | + 'get_distro_series_inital', |
443 | + 'get_release_requires_key', |
444 | + 'list_all_usable_osystems', |
445 | + 'list_all_usable_releases', |
446 | + 'list_osystem_choices', |
447 | + 'list_release_choices', |
448 | + ] |
449 | + |
450 | + |
451 | +from maasserver.models import ( |
452 | + BootImage, |
453 | + NodeGroup, |
454 | + ) |
455 | +from provisioningserver.drivers.osystem import OperatingSystemRegistry |
456 | + |
457 | + |
458 | +def list_all_usable_osystems(): |
459 | + """Return all operating systems that can be used for nodes. |
460 | + |
461 | + These are the operating systems for which any nodegroup has the boot images |
462 | + required to boot the node. |
463 | + """ |
464 | + # The Node edit form offers all usable operating systems as options for the |
465 | + # osystem field. Not all of these may be available in the node's |
466 | + # nodegroup, but to represent that accurately in the UI would depend on |
467 | + # the currently selected nodegroup. Narrowing the options down further |
468 | + # would have to happen browser-side. |
469 | + osystems = set() |
470 | + for nodegroup in NodeGroup.objects.all(): |
471 | + osystems = osystems.union( |
472 | + BootImage.objects.get_usable_osystems(nodegroup)) |
473 | + osystems = [ |
474 | + OperatingSystemRegistry[osystem] for osystem in osystems |
475 | + if osystem in OperatingSystemRegistry |
476 | + ] |
477 | + return sorted(osystems, key=lambda osystem: osystem.title) |
478 | + |
479 | + |
480 | +def list_osystem_choices(osystems): |
481 | + """Return Django "choices" list for `osystem`.""" |
482 | + choices = [('', 'Default OS')] |
483 | + choices += [ |
484 | + (osystem.name, osystem.title) |
485 | + for osystem in osystems |
486 | + ] |
487 | + return choices |
488 | + |
489 | + |
490 | +def list_all_usable_releases(osystems): |
491 | + """Return dictionary of usable `releases` for each opertaing system.""" |
492 | + distro_series = {} |
493 | + nodegroups = list(NodeGroup.objects.all()) |
494 | + for osystem in osystems: |
495 | + releases = set() |
496 | + for nodegroup in nodegroups: |
497 | + releases = releases.union( |
498 | + BootImage.objects.get_usable_releases(nodegroup, osystem.name)) |
499 | + distro_series[osystem.name] = sorted(releases) |
500 | + return distro_series |
501 | + |
502 | + |
503 | +def get_release_requires_key(osystem, release): |
504 | + """Return astrisk for any release that requires |
505 | + a license key. |
506 | + |
507 | + This is used by the JS, to display the licese_key field. |
508 | + """ |
509 | + if osystem.requires_license_key(release): |
510 | + return '*' |
511 | + return '' |
512 | + |
513 | + |
514 | +def list_release_choices(releases): |
515 | + """Return Django "choices" list for `releases`.""" |
516 | + choices = [('', 'Default OS Release')] |
517 | + for key, value in releases.items(): |
518 | + osystem = OperatingSystemRegistry[key] |
519 | + options = osystem.format_release_choices(value) |
520 | + requires_key = get_release_requires_key(osystem, '') |
521 | + choices += [( |
522 | + '%s/%s' % (osystem.name, requires_key), |
523 | + 'Latest %s Release' % osystem.title |
524 | + )] |
525 | + choices += [( |
526 | + '%s/%s%s' % ( |
527 | + osystem.name, |
528 | + name, |
529 | + get_release_requires_key(osystem, name) |
530 | + ), |
531 | + title |
532 | + ) |
533 | + for name, title in options |
534 | + ] |
535 | + return choices |
536 | + |
537 | + |
538 | +def get_distro_series_inital(instance): |
539 | + """Returns the distro_series initial value for the instance.""" |
540 | + osystem = instance.osystem |
541 | + series = instance.distro_series |
542 | + os_obj = OperatingSystemRegistry.get_item(osystem) |
543 | + if os_obj is not None: |
544 | + key_required = get_release_requires_key(os_obj, series) |
545 | + else: |
546 | + key_required = '' |
547 | + if osystem is not None and osystem != '': |
548 | + if series is None: |
549 | + series = '' |
550 | + return '%s/%s%s' % (osystem, series, key_required) |
551 | + return None |
552 | + |
553 | + |
554 | |
555 | === modified file 'src/maasserver/views/tests/test_settings.py' |
556 | --- src/maasserver/views/tests/test_settings.py 2014-05-29 18:20:58 +0000 |
557 | +++ src/maasserver/views/tests/test_settings.py 2014-06-11 09:53:56 +0000 |
558 | @@ -154,9 +154,11 @@ |
559 | |
560 | def test_settings_deploy_POST(self): |
561 | self.client_log_in(as_admin=True) |
562 | - new_osystem = factory.getRandomOS() |
563 | + osystem_name = factory.make_name('os') |
564 | + release_name = factory.make_name('release') |
565 | + new_osystem = factory.make_operating_system( |
566 | + name=osystem_name, release=release_name) |
567 | new_default_osystem = new_osystem.name |
568 | - new_default_distro_series = factory.getRandomRelease(new_osystem) |
569 | response = self.client.post( |
570 | reverse('settings'), |
571 | get_prefixed_form_data( |
572 | @@ -165,7 +167,7 @@ |
573 | 'default_osystem': new_default_osystem, |
574 | 'default_distro_series': '%s/%s' % ( |
575 | new_default_osystem, |
576 | - new_default_distro_series |
577 | + release_name, |
578 | ), |
579 | })) |
580 | |
581 | @@ -173,7 +175,7 @@ |
582 | self.assertEqual( |
583 | ( |
584 | new_default_osystem, |
585 | - new_default_distro_series, |
586 | + release_name, |
587 | ), |
588 | ( |
589 | Config.objects.get_config('default_osystem'), |
590 | |
591 | === modified file 'src/provisioningserver/boot/tests/test_tftppath.py' |
592 | --- src/provisioningserver/boot/tests/test_tftppath.py 2014-06-10 14:45:14 +0000 |
593 | +++ src/provisioningserver/boot/tests/test_tftppath.py 2014-06-11 09:53:56 +0000 |
594 | @@ -33,7 +33,6 @@ |
595 | locate_tftp_path, |
596 | ) |
597 | from provisioningserver.drivers.osystem import ( |
598 | - OperatingSystem, |
599 | OperatingSystemRegistry, |
600 | ) |
601 | from provisioningserver.import_images.boot_image_mapping import ( |
602 | @@ -48,6 +47,7 @@ |
603 | make_boot_image_storage_params, |
604 | ) |
605 | from provisioningserver.testing.config import set_tftp_root |
606 | +from provisioningserver.testing.os import FakeOS |
607 | from testtools.matchers import ( |
608 | Not, |
609 | StartsWith, |
610 | @@ -55,40 +55,6 @@ |
611 | from testtools.testcase import ExpectedException |
612 | |
613 | |
614 | -class FakeOS(OperatingSystem): |
615 | - |
616 | - name = "" |
617 | - title = "" |
618 | - |
619 | - def __init__(self, name, purpose, releases=None): |
620 | - self.name = name |
621 | - self.title = name |
622 | - self.purpose = purpose |
623 | - if releases is None: |
624 | - self.fake_list = [ |
625 | - factory.getRandomString() |
626 | - for _ in range(3) |
627 | - ] |
628 | - else: |
629 | - self.fake_list = releases |
630 | - |
631 | - def get_boot_image_purposes(self, *args): |
632 | - return self.purpose |
633 | - |
634 | - def get_supported_releases(self): |
635 | - return self.fake_list |
636 | - |
637 | - def get_default_release(self): |
638 | - return self.fake_list[0] |
639 | - |
640 | - def format_release_choices(self, releases): |
641 | - return [ |
642 | - (release, release) |
643 | - for release in releases |
644 | - if release in self.fake_list |
645 | - ] |
646 | - |
647 | - |
648 | def make_image(params, purpose, metadata=None): |
649 | """Describe an image as a dict similar to what `list_boot_images` returns. |
650 | |
651 | |
652 | === added file 'src/provisioningserver/testing/os.py' |
653 | --- src/provisioningserver/testing/os.py 1970-01-01 00:00:00 +0000 |
654 | +++ src/provisioningserver/testing/os.py 2014-06-11 09:53:56 +0000 |
655 | @@ -0,0 +1,54 @@ |
656 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
657 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
658 | +"""Utilities for testing operating systems-related code.""" |
659 | + |
660 | +from __future__ import ( |
661 | + absolute_import, |
662 | + print_function, |
663 | + unicode_literals, |
664 | + ) |
665 | + |
666 | +str = None |
667 | + |
668 | +__metaclass__ = type |
669 | +__all__ = [ |
670 | + 'FakeOS', |
671 | + ] |
672 | + |
673 | + |
674 | +from maastesting.factory import factory |
675 | +from provisioningserver.drivers.osystem import OperatingSystem |
676 | + |
677 | + |
678 | +class FakeOS(OperatingSystem): |
679 | + |
680 | + name = "" |
681 | + title = "" |
682 | + |
683 | + def __init__(self, name, purpose, releases=None): |
684 | + self.name = name |
685 | + self.title = name |
686 | + self.purpose = purpose |
687 | + if releases is None: |
688 | + self.fake_list = [ |
689 | + factory.getRandomString() |
690 | + for _ in range(3) |
691 | + ] |
692 | + else: |
693 | + self.fake_list = releases |
694 | + |
695 | + def get_boot_image_purposes(self, *args): |
696 | + return self.purpose |
697 | + |
698 | + def get_supported_releases(self): |
699 | + return self.fake_list |
700 | + |
701 | + def get_default_release(self): |
702 | + return self.fake_list[0] |
703 | + |
704 | + def format_release_choices(self, releases): |
705 | + return [ |
706 | + (release, release) |
707 | + for release in releases |
708 | + if release in self.fake_list |
709 | + ] |
Voting does not meet specified criteria. Required: Approve >= 1, Disapprove == 0. Got: 1 Pending.