launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06550
[Merge] lp:~rvb/maas/maas-settings-ubuntu into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-settings-ubuntu into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-settings-ubuntu/+merge/95172
Add the Ubuntu section on the settings page.
--
https://code.launchpad.net/~rvb/maas/maas-settings-ubuntu/+merge/95172
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-settings-ubuntu into lp:maas.
=== modified file 'src/maasserver/fields.py'
--- src/maasserver/fields.py 2012-02-21 11:52:58 +0000
+++ src/maasserver/fields.py 2012-02-29 13:51:17 +0000
@@ -1,7 +1,7 @@
# Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""..."""
+"""Custom model fields."""
from __future__ import (
print_function,
@@ -33,7 +33,7 @@
mac_re = re.compile(r'^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$')
-mac_error_msg = u"Enter a valid MAC address (e.g. AA:BB:CC:DD:EE:FF)."
+mac_error_msg = "Enter a valid MAC address (e.g. AA:BB:CC:DD:EE:FF)."
validate_mac = RegexValidator(regex=mac_re, message=mac_error_msg)
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-02-29 13:40:40 +0000
+++ src/maasserver/forms.py 2012-02-29 13:51:17 +0000
@@ -11,9 +11,11 @@
__metaclass__ = type
__all__ = [
"CommissioningForm",
+ "HostnameFormField",
"NodeForm",
"MACAddressForm",
"MaaSAndNetworkForm",
+ "UbuntuForm",
]
from django import forms
@@ -22,7 +24,10 @@
UserCreationForm,
)
from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
+from django.core.validators import URLValidator
from django.forms import (
+ CharField,
Form,
ModelForm,
)
@@ -180,3 +185,54 @@
check_compatibility = forms.BooleanField(
label="Check component compatibility and certification",
required=False)
+
+
+class UbuntuForm(ConfigForm):
+ fallback_master_archive = forms.BooleanField(
+ label="Fallback to Ubuntu master archive",
+ required=False)
+ keep_mirror_list_uptodate = forms.BooleanField(
+ label="Keep mirror list up to date",
+ required=False)
+ fetch_new_releases = forms.BooleanField(
+ label="Fetch new releases automatically",
+ required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(UbuntuForm, self).__init__(*args, **kwargs)
+ # The field 'update_from' must be added dynamically because its
+ # 'choices' must be evaluated each time the form is instanciated.
+ self.fields['update_from'] = forms.ChoiceField(
+ label="Update from",
+ choices=Config.objects.get_config('update_from_choice'))
+ # The list of fields has changed: load initial values.
+ self._load_initials()
+
+
+hostname_error_msg = "Enter a valid hostname (e.g. my.hostname.com)."
+
+
+def validate_hostname(value):
+ try:
+ validator = URLValidator(verify_exists=False)
+ validator('http://%s' % value)
+ except ValidationError:
+ raise ValidationError(hostname_error_msg)
+
+
+class HostnameFormField(CharField):
+
+ def __init__(self, *args, **kwargs):
+ super(HostnameFormField, self).__init__(
+ validators=[validate_hostname], *args, **kwargs)
+
+
+class AddArchiveForm(ConfigForm):
+ archive_name = HostnameFormField(label="Archive name")
+
+ def save(self):
+ """Save the archive name in the Config table."""
+ archive_name = self.cleaned_data.get('archive_name')
+ archives = Config.objects.get_config('update_from_choice')
+ archives.append([archive_name, archive_name])
+ Config.objects.set_config('update_from_choice', archives)
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py 2012-02-28 17:40:39 +0000
+++ src/maasserver/models.py 2012-02-29 13:51:17 +0000
@@ -21,6 +21,7 @@
"UserProfile",
]
+import copy
import datetime
import re
from socket import gethostname
@@ -586,9 +587,13 @@
# Default values for config options.
DEFAULT_CONFIG = {
+ # settings page config default values.
+ 'update_from': 'archive.ubuntu.com',
+ 'update_from_choice': (
+ [['archive.ubuntu.com', 'archive.ubuntu.com']])
# The host name or address where the nodes can access the metadata
# service.
- "metadata-host": gethostname(),
+ 'metadata-host': gethostname(),
}
@@ -613,7 +618,7 @@
try:
return self.get(name=name).value
except Config.DoesNotExist:
- return DEFAULT_CONFIG.get(name, default)
+ return copy.deepcopy(DEFAULT_CONFIG.get(name, default))
def get_config_list(self, name):
"""Return the config value list corresponding to the given config
=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html 2012-02-28 14:50:07 +0000
+++ src/maasserver/templates/maasserver/settings.html 2012-02-29 13:51:17 +0000
@@ -80,6 +80,33 @@
<div class="clear"></div>
</form>
</div>
+ <div id="ubuntu">
+ <h2>Ubuntu</h2>
+ <form action="{% url "settings" %}" method="post">
+ <ul>
+ {% with field=ubuntu_form.update_form %}
+ {% include "maasserver/form_field.html" %}
+ {% endwith %}
+ <p>
+ <a href="{% url "settings_add_archive" %}">
+ Add archive for newly provisioned machines
+ </a>
+ </p>
+ {% with field=ubuntu_form.fallback_master_archive %}
+ {% include "maasserver/form_field.html" %}
+ {% endwith %}
+ {% with field=ubuntu_form.keep_mirror_list_uptodate %}
+ {% include "maasserver/form_field.html" %}
+ {% endwith %}
+ {% with field=ubuntu_form.fetch_new_releases %}
+ {% include "maasserver/form_field.html" %}
+ {% endwith %}
+ </ul>
+ <input type="hidden" name="ubuntu_submit" value="1" />
+ <input type="submit" class="button right" value="Save" />
+ <div class="clear"></div>
+ </form>
+ </div>
<div id="maas_and_network">
<h2>Network Configuration</h2>
<form action="{% url "settings" %}" method="post">
=== added file 'src/maasserver/templates/maasserver/settings_add_archive.html'
--- src/maasserver/templates/maasserver/settings_add_archive.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_add_archive.html 2012-02-29 13:51:17 +0000
@@ -0,0 +1,17 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Add archive{% endblock %}
+{% block page-title %}Add archive{% endblock %}
+
+{% block content %}
+ <form action="." method="post">
+ <ul>
+ {% for field in form %}
+ {% include "maasserver/form_field.html" %}
+ {% endfor %}
+ </ul>
+ <input type="submit" value="Add archive" />
+ <a class="link-button" href="{% url 'settings' %}">Cancel</a>
+ </form>
+{% endblock %}
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-02-29 12:00:36 +0000
+++ src/maasserver/testing/factory.py 2012-02-29 13:51:17 +0000
@@ -42,6 +42,13 @@
if not key.startswith('__')]
return random.choice(enum_choices)
+ def getRandomChoice(self, choices):
+ # Get a random choice from the passed-in 'choices'. 'choices'
+ # must use Django form choices format:
+ # [('choice_id_1': "Choice name 1"), ('choice_id_2', "Choice
+ # name 2")]. A random choice id will be returned.
+ return random.choice(choices)[0]
+
def make_node(self, hostname='', set_hostname=False, status=None,
**kwargs):
# hostname=None is a valid value, hence the set_hostname trick.
=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py 2012-02-21 11:52:58 +0000
+++ src/maasserver/tests/test_fields.py 2012-02-29 13:51:17 +0000
@@ -1,7 +1,7 @@
# Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Test custom fields."""
+"""Test custom model fields."""
from __future__ import (
print_function,
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2012-02-29 07:49:59 +0000
+++ src/maasserver/tests/test_forms.py 2012-02-29 13:51:17 +0000
@@ -12,10 +12,13 @@
__all__ = []
from django import forms
+from django.core.exceptions import ValidationError
from django.http import QueryDict
from maasserver.forms import (
ConfigForm,
+ HostnameFormField,
NodeWithMACAddressesForm,
+ validate_hostname,
)
from maasserver.models import (
Config,
@@ -138,3 +141,34 @@
self.assertItemsEqual(['field1'], form.initial)
self.assertEqual(value, form.initial['field1'])
+
+
+class FormWithHostname(forms.Form):
+ hostname = HostnameFormField()
+
+
+class TestHostnameFormField(TestCase):
+
+ def test_validate_hostname_validates_valid_hostnames(self):
+ self.assertIsNone(validate_hostname('test.myhost.com'))
+ self.assertIsNone(validate_hostname('test.my-host.com'))
+ self.assertIsNone(validate_hostname('my-host.com'))
+
+ def test_validate_hostname_does_not_validate_invalid_hostnames(self):
+ self.assertRaises(ValidationError, validate_hostname, 'invalid-host')
+ self.assertRaises(ValidationError, validate_hostname, 'toolong' * 100)
+
+ def test_hostname_field_validation_cleaned_data_if_hostname_valid(self):
+ form = FormWithHostname({'hostname': 'my.hostname.com'})
+
+ self.assertTrue(form.is_valid())
+ self.assertEqual('my.host.name', form.cleaned_data['hostname'])
+
+ def test_hostname_field_validation_error_if_invalid_hostname(self):
+ form = FormWithHostname({'hostname': 'invalid-host'})
+
+ self.assertFalse(form.is_valid())
+ self.assertItemsEqual(['hostname'], form.errors.keys())
+ self.assertEqual(
+ ["Enter a valid hostname (e.g. my.hostname.com)."],
+ form.errors['hostname'])
=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py 2012-02-29 10:40:06 +0000
+++ src/maasserver/tests/test_models.py 2012-02-29 13:51:17 +0000
@@ -457,6 +457,14 @@
config = Config.objects.get_config(name, None)
self.assertEqual(value, config)
+ def test_default_config_cannot_be_changed(self):
+ name = factory.getRandomString()
+ DEFAULT_CONFIG[name] = {'key': 'value'}
+ config = Config.objects.get_config(name)
+ config.update({'key2': 'value2'})
+
+ self.assertEqual({'key': 'value'}, Config.objects.get_config(name))
+
def test_manager_get_config_list_returns_config_list(self):
Config.objects.create(name='name', value='config1')
Config.objects.create(name='name', value='config2')
=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py 2012-02-29 12:00:36 +0000
+++ src/maasserver/tests/test_views.py 2012-02-29 13:51:17 +0000
@@ -198,6 +198,49 @@
new_check_compatibility,
Config.objects.get_config('check_compatibility'))
+ def test_settings_ubuntu_POST(self):
+ new_fallback_master_archive = factory.getRandomBoolean()
+ new_keep_mirror_list_uptodate = factory.getRandomBoolean()
+ new_fetch_new_releases = factory.getRandomBoolean()
+ choices = Config.objects.get_config('update_from_choice')
+ new_update_from = factory.getRandomChoice(choices)
+ response = self.client.post(
+ '/settings/',
+ get_prefixed_form_data(
+ prefix='ubuntu',
+ data={
+ 'fallback_master_archive': new_fallback_master_archive,
+ 'keep_mirror_list_uptodate': new_keep_mirror_list_uptodate,
+ 'fetch_new_releases': new_fetch_new_releases,
+ 'update_from': new_update_from,
+ }))
+
+ self.assertEqual(httplib.FOUND, response.status_code)
+ self.assertEqual(
+ new_fallback_master_archive,
+ Config.objects.get_config('fallback_master_archive'))
+ self.assertEqual(
+ new_keep_mirror_list_uptodate,
+ Config.objects.get_config('keep_mirror_list_uptodate'))
+ self.assertEqual(
+ new_fetch_new_releases,
+ Config.objects.get_config('fetch_new_releases'))
+ self.assertEqual(
+ new_update_from, Config.objects.get_config('update_from'))
+
+ def test_settings_add_archive_POST(self):
+ choices = Config.objects.get_config('update_from_choice')
+ response = self.client.post(
+ '/settings/add_archive/',
+ data={'archive_name': 'my.hostname.com'}
+ )
+ new_choices = Config.objects.get_config('update_from_choice')
+
+ self.assertEqual(httplib.FOUND, response.status_code)
+ self.assertItemsEqual(
+ choices + [['my.hostname.com', 'my.hostname.com']],
+ new_choices)
+
class UserManagementTest(AdminLoggedInTestCase):
=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py 2012-02-24 15:23:10 +0000
+++ src/maasserver/urls.py 2012-02-29 13:51:17 +0000
@@ -32,6 +32,7 @@
NodeListView,
NodesCreateView,
settings,
+ settings_add_archive,
userprefsview,
)
@@ -69,6 +70,9 @@
# URLs for admin users.
urlpatterns += patterns('maasserver.views',
adminurl(r'^settings/$', settings, name='settings'),
+ adminurl(
+ r'^settings/add_archive/$', settings_add_archive,
+ name='settings_add_archive'),
adminurl(r'^accounts/add/$', AccountsAdd.as_view(), name='accounts-add'),
adminurl(
r'^accounts/(?P<username>\w+)/edit/$', AccountsEdit.as_view(),
=== modified file 'src/maasserver/views.py'
--- src/maasserver/views.py 2012-02-28 14:50:07 +0000
+++ src/maasserver/views.py 2012-02-29 13:51:17 +0000
@@ -35,11 +35,13 @@
)
from maasserver.exceptions import CannotDeleteUserException
from maasserver.forms import (
+ AddArchiveForm,
CommissioningForm,
EditUserForm,
MaaSAndNetworkForm,
NewUserCreationForm,
ProfileForm,
+ UbuntuForm,
)
from maasserver.models import (
Node,
@@ -189,11 +191,40 @@
else:
commissioning_form = CommissioningForm(prefix='commissioning')
+ # Process the Ubuntu form.
+ if 'ubuntu_submit' in request.POST:
+ ubuntu_form = UbuntuForm(
+ request.POST, prefix='ubuntu')
+ if ubuntu_form.is_valid():
+ messages.info(request, "Configuration updated.")
+ ubuntu_form.save()
+ return HttpResponseRedirect(reverse('settings'))
+ else:
+ ubuntu_form = UbuntuForm(
+ prefix='ubuntu')
+
return render_to_response(
'maasserver/settings.html',
{
'user_list': user_list,
'maas_and_network_form': maas_and_network_form,
'commissioning_form': commissioning_form,
+ 'ubuntu_form': ubuntu_form,
},
context_instance=RequestContext(request))
+
+
+def settings_add_archive(request):
+ if request.method == 'POST':
+ form = AddArchiveForm(request.POST)
+ if form.is_valid():
+ form.save()
+ messages.info(request, "Archive added.")
+ return HttpResponseRedirect(reverse('settings'))
+ else:
+ form = AddArchiveForm()
+
+ return render_to_response(
+ 'maasserver/settings_add_archive.html',
+ {'form': form},
+ context_instance=RequestContext(request))