← Back to team overview

launchpad-reviewers team mailing list archive

[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