launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13879
[Merge] lp:~julian-edwards/maas/1.2-backport-bug-1068843 into lp:maas/1.2
Julian Edwards has proposed merging lp:~julian-edwards/maas/1.2-backport-bug-1068843 into lp:maas/1.2.
Commit message:
Backport changes from trunk that fix bug 1068843 (cluster controller has no provisioning images).
Requested reviews:
MAAS Maintainers (maas-maintainers)
Related bugs:
Bug #1068843 in MAAS: "maas-cluster-controller doesn't have images for provisioning"
https://bugs.launchpad.net/maas/+bug/1068843
For more details, see:
https://code.launchpad.net/~julian-edwards/maas/1.2-backport-bug-1068843/+merge/132043
--
https://code.launchpad.net/~julian-edwards/maas/1.2-backport-bug-1068843/+merge/132043
Your team MAAS Maintainers is requested to review the proposed merge of lp:~julian-edwards/maas/1.2-backport-bug-1068843 into lp:maas/1.2.
=== modified file 'contrib/python-tx-tftp/tftp/protocol.py'
--- contrib/python-tx-tftp/tftp/protocol.py 2012-07-05 14:36:27 +0000
+++ contrib/python-tx-tftp/tftp/protocol.py 2012-10-30 10:29:27 +0000
@@ -12,6 +12,7 @@
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.protocol import DatagramProtocol
from twisted.python import log
+from twisted.python.context import call
class TFTP(DatagramProtocol):
@@ -48,11 +49,22 @@
@inlineCallbacks
def _startSession(self, datagram, addr, mode):
+ # Set up a call context so that we can pass extra arbitrary
+ # information to interested backends without adding extra call
+ # arguments, or switching to using a request object, for example.
+ context = {}
+ if self.transport is not None:
+ # Add the local and remote addresses to the call context.
+ local = self.transport.getHost()
+ context["local"] = local.host, local.port
+ context["remote"] = addr
try:
if datagram.opcode == OP_WRQ:
- fs_interface = yield self.backend.get_writer(datagram.filename)
+ fs_interface = yield call(
+ context, self.backend.get_writer, datagram.filename)
elif datagram.opcode == OP_RRQ:
- fs_interface = yield self.backend.get_reader(datagram.filename)
+ fs_interface = yield call(
+ context, self.backend.get_reader, datagram.filename)
except Unsupported, e:
self.transport.write(ERRORDatagram.from_code(ERR_ILLEGAL_OP,
str(e)).to_wire(), addr)
=== modified file 'contrib/python-tx-tftp/tftp/test/test_protocol.py'
--- contrib/python-tx-tftp/tftp/test/test_protocol.py 2012-07-25 19:59:37 +0000
+++ contrib/python-tx-tftp/tftp/test/test_protocol.py 2012-10-30 10:29:27 +0000
@@ -11,9 +11,11 @@
from tftp.netascii import NetasciiReceiverProxy, NetasciiSenderProxy
from tftp.protocol import TFTP
from twisted.internet import reactor
-from twisted.internet.defer import Deferred
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import Deferred, inlineCallbacks
from twisted.internet.protocol import DatagramProtocol
from twisted.internet.task import Clock
+from twisted.python import context
from twisted.python.filepath import FilePath
from twisted.test.proto_helpers import StringTransport
from twisted.trial import unittest
@@ -50,7 +52,8 @@
def setUp(self):
self.clock = Clock()
- self.transport = FakeTransport(hostAddress=('127.0.0.1', self.port))
+ self.transport = FakeTransport(
+ hostAddress=IPv4Address('UDP', '127.0.0.1', self.port))
def test_malformed_datagram(self):
tftp = TFTP(BackendFactory(), _clock=self.clock)
@@ -247,3 +250,71 @@
self.clock.advance(1)
self.assertTrue(d.called)
self.assertTrue(IWriter.providedBy(d.result.backend))
+
+
+class CapturedContext(Exception):
+ """A donkey, to carry the call context back up the stack."""
+
+ def __init__(self, args, names):
+ super(CapturedContext, self).__init__(*args)
+ self.context = {name: context.get(name) for name in names}
+
+
+class ContextCapturingBackend(object):
+ """A fake `IBackend` that raises `CapturedContext`.
+
+ Calling `get_reader` or `get_writer` raises a `CapturedContext` exception,
+ which captures the values of the call context for the given `names`.
+ """
+
+ def __init__(self, *names):
+ self.names = names
+
+ def get_reader(self, file_name):
+ raise CapturedContext(("get_reader", file_name), self.names)
+
+ def get_writer(self, file_name):
+ raise CapturedContext(("get_writer", file_name), self.names)
+
+
+class HostTransport(object):
+ """A fake `ITransport` that only responds to `getHost`."""
+
+ def __init__(self, host):
+ self.host = host
+
+ def getHost(self):
+ return IPv4Address("UDP", *self.host)
+
+
+class BackendCallingContext(unittest.TestCase):
+
+ def setUp(self):
+ super(BackendCallingContext, self).setUp()
+ self.backend = ContextCapturingBackend("local", "remote")
+ self.tftp = TFTP(self.backend)
+ self.tftp.transport = HostTransport(("12.34.56.78", 1234))
+
+ @inlineCallbacks
+ def test_context_rrq(self):
+ rrq_datagram = RRQDatagram('nonempty', 'NetASCiI', {})
+ rrq_addr = ('127.0.0.1', 1069)
+ error = yield self.assertFailure(
+ self.tftp._startSession(rrq_datagram, rrq_addr, "octet"),
+ CapturedContext)
+ self.assertEqual(("get_reader", rrq_datagram.filename), error.args)
+ self.assertEqual(
+ {"local": self.tftp.transport.host, "remote": rrq_addr},
+ error.context)
+
+ @inlineCallbacks
+ def test_context_wrq(self):
+ wrq_datagram = WRQDatagram('nonempty', 'NetASCiI', {})
+ wrq_addr = ('127.0.0.1', 1069)
+ error = yield self.assertFailure(
+ self.tftp._startSession(wrq_datagram, wrq_addr, "octet"),
+ CapturedContext)
+ self.assertEqual(("get_writer", wrq_datagram.filename), error.args)
+ self.assertEqual(
+ {"local": self.tftp.transport.host, "remote": wrq_addr},
+ error.context)
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-10-29 11:04:39 +0000
+++ src/maasserver/api.py 2012-10-30 10:29:27 +0000
@@ -162,7 +162,10 @@
compose_preseed_url,
)
from maasserver.server_address import get_maas_facing_server_address
-from maasserver.utils import map_enum
+from maasserver.utils import (
+ absolute_reverse,
+ map_enum,
+ )
from maasserver.utils.orm import get_one
from piston.handler import (
AnonymousBaseHandler,
@@ -1695,6 +1698,8 @@
:param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
'armhf').
:param subarch: Subarchitecture name (in the pxelinux namespace).
+ :param local: The IP address of the cluster controller.
+ :param remote: The IP address of the booting node.
"""
node = get_node_from_mac_string(request.GET.get('mac', None))
@@ -1749,11 +1754,12 @@
purpose = get_boot_purpose(node)
server_address = get_maas_facing_server_address()
+ cluster_address = get_mandatory_param(request.GET, "local")
params = KernelParameters(
arch=arch, subarch=subarch, release=series, purpose=purpose,
hostname=hostname, domain=domain, preseed_url=preseed_url,
- log_host=server_address, fs_host=server_address)
+ log_host=server_address, fs_host=cluster_address)
return HttpResponse(
json.dumps(params._asdict()),
@@ -1777,24 +1783,34 @@
`purpose`, all as in the code that determines TFTP paths for
these images.
"""
- check_nodegroup_access(request, NodeGroup.objects.ensure_master())
+ nodegroup_uuid = get_mandatory_param(request.data, "nodegroup")
+ nodegroup = get_object_or_404(NodeGroup, uuid=nodegroup_uuid)
+ check_nodegroup_access(request, nodegroup)
images = json.loads(get_mandatory_param(request.data, 'images'))
for image in images:
BootImage.objects.register_image(
+ nodegroup=nodegroup,
architecture=image['architecture'],
subarchitecture=image.get('subarchitecture', 'generic'),
release=image['release'],
purpose=image['purpose'])
- if len(images) == 0:
+ # Work out if any nodegroups are missing images.
+ nodegroup_ids_with_images = BootImage.objects.values_list(
+ "nodegroup_id", flat=True)
+ nodegroups_missing_images = NodeGroup.objects.exclude(
+ id__in=nodegroup_ids_with_images).filter(
+ status=NODEGROUP_STATUS.ACCEPTED)
+ if nodegroups_missing_images.exists():
warning = dedent("""\
- No boot images have been imported yet. Either the
+ Some cluster controllers are missing boot images. Either the
maas-import-pxe-files script has not run yet, or it failed.
- Try running it manually. If it succeeds, this message will
- go away within 5 minutes.
- """)
+ Try running it manually on the affected
+ <a href="%s#accepted-clusters">cluster controllers.</a>
+ If it succeeds, this message will go away within 5 minutes.
+ """ % absolute_reverse("settings"))
register_persistent_error(COMPONENT.IMPORT_PXE_FILES, warning)
else:
discard_persistent_error(COMPONENT.IMPORT_PXE_FILES)
=== modified file 'src/maasserver/components.py'
--- src/maasserver/components.py 2012-09-28 18:42:16 +0000
+++ src/maasserver/components.py 2012-10-30 10:29:27 +0000
@@ -17,6 +17,7 @@
"register_persistent_error",
]
+from django.utils.safestring import mark_safe
from maasserver.models import ComponentError
from maasserver.utils.orm import get_one
@@ -50,4 +51,5 @@
def get_persistent_errors():
"""Return list of current persistent error messages."""
- return sorted(err.error for err in ComponentError.objects.all())
+ return sorted(
+ mark_safe(err.error) for err in ComponentError.objects.all())
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-10-29 11:04:39 +0000
+++ src/maasserver/forms.py 2012-10-30 10:29:27 +0000
@@ -583,6 +583,12 @@
label="Default domain for new nodes", required=False, help_text=(
"If 'local' is chosen, nodes must be using mDNS. Leave empty to "
"use hostnames without a domain for newly enlisted nodes."))
+ http_proxy = forms.URLField(
+ label="Proxy for HTTP and HTTPS traffic", required=False,
+ help_text=(
+ "This is used by the cluster and region controllers for "
+ "downloading PXE boot images and other provisioning-related "
+ "resources. It is not passed into provisioned nodes."))
class CommissioningForm(ConfigForm):
=== added file 'src/maasserver/migrations/0039_add_nodegroup_to_bootimage.py'
--- src/maasserver/migrations/0039_add_nodegroup_to_bootimage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0039_add_nodegroup_to_bootimage.py 2012-10-30 10:29:27 +0000
@@ -0,0 +1,217 @@
+# -*- coding: 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):
+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'release', 'architecture', 'purpose']
+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'release', 'architecture', 'purpose'])
+
+ # Adding field 'BootImage.nodegroup'
+ db.add_column(u'maasserver_bootimage', 'nodegroup',
+ self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['maasserver.NodeGroup']),
+ keep_default=False)
+ # Set existing bootimage rows to have a nodegroup of the master
+ # nodegroup (which is the first row in that table).
+ db.execute("UPDATE maasserver_bootimage SET nodegroup_id = (SELECT id from maasserver_nodegroup ORDER BY id LIMIT 1)")
+ db.execute("ALTER TABLE maasserver_bootimage ALTER nodegroup_id SET NOT NULL")
+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'release', 'nodegroup', 'architecture', 'purpose']
+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'release', 'nodegroup_id', 'architecture', 'purpose'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'BootImage', fields ['subarchitecture', 'release', 'nodegroup', 'architecture', 'purpose']
+ db.delete_unique(u'maasserver_bootimage', ['subarchitecture', 'release', 'nodegroup_id', 'architecture', 'purpose'])
+
+ # Deleting field 'BootImage.nodegroup'
+ db.delete_column(u'maasserver_bootimage', 'nodegroup_id')
+
+ # Adding unique constraint on 'BootImage', fields ['subarchitecture', 'release', 'architecture', 'purpose']
+ db.create_unique(u'maasserver_bootimage', ['subarchitecture', 'release', 'architecture', 'purpose'])
+
+
+ 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'nodegroup', 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'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ '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.componenterror': {
+ 'Meta': {'object_name': 'ComponentError'},
+ 'component': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'error': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 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/generic'", 'max_length': '31'}),
+ 'cpu_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'distro_series': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '10', 'null': 'True', 'blank': 'True'}),
+ 'error': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'hardware_details': ('maasserver.fields.XMLField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+ 'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'memory': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ '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-f9e8804e-1d11-11e2-be72-0026c71eea0e'", 'unique': 'True', 'max_length': '41'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['maasserver.Tag']", 'symmetrical': 'False'}),
+ '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'}),
+ 'cluster_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'dhcp_key': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'})
+ },
+ u'maasserver.nodegroupinterface': {
+ 'Meta': {'unique_together': "((u'nodegroup', u'interface'),)", 'object_name': 'NodeGroupInterface'},
+ 'broadcast_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'interface': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+ 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+ 'ip_range_high': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'ip_range_low': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'management': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'nodegroup': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['maasserver.NodeGroup']"}),
+ 'router_ip': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'subnet_mask': ('django.db.models.fields.GenericIPAddressField', [], {'default': 'None', 'max_length': '39', 'null': 'True', 'blank': 'True'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 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.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {}),
+ 'definition': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'updated': ('django.db.models.fields.DateTimeField', [], {})
+ },
+ 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': '1350997269L'}),
+ '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/bootimage.py'
--- src/maasserver/models/bootimage.py 2012-09-13 06:53:55 +0000
+++ src/maasserver/models/bootimage.py 2012-10-30 10:29:27 +0000
@@ -17,10 +17,12 @@
from django.db.models import (
CharField,
+ ForeignKey,
Manager,
Model,
)
from maasserver import DefaultMeta
+from maasserver.models.nodegroup import NodeGroup
class BootImageManager(Manager):
@@ -29,25 +31,30 @@
Don't import or instantiate this directly; access as `BootImage.objects`.
"""
- def get_by_natural_key(self, architecture, subarchitecture, release,
- purpose):
+ def get_by_natural_key(self, nodegroup, architecture, subarchitecture,
+ release, purpose):
"""Look up a specific image."""
return self.get(
- architecture=architecture, subarchitecture=subarchitecture,
- release=release, purpose=purpose)
+ nodegroup=nodegroup, architecture=architecture,
+ subarchitecture=subarchitecture, release=release,
+ purpose=purpose)
- def register_image(self, architecture, subarchitecture, release, purpose):
+ def register_image(self, nodegroup, 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)
+ nodegroup=nodegroup, architecture=architecture,
+ subarchitecture=subarchitecture, release=release,
+ purpose=purpose)
- def have_image(self, architecture, subarchitecture, release, purpose):
+ def have_image(self, nodegroup, 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)
+ nodegroup=nodegroup, architecture=architecture,
+ subarchitecture=subarchitecture, release=release,
+ purpose=purpose)
return True
except BootImage.DoesNotExist:
return False
@@ -69,11 +76,15 @@
class Meta(DefaultMeta):
unique_together = (
- ('architecture', 'subarchitecture', 'release', 'purpose'),
+ ('nodegroup', 'architecture', 'subarchitecture', 'release',
+ 'purpose'),
)
objects = BootImageManager()
+ # Nodegroup (cluster controller) that has the images.
+ nodegroup = ForeignKey(NodeGroup, null=False, editable=False, unique=False)
+
# System architecture (e.g. "i386") that the image is for.
architecture = CharField(max_length=255, blank=False, editable=False)
=== modified file 'src/maasserver/models/config.py'
--- src/maasserver/models/config.py 2012-09-27 13:50:34 +0000
+++ src/maasserver/models/config.py 2012-10-30 10:29:27 +0000
@@ -53,6 +53,7 @@
'enlistment_domain': b'local',
'default_distro_series': DISTRO_SERIES.precise,
'commissioning_distro_series': DISTRO_SERIES.precise,
+ 'http_proxy': None,
## /settings
}
=== modified file 'src/maasserver/preseed.py'
--- src/maasserver/preseed.py 2012-10-02 21:07:00 +0000
+++ src/maasserver/preseed.py 2012-10-30 10:29:27 +0000
@@ -242,7 +242,7 @@
"""Whether or not the SquashFS image can be used during installation."""
arch, subarch = node.architecture.split("/")
return BootImage.objects.have_image(
- arch, subarch, node.get_distro_series(), "filesystem")
+ node.nodegroup, arch, subarch, node.get_distro_series(), "filesystem")
def render_preseed(node, prefix, release=''):
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2012-10-25 13:40:55 +0000
+++ src/maasserver/testing/factory.py 2012-10-30 10:29:27 +0000
@@ -325,7 +325,7 @@
'%s="%s"' % (key, value) for key, value in items.items()])
def make_boot_image(self, architecture=None, subarchitecture=None,
- release=None, purpose=None):
+ release=None, purpose=None, nodegroup=None):
if architecture is None:
architecture = self.make_name('architecture')
if subarchitecture is None:
@@ -334,7 +334,10 @@
release = self.make_name('release')
if purpose is None:
purpose = self.make_name('purpose')
+ if nodegroup is None:
+ nodegroup = self.make_node_group()
return BootImage.objects.create(
+ nodegroup=nodegroup,
architecture=architecture,
subarchitecture=subarchitecture,
release=release,
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-10-29 11:04:39 +0000
+++ src/maasserver/tests/test_api.py 2012-10-30 10:29:27 +0000
@@ -3043,11 +3043,16 @@
class TestPXEConfigAPI(AnonAPITestCase):
+ def get_default_params(self):
+ return {
+ "local": factory.getRandomIPAddress(),
+ "remote": factory.getRandomIPAddress(),
+ }
+
def get_mac_params(self):
- return {'mac': factory.make_mac_address().mac_address}
-
- def get_default_params(self):
- return dict()
+ params = self.get_default_params()
+ params['mac'] = factory.make_mac_address().mac_address
+ return params
def get_pxeconfig(self, params=None):
"""Make a request to `pxeconfig`, and return its response dict."""
@@ -3081,7 +3086,8 @@
ContainsAll(KernelParameters._fields))
def test_pxeconfig_returns_data_for_known_node(self):
- response = self.client.get(reverse('pxeconfig'), self.get_mac_params())
+ params = self.get_mac_params()
+ response = self.client.get(reverse('pxeconfig'), params)
self.assertEqual(httplib.OK, response.status_code)
def test_pxeconfig_returns_no_content_for_unknown_node(self):
@@ -3093,6 +3099,7 @@
architecture = factory.getRandomEnum(ARCHITECTURE)
arch, subarch = architecture.split('/')
params = dict(
+ self.get_default_params(),
mac=factory.getRandomMACAddress(delimiter=b'-'),
arch=arch,
subarch=subarch)
@@ -3121,15 +3128,19 @@
full_hostname = '.'.join([host, domain])
node = factory.make_node(hostname=full_hostname)
mac = factory.make_mac_address(node=node)
- pxe_config = self.get_pxeconfig(params={'mac': mac.mac_address})
+ params = self.get_default_params()
+ params['mac'] = mac.mac_address
+ pxe_config = self.get_pxeconfig(params)
self.assertEqual(host, pxe_config.get('hostname'))
self.assertNotIn(domain, pxe_config.values())
def test_pxeconfig_uses_nodegroup_domain_for_node(self):
mac = factory.make_mac_address()
+ params = self.get_default_params()
+ params['mac'] = mac
self.assertEqual(
mac.node.nodegroup.name,
- self.get_pxeconfig({'mac': mac.mac_address}).get('domain'))
+ self.get_pxeconfig(params).get('domain'))
def get_without_param(self, param):
"""Request a `pxeconfig()` response, but omit `param` from request."""
@@ -3194,6 +3205,13 @@
fake_boot_purpose,
json.loads(response.content)["purpose"])
+ def test_pxeconfig_returns_fs_host_as_cluster_controller(self):
+ # The kernel parameter `fs_host` points to the cluster controller
+ # address, which is passed over within the `local` parameter.
+ params = self.get_default_params()
+ kernel_params = KernelParameters(**self.get_pxeconfig(params))
+ self.assertEqual(params["local"], kernel_params.fs_host)
+
class TestNodeGroupsAPI(APIv10TestMixin, MultipleUsersScenarios, TestCase):
scenarios = [
@@ -3917,63 +3935,102 @@
('celery', FixtureResource(CeleryFixture())),
)
- def report_images(self, images, client=None):
+ def report_images(self, nodegroup, images, client=None):
if client is None:
client = self.client
return client.post(
- reverse('boot_images_handler'),
- {'op': 'report_boot_images', 'images': json.dumps(images)})
+ reverse('boot_images_handler'), {
+ 'images': json.dumps(images),
+ 'nodegroup': nodegroup.uuid,
+ 'op': 'report_boot_images',
+ })
def test_report_boot_images_does_not_work_for_normal_user(self):
- NodeGroup.objects.ensure_master()
+ nodegroup = NodeGroup.objects.ensure_master()
log_in_as_normal_user(self.client)
- response = self.report_images([])
- self.assertEqual(httplib.FORBIDDEN, response.status_code)
+ response = self.report_images(nodegroup, [])
+ self.assertEqual(
+ httplib.FORBIDDEN, response.status_code, response.content)
def test_report_boot_images_works_for_master_worker(self):
- client = make_worker_client(NodeGroup.objects.ensure_master())
- response = self.report_images([], client=client)
+ nodegroup = NodeGroup.objects.ensure_master()
+ client = make_worker_client(nodegroup)
+ response = self.report_images(nodegroup, [], client=client)
self.assertEqual(httplib.OK, response.status_code)
def test_report_boot_images_stores_images(self):
+ nodegroup = NodeGroup.objects.ensure_master()
image = make_boot_image_params()
- client = make_worker_client(NodeGroup.objects.ensure_master())
- response = self.report_images([image], client=client)
+ client = make_worker_client(nodegroup)
+ response = self.report_images(nodegroup, [image], client=client)
self.assertEqual(
(httplib.OK, "OK"),
(response.status_code, response.content))
self.assertTrue(
- BootImage.objects.have_image(**image))
+ BootImage.objects.have_image(nodegroup=nodegroup, **image))
def test_report_boot_images_ignores_unknown_image_properties(self):
+ nodegroup = NodeGroup.objects.ensure_master()
image = make_boot_image_params()
image['nonesuch'] = factory.make_name('nonesuch'),
- client = make_worker_client(NodeGroup.objects.ensure_master())
- response = self.report_images([image], client=client)
+ client = make_worker_client(nodegroup)
+ response = self.report_images(nodegroup, [image], client=client)
self.assertEqual(
(httplib.OK, "OK"),
(response.status_code, response.content))
def test_report_boot_images_warns_if_no_images_found(self):
- recorder = self.patch(api, 'register_persistent_error')
- client = make_worker_client(NodeGroup.objects.ensure_master())
-
- response = self.report_images([], client=client)
- self.assertEqual(
- (httplib.OK, "OK"),
- (response.status_code, response.content))
-
- self.assertIn(
- COMPONENT.IMPORT_PXE_FILES,
- [args[0][0] for args in recorder.call_args_list])
+ nodegroup = NodeGroup.objects.ensure_master()
+ factory.make_node_group() # Second nodegroup with no images.
+ recorder = self.patch(api, 'register_persistent_error')
+ client = make_worker_client(nodegroup)
+ response = self.report_images(nodegroup, [], client=client)
+ self.assertEqual(
+ (httplib.OK, "OK"),
+ (response.status_code, response.content))
+
+ self.assertIn(
+ COMPONENT.IMPORT_PXE_FILES,
+ [args[0][0] for args in recorder.call_args_list])
+ # Check that the persistent error message contains a link to the
+ # clusters listing.
+ self.assertIn(
+ "/settings/#accepted-clusters", recorder.call_args_list[0][0][1])
+
+ def test_report_boot_images_warns_if_any_nodegroup_has_no_images(self):
+ nodegroup = NodeGroup.objects.ensure_master()
+ # Second nodegroup with no images.
+ factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
+ recorder = self.patch(api, 'register_persistent_error')
+ client = make_worker_client(nodegroup)
+ image = make_boot_image_params()
+ response = self.report_images(nodegroup, [image], client=client)
+ self.assertEqual(
+ (httplib.OK, "OK"),
+ (response.status_code, response.content))
+
+ self.assertIn(
+ COMPONENT.IMPORT_PXE_FILES,
+ [args[0][0] for args in recorder.call_args_list])
+
+ def test_report_boot_images_ignores_non_accepted_groups(self):
+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
+ factory.make_node_group(status=NODEGROUP_STATUS.PENDING)
+ factory.make_node_group(status=NODEGROUP_STATUS.REJECTED)
+ recorder = self.patch(api, 'register_persistent_error')
+ client = make_worker_client(nodegroup)
+ image = make_boot_image_params()
+ response = self.report_images(nodegroup, [image], client=client)
+ self.assertEqual(0, recorder.call_count)
def test_report_boot_images_removes_warning_if_images_found(self):
self.patch(api, 'register_persistent_error')
self.patch(api, 'discard_persistent_error')
- client = make_worker_client(NodeGroup.objects.ensure_master())
+ nodegroup = factory.make_node_group()
+ image = make_boot_image_params()
+ client = make_worker_client(nodegroup)
- response = self.report_images(
- [make_boot_image_params()], client=client)
+ response = self.report_images(nodegroup, [image], client=client)
self.assertEqual(
(httplib.OK, "OK"),
(response.status_code, response.content))
=== modified file 'src/maasserver/tests/test_bootimage.py'
--- src/maasserver/tests/test_bootimage.py 2012-09-14 14:16:01 +0000
+++ src/maasserver/tests/test_bootimage.py 2012-10-30 10:29:27 +0000
@@ -12,7 +12,10 @@
__metaclass__ = type
__all__ = []
-from maasserver.models import BootImage
+from maasserver.models import (
+ BootImage,
+ NodeGroup,
+ )
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
from provisioningserver.testing.boot_images import make_boot_image_params
@@ -20,22 +23,30 @@
class TestBootImageManager(TestCase):
+ def setUp(self):
+ super(TestBootImageManager, self).setUp()
+ self.nodegroup = NodeGroup.objects.ensure_master()
+
def test_have_image_returns_False_if_image_not_available(self):
self.assertFalse(
- BootImage.objects.have_image(**make_boot_image_params()))
+ BootImage.objects.have_image(
+ self.nodegroup, **make_boot_image_params()))
def test_have_image_returns_True_if_image_available(self):
params = make_boot_image_params()
- factory.make_boot_image(**params)
- self.assertTrue(BootImage.objects.have_image(**params))
+ factory.make_boot_image(nodegroup=self.nodegroup, **params)
+ self.assertTrue(
+ BootImage.objects.have_image(self.nodegroup, **params))
def test_register_image_registers_new_image(self):
params = make_boot_image_params()
- BootImage.objects.register_image(**params)
- self.assertTrue(BootImage.objects.have_image(**params))
+ BootImage.objects.register_image(self.nodegroup, **params)
+ self.assertTrue(
+ BootImage.objects.have_image(self.nodegroup, **params))
def test_register_image_leaves_existing_image_intact(self):
params = make_boot_image_params()
- factory.make_boot_image(**params)
- BootImage.objects.register_image(**params)
- self.assertTrue(BootImage.objects.have_image(**params))
+ factory.make_boot_image(nodegroup=self.nodegroup, **params)
+ BootImage.objects.register_image(self.nodegroup, **params)
+ self.assertTrue(
+ BootImage.objects.have_image(self.nodegroup, **params))
=== modified file 'src/maasserver/tests/test_config.py'
--- src/maasserver/tests/test_config.py 2012-05-11 07:51:59 +0000
+++ src/maasserver/tests/test_config.py 2012-10-30 10:29:27 +0000
@@ -16,10 +16,8 @@
from fixtures import TestWithFixtures
from maasserver.models import Config
-from maasserver.models.config import (
- DEFAULT_CONFIG,
- get_default_config,
- )
+import maasserver.models.config
+from maasserver.models.config import get_default_config
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
@@ -31,6 +29,14 @@
default_config = get_default_config()
self.assertEqual(gethostname(), default_config['maas_name'])
+ def test_defaults(self):
+ expected = get_default_config()
+ observed = {
+ name: Config.objects.get_config(name)
+ for name in expected
+ }
+ self.assertEqual(expected, observed)
+
class CallRecorder:
"""A utility class which tracks the calls to its 'call' method and
@@ -63,13 +69,15 @@
def test_manager_get_config_not_found_in_default_config(self):
name = factory.getRandomString()
value = factory.getRandomString()
- DEFAULT_CONFIG[name] = value
+ self.patch(maasserver.models.config, "DEFAULT_CONFIG", {name: value})
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'}
+ self.patch(
+ maasserver.models.config, "DEFAULT_CONFIG",
+ {name: {'key': 'value'}})
config = Config.objects.get_config(name)
config.update({'key2': 'value2'})
=== modified file 'src/maasserver/tests/test_preseed.py'
--- src/maasserver/tests/test_preseed.py 2012-10-05 04:21:12 +0000
+++ src/maasserver/tests/test_preseed.py 2012-10-30 10:29:27 +0000
@@ -330,10 +330,10 @@
)
def test_squashfs_available(self):
- BootImage.objects.register_image(
- self.arch, self.subarch, self.series, self.purpose)
node = factory.make_node(
architecture="i386/generic", distro_series="quantal")
+ BootImage.objects.register_image(
+ node.nodegroup, self.arch, self.subarch, self.series, self.purpose)
self.assertEqual(self.present, is_squashfs_image_present(node))
=== modified file 'src/maasserver/tests/test_start_up.py'
--- src/maasserver/tests/test_start_up.py 2012-09-28 18:42:16 +0000
+++ src/maasserver/tests/test_start_up.py 2012-10-30 10:29:27 +0000
@@ -32,9 +32,9 @@
NodeGroup,
)
from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
from maastesting.celery import CeleryFixture
from maastesting.fakemethod import FakeMethod
-from maastesting.testcase import TestCase
from mock import Mock
from provisioningserver import tasks
from testresources import FixtureResource
=== modified file 'src/maasserver/tests/test_views_settings.py'
--- src/maasserver/tests/test_views_settings.py 2012-10-03 15:48:11 +0000
+++ src/maasserver/tests/test_views_settings.py 2012-10-30 10:29:27 +0000
@@ -82,6 +82,7 @@
self.patch(settings, "DNS_CONNECT", False)
new_name = factory.getRandomString()
new_domain = factory.getRandomString()
+ new_proxy = "http://%s.example.com:1234/" % factory.getRandomString()
response = self.client.post(
reverse('settings'),
get_prefixed_form_data(
@@ -89,12 +90,16 @@
data={
'maas_name': new_name,
'enlistment_domain': new_domain,
+ 'http_proxy': new_proxy,
}))
-
- self.assertEqual(httplib.FOUND, response.status_code)
- self.assertEqual(new_name, Config.objects.get_config('maas_name'))
+ self.assertEqual(httplib.FOUND, response.status_code, response.content)
self.assertEqual(
- new_domain, Config.objects.get_config('enlistment_domain'))
+ (new_name,
+ new_domain,
+ new_proxy),
+ (Config.objects.get_config('maas_name'),
+ Config.objects.get_config('enlistment_domain'),
+ Config.objects.get_config('http_proxy')))
def test_settings_commissioning_POST(self):
new_after_commissioning = factory.getRandomEnum(
=== modified file 'src/provisioningserver/boot_images.py'
--- src/provisioningserver/boot_images.py 2012-09-14 11:04:33 +0000
+++ src/provisioningserver/boot_images.py 2012-10-30 10:29:27 +0000
@@ -32,6 +32,7 @@
from provisioningserver.config import Config
from provisioningserver.logging import task_logger
from provisioningserver.pxe import tftppath
+from provisioningserver.start_cluster_controller import get_cluster_uuid
def get_cached_knowledge():
@@ -53,7 +54,7 @@
"""Submit images to server."""
MAASClient(MAASOAuth(*api_credentials), MAASDispatcher(), maas_url).post(
'api/1.0/boot-images/', 'report_boot_images',
- images=json.dumps(images))
+ nodegroup=get_cluster_uuid(), images=json.dumps(images))
def report_to_server():
=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py 2012-10-11 13:25:43 +0000
+++ src/provisioningserver/tasks.py 2012-10-30 10:29:27 +0000
@@ -23,6 +23,7 @@
'write_full_dns_config',
]
+import os
from subprocess import (
CalledProcessError,
check_call,
@@ -347,6 +348,11 @@
UPDATE_NODE_TAGS_RETRY_DELAY = 2
+# =====================================================================
+# Tags-related tasks
+# =====================================================================
+
+
@task(max_retries=UPDATE_NODE_TAGS_MAX_RETRY)
def update_node_tags(tag_name, tag_definition, retry=True):
"""Update the nodes for a new/changed tag definition.
@@ -363,3 +369,16 @@
exc=exc, countdown=UPDATE_NODE_TAGS_RETRY_DELAY)
else:
raise
+
+
+# =====================================================================
+# Image importing-related tasks
+# =====================================================================
+
+@task
+def import_pxe_files(http_proxy=None):
+ env = dict(os.environ)
+ if http_proxy is not None:
+ env['http_proxy'] = http_proxy
+ env['https_proxy'] = http_proxy
+ check_call(['maas-import-pxe-files'], env=env)
=== modified file 'src/provisioningserver/testing/boot_images.py'
--- src/provisioningserver/testing/boot_images.py 2012-09-14 14:16:01 +0000
+++ src/provisioningserver/testing/boot_images.py 2012-10-30 10:29:27 +0000
@@ -17,7 +17,7 @@
from maastesting.factory import factory
-def make_boot_image_params(**kwargs):
+def make_boot_image_params():
"""Create an arbitrary dict of boot-image parameters.
These are the parameters that together describe a kind of boot that we
@@ -25,10 +25,8 @@
Ubuntu release, and boot purpose. See the `tftppath` module for how
these fit together.
"""
- fields = dict(
+ return dict(
architecture=factory.make_name('architecture'),
subarchitecture=factory.make_name('subarchitecture'),
release=factory.make_name('release'),
purpose=factory.make_name('purpose'))
- fields.update(kwargs)
- return fields
=== modified file 'src/provisioningserver/tests/test_boot_images.py'
--- src/provisioningserver/tests/test_boot_images.py 2012-10-04 05:49:09 +0000
+++ src/provisioningserver/tests/test_boot_images.py 2012-10-30 10:29:27 +0000
@@ -15,7 +15,10 @@
import json
from apiclient.maas_client import MAASClient
-from mock import Mock
+from mock import (
+ Mock,
+ sentinel,
+ )
from provisioningserver import boot_images
from provisioningserver.pxe import tftppath
from provisioningserver.testing.boot_images import make_boot_image_params
@@ -34,11 +37,12 @@
self.set_api_credentials()
image = make_boot_image_params()
self.patch(tftppath, 'list_boot_images', Mock(return_value=[image]))
+ get_cluster_uuid = self.patch(boot_images, "get_cluster_uuid")
+ get_cluster_uuid.return_value = sentinel.uuid
self.patch(MAASClient, 'post')
-
boot_images.report_to_server()
-
args, kwargs = MAASClient.post.call_args
+ self.assertIs(sentinel.uuid, kwargs["nodegroup"])
self.assertItemsEqual([image], json.loads(kwargs['images']))
def test_does_nothing_without_maas_url(self):
=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py 2012-10-08 08:24:11 +0000
+++ src/provisioningserver/tests/test_tasks.py 2012-10-30 10:29:27 +0000
@@ -25,6 +25,7 @@
from apiclient.maas_client import MAASClient
from apiclient.testing.credentials import make_api_credentials
from celery.app import app_or_default
+from celery.task import Task
from maastesting.celery import CeleryFixture
from maastesting.factory import factory
from maastesting.fakemethod import (
@@ -32,7 +33,10 @@
MultiFakeMethod,
)
from maastesting.matchers import ContainsAll
-from mock import Mock
+from mock import (
+ ANY,
+ Mock,
+ )
from netaddr import IPNetwork
from provisioningserver import (
auth,
@@ -59,6 +63,7 @@
from provisioningserver.tags import MissingCredentials
from provisioningserver.tasks import (
add_new_dhcp_host_map,
+ import_pxe_files,
Omshell,
power_off,
power_on,
@@ -534,3 +539,26 @@
self.assertRaises(
MissingCredentials, update_node_tags.delay, tag,
'//node', retry=True)
+
+
+class TestImportPxeFiles(PservTestCase):
+
+ def test_import_pxe_files(self):
+ recorder = self.patch(tasks, 'check_call', Mock())
+ import_pxe_files()
+ recorder.assert_called_once_with(['maas-import-pxe-files'], env=ANY)
+ self.assertIsInstance(import_pxe_files, Task)
+
+ def test_import_pxe_files_preserves_environment(self):
+ recorder = self.patch(tasks, 'check_call', Mock())
+ import_pxe_files()
+ recorder.assert_called_once_with(
+ ['maas-import-pxe-files'], env=os.environ)
+
+ def test_import_pxe_files_sets_proxy(self):
+ recorder = self.patch(tasks, 'check_call', Mock())
+ proxy = factory.getRandomString()
+ import_pxe_files(http_proxy=proxy)
+ expected_env = dict(os.environ, http_proxy=proxy, https_proxy=proxy)
+ recorder.assert_called_once_with(
+ ['maas-import-pxe-files'], env=expected_env)
=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py 2012-10-02 10:53:22 +0000
+++ src/provisioningserver/tests/test_tftp.py 2012-10-30 10:29:27 +0000
@@ -35,6 +35,7 @@
inlineCallbacks,
succeed,
)
+from twisted.python import context
from zope.interface.verify import verifyObject
@@ -213,6 +214,16 @@
mac = factory.getRandomMACAddress(b"-")
config_path = compose_config_path(mac)
backend = TFTPBackend(self.make_dir(), b"http://example.com/")
+ # python-tx-tftp sets up call context so that backends can discover
+ # more about the environment in which they're running.
+ call_context = {
+ "local": (
+ factory.getRandomIPAddress(),
+ factory.getRandomPort()),
+ "remote": (
+ factory.getRandomIPAddress(),
+ factory.getRandomPort()),
+ }
@partial(self.patch, backend, "get_config_reader")
def get_config_reader(params):
@@ -220,9 +231,16 @@
params_json_reader = BytesReader(params_json)
return succeed(params_json_reader)
- reader = yield backend.get_reader(config_path)
+ reader = yield context.call(
+ call_context, backend.get_reader, config_path)
output = reader.read(10000)
- expected_params = dict(mac=mac)
+ # The addresses provided by python-tx-tftp in the call context are
+ # passed over the wire as address:port strings.
+ expected_params = {
+ "mac": mac,
+ "local": call_context["local"][0], # address only.
+ "remote": call_context["remote"][0], # address only.
+ }
observed_params = json.loads(output)
self.assertEqual(expected_params, observed_params)
=== modified file 'src/provisioningserver/tftp.py'
--- src/provisioningserver/tftp.py 2012-10-03 08:50:17 +0000
+++ src/provisioningserver/tftp.py 2012-10-30 10:29:27 +0000
@@ -34,6 +34,7 @@
IReader,
)
from tftp.errors import FileNotFound
+from twisted.python.context import get
from twisted.web.client import getPage
import twisted.web.error
from zope.interface import implementer
@@ -211,6 +212,11 @@
for key, value in config_file_match.groupdict().items()
if value is not None
}
+ # Send the local and remote endpoint addresses.
+ local_host, local_port = get("local", (None, None))
+ params["local"] = local_host
+ remote_host, remote_port = get("remote", (None, None))
+ params["remote"] = remote_host
d = self.get_config_reader(params)
d.addErrback(self.get_page_errback, file_name)
return d
Follow ups