launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #11774
[Merge] lp:~jtv/maas/bug-1025582-model into lp:maas
Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/bug-1025582-model into lp:maas.
Requested reviews:
MAAS Maintainers (maas-maintainers)
Related bugs:
Bug #1025582 in MAAS: "No warning shown that maas-import-pxe-files has not been run"
https://bugs.launchpad.net/maas/+bug/1025582
For more details, see:
https://code.launchpad.net/~jtv/maas/bug-1025582-model/+merge/123679
The master worker (which runs on the same machine as the TFTP server) will report available images regularly through the MAAS API. The download script can't do this directly because it has no API credentials.
Nobody was available to pre-imp this part of the work with, so read critically.
Jeroen
--
https://code.launchpad.net/~jtv/maas/bug-1025582-model/+merge/123679
Your team MAAS Maintainers is requested to review the proposed merge of lp:~jtv/maas/bug-1025582-model into lp:maas.
=== added file 'src/maasserver/migrations/0021_add_bootimage_model.py'
--- src/maasserver/migrations/0021_add_bootimage_model.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0021_add_bootimage_model.py 2012-09-11 05:16:18 +0000
@@ -0,0 +1,182 @@
+# encoding: utf-8
+import datetime
+
+from django.db import models
+from south.db import db
+from south.v2 import SchemaMigration
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'BootImage'
+ db.create_table(u'maasserver_bootimage', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('architecture', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('subarchitecture', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('release', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('purpose', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ))
+ db.send_create_signal(u'maasserver', ['BootImage'])
+
+ # Adding unique constraint on 'BootImage', fields ['architecture', 'subarchitecture', 'release', 'purpose']
+ db.create_unique(u'maasserver_bootimage', ['architecture', 'subarchitecture', 'release', 'purpose'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'BootImage', fields ['architecture', 'subarchitecture', 'release', 'purpose']
+ db.delete_unique(u'maasserver_bootimage', ['architecture', 'subarchitecture', 'release', 'purpose'])
+
+ # Deleting model 'BootImage'
+ db.delete_table(u'maasserver_bootimage')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ u'maasserver.bootimage': {
+ 'Meta': {'unique_together': "((u'architecture', u'subarchitecture', u'release', u'purpose'),)", 'object_name': 'BootImage'},
+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'purpose': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'release': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'subarchitecture': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ },
+ u'maasserver.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+ },
+ u'maasserver.dhcplease': {
+ 'Meta': {'object_name': 'DHCPLease'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'mac': ('maasserver.fields.MACAddressField', [], {}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"})
+ },
+ u'maasserver.filestorage': {
+ 'Meta': {'object_name': 'FileStorage'},
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+ 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ u'maasserver.macaddress': {
+ 'Meta': {'object_name': 'MACAddress'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+ 'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.Node']"}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.node': {
+ 'Meta': {'object_name': 'Node'},
+ 'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386'", 'max_length': '10'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'netboot': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']", 'null': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'power_parameters': ('maasserver.fields.JSONObjectField', [], {'default': "u''", 'blank': 'True'}),
+ 'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+ 'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-ee3df1b4-fbcb-11e1-8b8f-002608dc6120'", 'unique': 'True', 'max_length': '41'}),
+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'null': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ u'maasserver.nodegroup': {
+ 'Meta': {'object_name': 'NodeGroup'},
+ 'api_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '18'}),
+ 'api_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Token']", 'unique': 'True'}),
+ 'broadcast_ip': ('django.db.models.fields.IPAddressField', [], {'default': "u''", 'max_length': '15', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_interfaces': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_range_high': ('django.db.models.fields.IPAddressField', [], {'default': "u''", 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.IPAddressField', [], {'default': "u''", 'max_length': '15', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'router_ip': ('django.db.models.fields.IPAddressField', [], {'default': "u''", 'max_length': '15', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.IPAddressField', [], {'default': "u''", 'max_length': '15', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'worker_ip': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'})
+ },
+ u'maasserver.sshkey': {
+ 'Meta': {'unique_together': "((u'user', u'key'),)", 'object_name': 'SSHKey'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.TextField', [], {}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ u'maasserver.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'piston.consumer': {
+ 'Meta': {'object_name': 'Consumer'},
+ 'description': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '16'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'consumers'", 'null': 'True', 'to': "orm['auth.User']"})
+ },
+ 'piston.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'callback': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'callback_confirmed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['piston.Consumer']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_approved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '18'}),
+ 'secret': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'timestamp': ('django.db.models.fields.IntegerField', [], {'default': '1347338908L'}),
+ 'token_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'tokens'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'verifier': ('django.db.models.fields.CharField', [], {'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['maasserver']
=== modified file 'src/maasserver/models/__init__.py'
--- src/maasserver/models/__init__.py 2012-08-16 13:38:51 +0000
+++ src/maasserver/models/__init__.py 2012-09-11 05:16:18 +0000
@@ -11,6 +11,7 @@
__metaclass__ = type
__all__ = [
+ 'BootImage',
'Config',
'DHCPLease',
'FileStorage',
@@ -29,6 +30,7 @@
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from maasserver.enum import NODE_PERMISSION
+from maasserver.models.bootimage import BootImage
from maasserver.models.config import Config
from maasserver.models.dhcplease import DHCPLease
from maasserver.models.filestorage import FileStorage
@@ -61,8 +63,9 @@
# Register the models in the admin site.
+admin.site.register(BootImage)
+admin.site.register(Config)
admin.site.register(Consumer)
-admin.site.register(Config)
admin.site.register(FileStorage)
admin.site.register(MACAddress)
admin.site.register(Node)
=== added file 'src/maasserver/models/bootimage.py'
--- src/maasserver/models/bootimage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/models/bootimage.py 2012-09-11 05:16:18 +0000
@@ -0,0 +1,78 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Registration of available boot images."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'BootImage',
+ ]
+
+
+from django.db.models import (
+ CharField,
+ Manager,
+ Model,
+ )
+from maasserver import DefaultMeta
+
+
+class BootImageManager(Manager):
+ """Manager for model class.
+
+ Don't import or instantiate this directly; access as `BootImage.objects`.
+ """
+
+ def get_by_natural_key(self, architecture, subarchitecture, release,
+ purpose):
+ """Look up a specific image."""
+ return self.get(
+ architecture=architecture, subarchitecture=subarchitecture,
+ release=release, purpose=purpose)
+
+ def register_image(self, architecture, subarchitecture, release, purpose):
+ """Register an image if it wasn't already registered."""
+ self.get_or_create(
+ architecture=architecture, subarchitecture=subarchitecture,
+ release=release, purpose=purpose)
+
+ def have_image(self, architecture, subarchitecture, release, purpose):
+ """Is an image for the given kind of boot available?"""
+ try:
+ self.get_by_natural_key(
+ architecture=architecture, subarchitecture=subarchitecture,
+ release=release, purpose=purpose)
+ return True
+ except BootImage.DoesNotExist:
+ return False
+
+
+class BootImage(Model):
+ """Available boot image (i.e. kerne and initrd).
+
+ Each `BootImage` represents a type of boot for which a boot image is
+ available. The `maas-import-pxe-files` script imports these, and the
+ TFTP server provides them to booting nodes.
+
+ If a boot image is missing, that may mean that the import script has not
+ been run yet, or has failed; or that it was not configured to provide
+ that particular image.
+ """
+
+ class Meta(DefaultMeta):
+ unique_together = (
+ ('architecture', 'subarchitecture', 'release', 'purpose'),
+ )
+
+ objects = BootImageManager()
+
+ architecture = CharField(max_length=255, blank=False)
+ subarchitecture = CharField(max_length=255, blank=False)
+ release = CharField(max_length=255, blank=False)
+ purpose = CharField(max_length=255, blank=False)
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-09-05 13:30:21 +0000
+++ src/maasserver/testing/factory.py 2012-09-11 05:16:18 +0000
@@ -24,6 +24,7 @@
NODE_STATUS,
)
from maasserver.models import (
+ BootImage,
DHCPLease,
FileStorage,
MACAddress,
@@ -268,6 +269,22 @@
return "OAuth " + ", ".join([
'%s="%s"' % (key, value) for key, value in items.items()])
+ def make_boot_image(self, architecture=None, subarchitecture=None,
+ release=None, purpose=None):
+ if architecture is None:
+ architecture = self.make_name('architecture')
+ if subarchitecture is None:
+ subarchitecture = self.make_name('subarchitecture')
+ if release is None:
+ release = self.make_name('release')
+ if purpose is None:
+ purpose = self.make_name('purpose')
+ return BootImage.objects.create(
+ architecture=architecture,
+ subarchitecture=subarchitecture,
+ release=release,
+ purpose=purpose)
+
# Create factory singleton.
factory = Factory()
=== added file 'src/maasserver/tests/test_bootimage.py'
--- src/maasserver/tests/test_bootimage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_bootimage.py 2012-09-11 05:16:18 +0000
@@ -0,0 +1,47 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for :class:`BootImage`."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+from maasserver.models import BootImage
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
+
+
+class TestBootImageManager(TestCase):
+
+ def make_image_params(self):
+ return dict(
+ architecture=factory.make_name('architecture'),
+ subarchitecture=factory.make_name('subarchitecture'),
+ release=factory.make_name('release'),
+ purpose=factory.make_name('purpose'))
+
+ def test_have_image_returns_False_if_image_not_available(self):
+ self.assertFalse(
+ BootImage.objects.have_image(**self.make_image_params()))
+
+ def test_have_image_returns_True_if_image_available(self):
+ params = self.make_image_params()
+ factory.make_boot_image(**params)
+ self.assertTrue(BootImage.objects.have_image(**params))
+
+ def test_register_image_registers_new_image(self):
+ params = self.make_image_params()
+ BootImage.objects.register_image(**params)
+ self.assertTrue(BootImage.objects.have_image(**params))
+
+ def test_register_image_leaves_existing_image_intact(self):
+ params = self.make_image_params()
+ factory.make_boot_image(**params)
+ BootImage.objects.register_image(**params)
+ self.assertTrue(BootImage.objects.have_image(**params))