Julian Edwards has proposed merging lp:~julian-edwards/maas/pxe-control into
lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~julian-edwards/maas/pxe-control/+merge/110717
Add Node.netboot boolean and an API in the metadata server to control it.
The new flag is not used anywhere yet but when MAAS completely controls DHCP
and TFTP then the preseed scripts will need to change to use this new API.
We'll also add a UI to control the flag and ensure that the TFTP files for the
node are added/removed as required when it's changed.
--
https://code.launchpad.net/~julian-edwards/maas/pxe-control/+merge/110717
Your team Launchpad code reviewers is requested to review the proposed merge of
lp:~julian-edwards/maas/pxe-control into lp:maas.
=== added file 'src/maasserver/migrations/0010_add_node_netboot.py'
--- src/maasserver/migrations/0010_add_node_netboot.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0010_add_node_netboot.py 2012-06-18 05:14:19 +0000
@@ -0,0 +1,153 @@
+# flake8: noqa
+# SKIP this file when reformatting.
+# The rest of this file was generated by South.
+
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Node.netboot'
+ db.add_column(u'maasserver_node', 'netboot', self.gf('django.db.models.fields.BooleanField')(default=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Node.netboot'
+ db.delete_column(u'maasserver_node', 'netboot')
+
+
+ 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.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.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'}),
+ '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-20250ca0-b8f4-11e1-afce-002215205ce8'", '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', [], {'max_length': '15'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_range_high': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'ip_range_low': ('django.db.models.fields.IPAddressField', [], {'unique': 'True', 'max_length': '15'}),
+ 'name': ('django.db.models.fields.CharField', [], {'default': "u''", 'unique': 'True', 'max_length': '80'}),
+ 'router_ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'subnet_mask': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ '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': '1339989444L'}),
+ '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/node.py'
--- src/maasserver/models/node.py 2012-06-15 05:07:40 +0000
+++ src/maasserver/models/node.py 2012-06-18 05:14:19 +0000
@@ -30,6 +30,7 @@
ValidationError,
)
from django.db.models import (
+ BooleanField,
CharField,
ForeignKey,
IntegerField,
@@ -374,6 +375,8 @@
error = CharField(max_length=255, blank=True, default='')
+ netboot = BooleanField(default=True)
+
objects = NodeManager()
def __unicode__(self):
=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py 2012-06-15 08:41:34 +0000
+++ src/maasserver/tests/test_models.py 2012-06-18 05:14:19 +0000
@@ -502,6 +502,10 @@
"Invalid transition: Retired -> Allocated.",
node.save)
+ def test_netboot_defaults_to_True(self):
+ node = factory.make_node()
+ self.assertTrue(node.netboot)
+
class NodeTransitionsTests(TestCase):
"""Test the structure of NODE_TRANSITIONS."""
=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py 2012-06-05 10:49:29 +0000
+++ src/metadataserver/api.py 2012-06-18 05:14:19 +0000
@@ -226,6 +226,26 @@
return rc.ALL_OK
+ @api_exported('POST')
+ def netboot_off(self, request, version=None, mac=None):
+ """Turn off netboot on the node.
+
+ A commissioning node can call this to turn off netbooting when
+ it finishes installing itself.
+ """
+ node = get_queried_node(request, for_mac=mac)
+ node.netboot = False
+ node.save()
+ return rc.ALL_OK
+
+ @api_exported('POST')
+ def netboot_on(self, request, version=None, mac=None):
+ """Turn on netboot on the node."""
+ node = get_queried_node(request, for_mac=mac)
+ node.netboot = True
+ node.save()
+ return rc.ALL_OK
+
class MetaDataHandler(VersionIndexHandler):
"""Meta-data listing for a given version."""
=== modified file 'src/metadataserver/tests/test_api.py'
--- src/metadataserver/tests/test_api.py 2012-06-06 03:11:48 +0000
+++ src/metadataserver/tests/test_api.py 2012-06-18 05:14:19 +0000
@@ -526,3 +526,18 @@
response = self.get(
'/latest/by-mac/%s/meta-data/instance-id/' % mac.mac_address)
self.assertEqual(httplib.FORBIDDEN, response.status_code)
+
+ def test_netboot_off(self):
+ node = factory.make_node(netboot=True)
+ client = self.make_node_client(node=node)
+ response = client.post(
+ self.make_url('/latest/'), {'op': 'netboot_off'})
+ node = reload_object(node)
+ self.assertFalse(node.netboot, response)
+
+ def test_netboot_on(self):
+ node = factory.make_node(netboot=False)
+ client = self.make_node_client(node=node)
+ response = client.post(self.make_url('/latest/'), {'op': 'netboot_on'})
+ node = reload_object(node)
+ self.assertTrue(node.netboot, response)
_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to : [email protected]
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help : https://help.launchpad.net/ListHelp