← Back to team overview

launchpad-reviewers team mailing list archive

[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" />
+      &nbsp;&nbsp;<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))