=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2014-10-02 10:13:50 +0000
+++ src/maasserver/forms.py 2014-10-03 00:26:15 +0000
@@ -440,6 +440,13 @@
return is_valid
def clean_license_key(self):
+ """Validates the license_key field is the correct format for the
+ selected operating system."""
+ # We allow the license_key field to be blank, even if the OS requires
+ # a license key. This is to allow for situations where the OS has a
+ # license key installed in the image that gets deployed, or where the
+ # OS is activated using some other activation service (for example
+ # Windows KMS activation).
key = self.cleaned_data.get('license_key')
if key == '':
return ''
@@ -513,7 +520,7 @@
"FQDN."))
license_key = forms.CharField(
- label="License Key (Required)", required=False, help_text=(
+ label="License Key", required=False, help_text=(
"License key for operating system"),
max_length=30)
=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html 2014-09-26 20:39:22 +0000
+++ src/maasserver/templates/maasserver/settings.html 2014-10-03 00:26:15 +0000
@@ -91,6 +91,13 @@
{% include "maasserver/settings_commissioning_scripts.html" %}
Commissioning
=== added file 'src/maasserver/templates/maasserver/settings_add_license_key.html'
--- src/maasserver/templates/maasserver/settings_add_license_key.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_add_license_key.html 2014-10-03 00:26:15 +0000
@@ -0,0 +1,20 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Add license key{% endblock %}
+{% block page-title %}Add license key{% endblock %}
+
+{% block content %}
+
+{% endblock %}
=== added file 'src/maasserver/templates/maasserver/settings_confirm_delete_license_key.html'
--- src/maasserver/templates/maasserver/settings_confirm_delete_license_key.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_confirm_delete_license_key.html 2014-10-03 00:26:15 +0000
@@ -0,0 +1,23 @@
+{% extends "maasserver/base.html" %}
+
+{% block title %}Delete license key{% endblock %}
+{% block page-title %}Delete license key{% endblock %}
+
+{% block content %}
+
+
+ Are you sure you want to delete the license key
+ '{{ license_key_to_delete.osystem }} / {{ license_key_to_delete.distro_series }}'?
+
+
+
This action is permanent and can not be undone.
+
+
+
+
+{% endblock %}
=== added file 'src/maasserver/templates/maasserver/settings_edit_license_key.html'
--- src/maasserver/templates/maasserver/settings_edit_license_key.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_edit_license_key.html 2014-10-03 00:26:15 +0000
@@ -0,0 +1,20 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Edit license key{% endblock %}
+{% block page-title %}Edit license key{% endblock %}
+
+{% block content %}
+
+{% endblock %}
=== added file 'src/maasserver/templates/maasserver/settings_license_keys.html'
--- src/maasserver/templates/maasserver/settings_license_keys.html 1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_license_keys.html 2014-10-03 00:26:15 +0000
@@ -0,0 +1,35 @@
+
+
License Keys
+
+
+ {% for key in license_keys %}
+
+ {{ key.osystem_title }}
+ {{ key.distro_series_title }}
+
+
+
+
+
+
+
+
+ {% empty %}
+ No license keys.
+ {% endfor %}
+
+
+
+
+
=== modified file 'src/maasserver/templates/maasserver/snippets.html'
--- src/maasserver/templates/maasserver/snippets.html 2014-09-02 09:22:49 +0000
+++ src/maasserver/templates/maasserver/snippets.html 2014-10-03 00:26:15 +0000
@@ -25,7 +25,7 @@
{{ node_form.distro_series }}
- License Key (Required)
+ License Key
{{ node_form.license_key }}
=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py 2014-09-26 19:30:50 +0000
+++ src/maasserver/urls.py 2014-10-03 00:26:15 +0000
@@ -85,6 +85,11 @@
CommissioningScriptCreate,
CommissioningScriptDelete,
)
+from maasserver.views.settings_license_keys import (
+ LicenseKeyCreate,
+ LicenseKeyDelete,
+ LicenseKeyEdit,
+ )
from maasserver.views.tags import TagView
from maasserver.views.zones import (
ZoneAdd,
@@ -257,6 +262,18 @@
r'^commissioning-results/$',
NodeCommissionResultListView.as_view(),
name='nodecommissionresult-list'),
+ adminurl(
+ r'^license-key/(?P[^/]+)/(?P[^/]+)/delete/$',
+ LicenseKeyDelete.as_view(),
+ name='license-key-delete'),
+ adminurl(
+ r'^license-key/(?P[^/]+)/(?P[^/]+)/edit/$',
+ LicenseKeyEdit.as_view(),
+ name='license-key-edit'),
+ adminurl(
+ r'^license-key/add/$',
+ LicenseKeyCreate.as_view(),
+ name='license-key-add'),
)
# Tag views.
=== modified file 'src/maasserver/views/settings.py'
--- src/maasserver/views/settings.py 2014-09-26 20:33:56 +0000
+++ src/maasserver/views/settings.py 2014-10-03 00:26:15 +0000
@@ -38,6 +38,7 @@
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
+from maasserver.clusterrpc.osystems import gen_all_known_operating_systems
from maasserver.exceptions import CannotDeleteUserException
from maasserver.forms import (
CommissioningForm,
@@ -51,7 +52,14 @@
UbuntuForm,
WindowsForm,
)
-from maasserver.models import UserProfile
+from maasserver.models import (
+ LicenseKey,
+ UserProfile,
+ )
+from maasserver.utils.osystems import (
+ get_osystem_from_osystems,
+ get_release_from_osystem,
+ )
from maasserver.views import process_form
from metadataserver.models import CommissioningScript
@@ -156,6 +164,35 @@
return self.respond(request, profile_form, password_form)
+def has_osystems_supporting_license_keys(osystems):
+ """Return True if the given osystems supports releases with license keys.
+ """
+ for osystem in osystems:
+ for release in osystem['releases']:
+ if release['requires_license_key']:
+ return True
+ return False
+
+
+def set_license_key_titles(license_key, osystems):
+ """Sets the osystem_title and distro_series_title field on the
+ license_key.
+
+ Uses the given "osystems" to get the titles.
+ """
+ osystem = get_osystem_from_osystems(osystems, license_key.osystem)
+ if osystem is None:
+ license_key.osystem_title = license_key.osystem
+ license_key.distro_series_title = license_key.distro_series
+ return
+ license_key.osystem_title = osystem['title']
+ release = get_release_from_osystem(osystem, license_key.distro_series)
+ if release is None:
+ license_key.distro_series_title = license_key.distro_series
+ return
+ license_key.distro_series_title = release['title']
+
+
def settings(request):
user_list = UserProfile.objects.all_users().order_by('username')
@@ -218,11 +255,20 @@
# Commissioning scripts.
commissioning_scripts = CommissioningScript.objects.all()
+ # License keys w/ titles for osystem and distro_series
+ osystems = list(gen_all_known_operating_systems())
+ show_license_keys = has_osystems_supporting_license_keys(osystems)
+ license_keys = LicenseKey.objects.all()
+ for license_key in license_keys:
+ set_license_key_titles(license_key, osystems)
+
return render_to_response(
'maasserver/settings.html',
{
'user_list': user_list,
'commissioning_scripts': commissioning_scripts,
+ 'show_license_keys': show_license_keys,
+ 'license_keys': license_keys,
'maas_and_network_form': maas_and_network_form,
'third_party_drivers_form': third_party_drivers_form,
'disk_erasing_on_release_form': disk_erasing_on_release_form,
=== added file 'src/maasserver/views/settings_license_keys.py'
--- src/maasserver/views/settings_license_keys.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/views/settings_license_keys.py 2014-10-03 00:26:15 +0000
@@ -0,0 +1,95 @@
+# Copyright 2014 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""License Key Settings views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+str = None
+
+__metaclass__ = type
+__all__ = [
+ "LicenseKeyCreate",
+ "LicenseKeyDelete",
+ "LicenseKeyEdit",
+ ]
+
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.views.generic import (
+ CreateView,
+ DeleteView,
+ UpdateView,
+ )
+from maasserver.forms import LicenseKeyForm
+from maasserver.models import LicenseKey
+
+# The anchor of the license keys slot on the settings page.
+LICENSE_KEY_ANCHOR = 'license_keys'
+
+
+class LicenseKeyDelete(DeleteView):
+
+ template_name = (
+ 'maasserver/settings_confirm_delete_license_key.html')
+ context_object_name = 'license_key_to_delete'
+
+ def get_object(self):
+ osystem = self.kwargs.get('osystem', None)
+ distro_series = self.kwargs.get('distro_series', None)
+ return get_object_or_404(
+ LicenseKey, osystem=osystem, distro_series=distro_series)
+
+ def get_next_url(self):
+ return reverse('settings') + '#' + LICENSE_KEY_ANCHOR
+
+ def delete(self, request, *args, **kwargs):
+ license_key = self.get_object()
+ license_key.delete()
+ messages.info(
+ request,
+ "License key %s/%s deleted." % (
+ license_key.osystem,
+ license_key.distro_series,
+ ))
+ return HttpResponseRedirect(self.get_next_url())
+
+
+class LicenseKeyCreate(CreateView):
+ template_name = 'maasserver/settings_add_license_key.html'
+ form_class = LicenseKeyForm
+ context_object_name = 'licensekey'
+
+ def get_success_url(self):
+ return reverse('settings') + '#' + LICENSE_KEY_ANCHOR
+
+ def form_valid(self, form):
+ messages.info(self.request, "License key created.")
+ return super(LicenseKeyCreate, self).form_valid(form)
+
+
+class LicenseKeyEdit(UpdateView):
+ """View for editing a license key."""
+
+ model = LicenseKey
+ form_class = LicenseKeyForm
+ template_name = 'maasserver/settings_edit_license_key.html'
+
+ def get_object(self):
+ osystem = self.kwargs.get('osystem', None)
+ distro_series = self.kwargs.get('distro_series', None)
+ return get_object_or_404(
+ LicenseKey, osystem=osystem, distro_series=distro_series)
+
+ def get_success_url(self):
+ return reverse('settings') + '#' + LICENSE_KEY_ANCHOR
+
+ def form_valid(self, form):
+ messages.info(self.request, "License key updated.")
+ return super(LicenseKeyEdit, self).form_valid(form)
=== modified file 'src/maasserver/views/tests/test_settings.py'
--- src/maasserver/views/tests/test_settings.py 2014-09-26 20:48:20 +0000
+++ src/maasserver/views/tests/test_settings.py 2014-10-03 00:26:15 +0000
@@ -39,6 +39,7 @@
patch_usable_osystems,
)
from maasserver.testing.testcase import MAASServerTestCase
+from maasserver.views import settings as settings_view
class SettingsTest(MAASServerTestCase):
@@ -142,6 +143,27 @@
Config.objects.get_config('commissioning_distro_series'),
))
+ def test_settings_hides_license_keys_if_no_OS_supporting_keys(self):
+ self.client_log_in(as_admin=True)
+ response = self.client.get(reverse('settings'))
+ doc = fromstring(response.content)
+ license_keys = doc.cssselect('#license_keys')
+ self.assertEqual(
+ 0, len(license_keys), "Didn't hide the license key section.")
+
+ def test_settings_shows_license_keys_if_OS_supporting_keys(self):
+ self.client_log_in(as_admin=True)
+ release = make_rpc_release(requires_license_key=True)
+ osystem = make_rpc_osystem(releases=[release])
+ self.patch(
+ settings_view,
+ 'gen_all_known_operating_systems').return_value = [osystem]
+ response = self.client.get(reverse('settings'))
+ doc = fromstring(response.content)
+ license_keys = doc.cssselect('#license_keys')
+ self.assertEqual(
+ 1, len(license_keys), "Didn't show the license key section.")
+
def test_settings_third_party_drivers_POST(self):
self.client_log_in(as_admin=True)
new_enable_third_party_drivers = factory.pick_bool()
=== added file 'src/maasserver/views/tests/test_settings_license_keys.py'
--- src/maasserver/views/tests/test_settings_license_keys.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/views/tests/test_settings_license_keys.py 2014-10-03 00:26:15 +0000
@@ -0,0 +1,184 @@
+# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver license key settings views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+str = None
+
+__metaclass__ = type
+__all__ = []
+
+import httplib
+
+from django.core.urlresolvers import reverse
+from lxml.html import fromstring
+from maasserver import forms
+from maasserver.clusterrpc.testing.osystems import (
+ make_rpc_osystem,
+ make_rpc_release,
+ )
+from maasserver.models import LicenseKey
+from maasserver.testing import (
+ extract_redirect,
+ get_content_links,
+ )
+from maasserver.testing.factory import factory
+from maasserver.testing.orm import reload_object
+from maasserver.testing.osystems import patch_usable_osystems
+from maasserver.testing.testcase import MAASServerTestCase
+from maasserver.views import settings as settings_view
+from maasserver.views.settings_license_keys import LICENSE_KEY_ANCHOR
+from testtools.matchers import ContainsAll
+
+
+def make_osystem_requiring_license_key(osystem=None, distro_series=None):
+ if osystem is None:
+ osystem = factory.make_name('osystem')
+ if distro_series is None:
+ distro_series = factory.make_name('distro_series')
+ rpc_release = make_rpc_release(
+ distro_series, requires_license_key=True)
+ rpc_osystem = make_rpc_osystem(osystem, releases=[rpc_release])
+ return rpc_osystem
+
+
+class LicenseKeyListingTest(MAASServerTestCase):
+
+ def make_license_key_with_os(self, osystem=None, distro_series=None,
+ license_key=None):
+ license_key = factory.make_LicenseKey(
+ osystem=osystem, distro_series=distro_series,
+ license_key=license_key)
+ osystem = make_osystem_requiring_license_key(
+ license_key.osystem, license_key.distro_series)
+ return license_key, osystem
+
+ def make_license_keys(self, count):
+ keys = []
+ osystems = []
+ for _ in range(count):
+ key, osystem = self.make_license_key_with_os()
+ keys.append(key)
+ osystems.append(osystem)
+ patch_usable_osystems(self, osystems=osystems)
+ self.patch(
+ settings_view,
+ 'gen_all_known_operating_systems').return_value = osystems
+ return keys, osystems
+
+ def test_settings_contains_osystem_and_distro_series(self):
+ self.client_log_in(as_admin=True)
+ keys, _ = self.make_license_keys(3)
+ response = self.client.get(reverse('settings'))
+ os_titles = [key.osystem for key in keys]
+ series_titles = [key.distro_series for key in keys]
+ self.assertThat(
+ response.content, ContainsAll(os_titles + series_titles))
+
+ def test_settings_link_to_add_license_key(self):
+ self.client_log_in(as_admin=True)
+ self.make_license_keys(3)
+ links = get_content_links(self.client.get(reverse('settings')))
+ script_add_link = reverse('license-key-add')
+ self.assertIn(script_add_link, links)
+
+ def test_settings_contains_links_to_delete(self):
+ self.client_log_in(as_admin=True)
+ keys, _ = self.make_license_keys(3)
+ links = get_content_links(self.client.get(reverse('settings')))
+ license_key_delete_links = [
+ reverse(
+ 'license-key-delete', args=[key.osystem, key.distro_series])
+ for key in keys]
+ self.assertThat(links, ContainsAll(license_key_delete_links))
+
+ def test_settings_contains_links_to_edit(self):
+ self.client_log_in(as_admin=True)
+ keys, _ = self.make_license_keys(3)
+ links = get_content_links(self.client.get(reverse('settings')))
+ license_key_delete_links = [
+ reverse(
+ 'license-key-edit', args=[key.osystem, key.distro_series])
+ for key in keys]
+ self.assertThat(links, ContainsAll(license_key_delete_links))
+
+ def test_settings_contains_commissioning_scripts_slot_anchor(self):
+ self.client_log_in(as_admin=True)
+ self.make_license_keys(3)
+ response = self.client.get(reverse('settings'))
+ document = fromstring(response.content)
+ slots = document.xpath(
+ "//div[@id='%s']" % LICENSE_KEY_ANCHOR)
+ self.assertEqual(
+ 1, len(slots),
+ "Missing anchor '%s'" % LICENSE_KEY_ANCHOR)
+
+
+class LicenseKeyAddTest(MAASServerTestCase):
+
+ def test_can_create_license_key(self):
+ self.client_log_in(as_admin=True)
+ osystem = make_osystem_requiring_license_key()
+ patch_usable_osystems(self, osystems=[osystem])
+ self.patch(forms, 'validate_license_key').return_value = True
+ series = osystem['default_release']
+ key = factory.make_name('key')
+ add_link = reverse('license-key-add')
+ definition = {
+ 'osystem': osystem['name'],
+ 'distro_series': series,
+ 'license_key': key,
+ }
+ response = self.client.post(add_link, definition)
+ self.assertEqual(
+ (httplib.FOUND, reverse('settings')),
+ (response.status_code, extract_redirect(response)))
+ new_license_key = LicenseKey.objects.get(
+ osystem=osystem['name'], distro_series=series)
+ self.assertAttributes(new_license_key, definition)
+
+
+class LicenseKeyEditTest(MAASServerTestCase):
+
+ def test_can_update_license_key(self):
+ self.client_log_in(as_admin=True)
+ key = factory.make_LicenseKey()
+ osystem = make_osystem_requiring_license_key(
+ key.osystem, key.distro_series)
+ patch_usable_osystems(self, osystems=[osystem])
+ self.patch(forms, 'validate_license_key').return_value = True
+ new_key = factory.make_name('key')
+ edit_link = reverse(
+ 'license-key-edit', args=[key.osystem, key.distro_series])
+ definition = {
+ 'osystem': key.osystem,
+ 'distro_series': key.distro_series,
+ 'license_key': new_key,
+ }
+ response = self.client.post(edit_link, definition)
+ self.assertEqual(
+ (httplib.FOUND, reverse('settings')),
+ (response.status_code, extract_redirect(response)))
+ self.assertAttributes(reload_object(key), definition)
+
+
+class LicenseKeyDeleteTest(MAASServerTestCase):
+
+ def test_can_delete_license_key(self):
+ self.client_log_in(as_admin=True)
+ key = factory.make_LicenseKey()
+ delete_link = reverse(
+ 'license-key-delete', args=[key.osystem, key.distro_series])
+ response = self.client.post(delete_link, {'post': 'yes'})
+ self.assertEqual(
+ (httplib.FOUND, reverse('settings')),
+ (response.status_code, extract_redirect(response)))
+ self.assertFalse(
+ LicenseKey.objects.filter(
+ osystem=key.osystem, distro_series=key.distro_series).exists())