← Back to team overview

launchpad-reviewers team mailing list archive

[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))