launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06667
[Merge] lp:~jtv/maas/power-type into lp:maas
Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/power-type into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~jtv/maas/power-type/+merge/96986
Make power_type (for booting nodes) configurable, both globally and as a node-specific choice. This branch covers only the back-end side of that feature.
It works like this: a node may optionally have a power_type setting. If it has none, whenever we send the node's data to cobbler, we use a configuration setting instead. The configuration setting defaults to wake-on-LAN.
The new attribute is covered by two overlapping integration tests: one covers the part from maasserver to provisioningserver, and the other does the part from provisioningserver to cobbler. They're otherwise very similar.
--
https://code.launchpad.net/~jtv/maas/power-type/+merge/96986
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/power-type into lp:maas.
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py 2012-03-12 07:25:31 +0000
+++ src/maasserver/models.py 2012-03-13 06:03:18 +0000
@@ -54,6 +54,10 @@
Consumer,
Token,
)
+from provisioningserver.enum import (
+ POWER_TYPE,
+ POWER_TYPE_CHOICES,
+ )
# Special users internal to MaaS.
SYSTEM_USERS = [
@@ -367,6 +371,9 @@
max_length=10, choices=ARCHITECTURE_CHOICES, blank=False,
default=ARCHITECTURE.i386)
+ power_type = models.CharField(
+ max_length=10, choices=POWER_TYPE_CHOICES, null=True, blank=True)
+
objects = NodeManager()
def __unicode__(self):
@@ -406,6 +413,17 @@
if mac:
mac.delete()
+ def get_effective_power_type(self):
+ """Get power-type to use for this node.
+
+ If no power type has been set for the node, get the configured
+ default.
+ """
+ if self.power_type:
+ return self.power_type
+ else:
+ return Config.objects.get_config('node_power_type')
+
def acquire(self, by_user):
"""Mark commissioned node as acquired by the given user."""
assert self.status == NODE_STATUS.READY
@@ -731,6 +749,7 @@
# Commissioning section configuration.
'after_commissioning': NODE_AFTER_COMMISSIONING_ACTION.DEFAULT,
'check_compatibility': False,
+ 'node_power_type': POWER_TYPE.WAKE_ON_LAN,
# Ubuntu section configuration.
'fallback_master_archive': False,
'keep_mirror_list_uptodate': False,
=== modified file 'src/maasserver/provisioning.py'
--- src/maasserver/provisioning.py 2012-03-12 07:25:31 +0000
+++ src/maasserver/provisioning.py 2012-03-13 06:03:18 +0000
@@ -92,7 +92,11 @@
"""Create or update nodes in the provisioning server."""
papi = get_provisioning_api_proxy()
profile = select_profile_for_node(instance, papi)
- papi.add_node(instance.system_id, profile, compose_metadata(instance))
+ power_type = instance.get_effective_power_type()
+ metadata = compose_metadata(instance)
+ papi.add_node(
+ name=instance.system_id, profile=profile, power_type=power_type,
+ metadata=metadata)
def set_node_mac_addresses(node):
=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py 2012-03-13 05:34:38 +0000
+++ src/maasserver/tests/test_models.py 2012-03-13 06:03:18 +0000
@@ -42,6 +42,7 @@
SYSTEM_USERS,
UserProfile,
)
+from maasserver.testing.enum import map_enum
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
from metadataserver.models import NodeUserData
@@ -51,6 +52,7 @@
SECRET_SIZE,
Token,
)
+from provisioningserver.enum import POWER_TYPE
from testtools.matchers import (
GreaterThan,
LessThan,
@@ -89,6 +91,23 @@
node=node, mac_address='AA:BB:CC:DD:EE:FF').count()
self.assertEqual(0, macs)
+ def test_get_effective_power_type_defaults_to_config(self):
+ power_types = list(map_enum(POWER_TYPE).values())
+ node = factory.make_node(power_type=None)
+ effective_types = []
+ for power_type in power_types:
+ Config.objects.set_config('node_power_type', power_type)
+ effective_types.append(node.get_effective_power_type())
+ self.assertEqual(power_types, effective_types)
+
+ def test_get_effective_power_type_reads_node_field(self):
+ power_types = list(map_enum(POWER_TYPE).values())
+ nodes = [
+ factory.make_node(power_type=power_type)
+ for power_type in power_types]
+ self.assertEqual(
+ power_types, [node.get_effective_power_type() for node in nodes])
+
def test_acquire(self):
node = factory.make_node(status=NODE_STATUS.READY)
user = factory.make_user()
=== modified file 'src/maasserver/tests/test_provisioning.py'
--- src/maasserver/tests/test_provisioning.py 2012-03-13 05:34:38 +0000
+++ src/maasserver/tests/test_provisioning.py 2012-03-13 06:03:18 +0000
@@ -30,6 +30,7 @@
from maasserver.testing.factory import factory
from maasserver.testing.testcase import TestCase
from metadataserver.models import NodeKey
+from provisioningserver.enum import POWER_TYPE
class ProvisioningTests:
@@ -88,6 +89,19 @@
pserv_node = self.papi.get_nodes_by_name([system_id])[system_id]
self.assertEqual("precise-i386", pserv_node["profile"])
+ def test_provision_post_save_Node_registers_effective_power_type(self):
+ power_types = list(map_enum(POWER_TYPE).values()) + [None]
+ nodes = {
+ power_type: factory.make_node(power_type=power_type)
+ for power_type in power_types}
+ effective_power_types = {
+ power_type: node.get_effective_power_type()
+ for power_type, node in nodes.items()}
+ pserv_power_types = {
+ power_type: self.papi.power_types[node.system_id]
+ for power_type, node in nodes.items()}
+ self.assertEqual(effective_power_types, pserv_power_types)
+
def test_provision_post_save_MACAddress_create(self):
# Creating and saving a MACAddress updates the Node with which it's
# associated.
=== modified file 'src/provisioningserver/api.py'
--- src/provisioningserver/api.py 2012-02-29 10:40:06 +0000
+++ src/provisioningserver/api.py 2012-03-13 06:03:18 +0000
@@ -21,6 +21,7 @@
CobblerProfile,
CobblerSystem,
)
+from provisioningserver.enum import POWER_TYPE
from provisioningserver.interfaces import IProvisioningAPI
from provisioningserver.utils import deferred
from twisted.internet.defer import (
@@ -187,13 +188,15 @@
returnValue(profile.name)
@inlineCallbacks
- def add_node(self, name, profile, metadata):
+ def add_node(self, name, profile, power_type, metadata):
assert isinstance(name, basestring)
assert isinstance(profile, basestring)
+ assert power_type in (POWER_TYPE.VIRSH, POWER_TYPE.WAKE_ON_LAN)
assert isinstance(metadata, dict)
attributes = {
"profile": profile,
"ks_meta": {"MAAS_PRESEED": metadata_preseed % metadata},
+ "power_type": power_type,
}
system = yield CobblerSystem.new(self.session, name, attributes)
returnValue(system.name)
=== added file 'src/provisioningserver/enum.py'
--- src/provisioningserver/enum.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/enum.py 2012-03-13 06:03:18 +0000
@@ -0,0 +1,31 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Enumerations meaningful to the provisioning server."""
+
+from __future__ import (
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'POWER_TYPE',
+ 'POWER_TYPE_CHOICES',
+ ]
+
+
+class POWER_TYPE:
+ """Choice of mechanism to control a node's power."""
+
+ # Use virsh (for virtual machines).
+ VIRSH = 'virsh'
+
+ # Network wake-up.
+ WAKE_ON_LAN = 'ether_wake'
+
+
+POWER_TYPE_CHOICES = (
+ (POWER_TYPE.VIRSH, "virsh (virtual systems)"),
+ (POWER_TYPE.WAKE_ON_LAN, "Wake-on-LAN"),
+ )
=== modified file 'src/provisioningserver/interfaces.py'
--- src/provisioningserver/interfaces.py 2012-02-29 10:40:06 +0000
+++ src/provisioningserver/interfaces.py 2012-03-13 06:03:18 +0000
@@ -50,10 +50,12 @@
:return: The name of the new profile.
"""
- def add_node(name, profile, metadata):
+ def add_node(name, profile, power_type, metadata):
"""Add a node with the given `name`.
:param profile: Name of profile to associate the node with.
+ :param power_type: A choice of power-control method, as in
+ :class:POWER_TYPE.
:param metadata: Dict of ks_meta items to pre-seed into the node.
Should include maas-metadata-url (URL for the metadata service)
and maas-metadata-credentials (OAuth token for accessing the
=== modified file 'src/provisioningserver/testing/fakeapi.py'
--- src/provisioningserver/testing/fakeapi.py 2012-03-12 12:26:24 +0000
+++ src/provisioningserver/testing/fakeapi.py 2012-03-13 06:03:18 +0000
@@ -73,6 +73,11 @@
self.distros = FakeProvisioningDatabase()
self.profiles = FakeProvisioningDatabase()
self.nodes = FakeProvisioningDatabase()
+ # Record power_type settings for nodes (by node name). This is
+ # not part of the provisioning-server node as returned to the
+ # maasserver, so it's not stored as a regular attribute even if
+ # it works like one internally.
+ self.power_types = {}
# This records nodes that start/stop commands have been issued
# for. If a node has been started, its name maps to 'start'; if
# it has been stopped, its name maps to 'stop' (whichever
@@ -88,10 +93,11 @@
self.profiles[name]["distro"] = distro
return name
- def add_node(self, name, profile, metadata):
+ def add_node(self, name, profile, power_type, metadata):
self.nodes[name]["profile"] = profile
self.nodes[name]["mac_addresses"] = []
self.nodes[name]["metadata"] = metadata
+ self.power_types[name] = power_type
return name
def modify_distros(self, deltas):
=== modified file 'src/provisioningserver/tests/test_api.py'
--- src/provisioningserver/tests/test_api.py 2012-03-12 12:24:42 +0000
+++ src/provisioningserver/tests/test_api.py 2012-03-13 06:03:18 +0000
@@ -28,6 +28,7 @@
from unittest import skipIf
from urlparse import urlparse
+from maasserver.testing.enum import map_enum
from provisioningserver.api import (
cobbler_to_papi_distro,
cobbler_to_papi_node,
@@ -40,6 +41,7 @@
CobblerSession,
CobblerSystem,
)
+from provisioningserver.enum import POWER_TYPE
from provisioningserver.interfaces import IProvisioningAPI
from provisioningserver.testing.fakeapi import FakeAsynchronousProvisioningAPI
from provisioningserver.testing.fakecobbler import make_fake_cobbler_session
@@ -306,7 +308,8 @@
returnValue(profile_name)
@inlineCallbacks
- def add_node(self, papi, name=None, profile_name=None, metadata=None):
+ def add_node(self, papi, name=None, profile_name=None, power_type=None,
+ metadata=None):
"""Creates a new node object via `papi`.
Arranges for it to be deleted during test clean-up. If `name` is not
@@ -319,10 +322,13 @@
name = fake_name()
if profile_name is None:
profile_name = yield self.add_profile(papi)
+ if power_type is None:
+ power_type = POWER_TYPE.WAKE_ON_LAN
if metadata is None:
metadata = fake_node_metadata()
node_name = yield papi.add_node(
- name, profile_name, metadata)
+ name=name, profile=profile_name, power_type=power_type,
+ metadata=metadata)
self.addCleanup(
self.cleanup_objects,
papi.delete_nodes_by_name,
@@ -571,6 +577,25 @@
pass
+class ProvisioningAPITestsWithCobbler:
+ """Provisioning API tests that also access a real, or fake, Cobbler."""
+
+ @inlineCallbacks
+ def test_add_node_sets_power_type(self):
+ papi = self.get_provisioning_api()
+ power_types = list(map_enum(POWER_TYPE).values())
+ nodes = {}
+ for power_type in power_types:
+ nodes[power_type] = yield self.add_node(
+ papi, power_type=power_type)
+ cobbler_power_types = {}
+ for power_type, node_id in nodes.items():
+ attrs = yield CobblerSystem(papi.session, node_id).get_values()
+ cobbler_power_types[power_type] = attrs['power_type']
+ self.assertItemsEqual(
+ dict(zip(power_types, power_types)), cobbler_power_types)
+
+
class TestFakeProvisioningAPI(ProvisioningAPITests, TestCase):
"""Test :class:`FakeAsynchronousProvisioningAPI`.
@@ -582,7 +607,9 @@
return FakeAsynchronousProvisioningAPI()
-class TestProvisioningAPIWithFakeCobbler(ProvisioningAPITests, TestCase):
+class TestProvisioningAPIWithFakeCobbler(ProvisioningAPITests,
+ ProvisioningAPITestsWithCobbler,
+ TestCase):
"""Test :class:`ProvisioningAPI` with a fake Cobbler instance.
Includes by inheritance all the tests in :class:`ProvisioningAPITests`.
@@ -603,7 +630,9 @@
self.assertIn(metadata['maas-metadata-credentials'], preseed)
-class TestProvisioningAPIWithRealCobbler(ProvisioningAPITests, TestCase):
+class TestProvisioningAPIWithRealCobbler(ProvisioningAPITests,
+ ProvisioningAPITestsWithCobbler,
+ TestCase):
"""Test :class:`ProvisioningAPI` with a real Cobbler instance.
The URL for the Cobbler instance must be provided in the