← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~julian-edwards/maas/power-basics into lp:maas

 

Julian Edwards has proposed merging lp:~julian-edwards/maas/power-basics into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~julian-edwards/maas/power-basics/+merge/106299

This is the beginnings of a power library for maas.  For now it just allows the user to create a PowerAction object which is going to read the actual power-related action from a template script to which you need to pass parameters.

The template is a big-standard Python interpolation.  If we need, we can use another templating language in the future.  We have easy access to Django's here.

I can't remember who I pre-imped with apart from Daviey who agreed that using templates is the way to go as it's pretty flexible.
-- 
https://code.launchpad.net/~julian-edwards/maas/power-basics/+merge/106299
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~julian-edwards/maas/power-basics into lp:maas.
=== modified file 'src/maas/settings.py'
--- src/maas/settings.py	2012-04-30 02:15:00 +0000
+++ src/maas/settings.py	2012-05-18 02:58:19 +0000
@@ -280,5 +280,10 @@
 # to have failed and mark it as FAILED_TESTS.
 COMMISSIONING_TIMEOUT = 60
 
+# Location of power action templates. Use an absolute path.
+POWER_TEMPLATES_DIR = os.path.join(
+    os.path.dirname(__file__), os.pardir, "provisioningserver", "power",
+    "templates")
+
 # Allow the user to override settings in maas_local_settings.
 import_local_settings()

=== modified file 'src/provisioningserver/enum.py'
--- src/provisioningserver/enum.py	2012-04-16 10:00:51 +0000
+++ src/provisioningserver/enum.py	2012-05-18 02:58:19 +0000
@@ -55,6 +55,12 @@
     # Network wake-up.
     WAKE_ON_LAN = 'ether_wake'
 
+    # IMPI (Intelligent Platform Management Interface).
+    IPMI = 'ipmi'
+
+    # IPMI over LAN.
+    IPMI_LAN = 'ipmi_lan'
+
 
 POWER_TYPE_CHOICES = (
     (POWER_TYPE.VIRSH, "virsh (virtual systems)"),

=== added directory 'src/provisioningserver/power'
=== added file 'src/provisioningserver/power/__init__.py'
=== added file 'src/provisioningserver/power/poweraction.py'
--- src/provisioningserver/power/poweraction.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/power/poweraction.py	2012-05-18 02:58:19 +0000
@@ -0,0 +1,76 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Actions for power-related operations."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = [
+    "PowerAction",
+    "PowerActionFail",
+    "UnknownPowerType",
+    ]
+
+
+import os
+import subprocess
+
+from django.conf import settings
+
+
+class UnknownPowerType(Exception):
+    """Raised when trying to process an unknown power type."""
+
+
+class PowerActionFail(Exception):
+    """Raised when there's a problem execting a power script."""
+
+
+class PowerAction:
+    """Actions for power-related operations."""
+
+    def __init__(self, power_type):
+        basedir = settings.POWER_TEMPLATES_DIR
+        self.path = os.path.join(basedir, power_type + ".template")
+        if not os.path.exists(self.path):
+            raise UnknownPowerType
+
+        self.power_type = power_type
+
+    def get_template(self):
+        with open(self.path, "r") as f:
+            template = f.read()
+        return template
+
+    def render_template(self, template, **kwargs):
+        try:
+            rendered = template % kwargs
+        except KeyError:
+            raise PowerActionFail(
+                "Not enough parameters supplied to the template")
+        return rendered
+
+    def execute(self, **kwargs):
+        template = self.get_template()
+        rendered = self.render_template(template, **kwargs)
+
+        # This might need retrying but it could be better to leave that
+        # to the individual scripts.
+        try:
+            proc = subprocess.Popen(
+                rendered, shell=True, stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE, close_fds=True)
+        except OSError, e:
+            raise PowerActionFail(e)
+
+        stdout, stderr = proc.communicate()
+        # TODO: log output on errors
+        code = proc.returncode
+        if code != 0:
+            raise PowerActionFail("%s failed with return code %s" % (
+                self.power_type, code))

=== added directory 'src/provisioningserver/power/templates'
=== added file 'src/provisioningserver/power/templates/ether_wake.template'
--- src/provisioningserver/power/templates/ether_wake.template	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/power/templates/ether_wake.template	2012-05-18 02:58:19 +0000
@@ -0,0 +1,6 @@
+set mac = %(mac)s
+if [ -x /usr/bin/wakeonlan ]; then
+    /usr/bin/wakeonlan $mac
+elif [ -x /usr/sbin/etherwake ]; then
+    /usr/sbin/etherwake $mac
+fi

=== added directory 'src/provisioningserver/power/tests'
=== added file 'src/provisioningserver/power/tests/test_poweraction.py'
--- src/provisioningserver/power/tests/test_poweraction.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/power/tests/test_poweraction.py	2012-05-18 02:58:19 +0000
@@ -0,0 +1,124 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `provisioningserver.power`.
+"""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from fixtures import TempDir
+import os
+from testtools import TestCase
+from textwrap import dedent
+
+from django.conf import settings
+from provisioningserver.enum import POWER_TYPE
+from provisioningserver.power.poweraction import (
+    PowerAction,
+    PowerActionFail,
+    UnknownPowerType,
+    )
+
+
+class TestPowerAction(TestCase):
+    """Tests for PowerAction."""
+
+    def test_init_raises_for_unknown_powertype(self):
+        # If constructed with a power type that doesn't map to a
+        # template file, UnknownPowerType should be raised.
+        powertype = "weinerschnitzel"
+        self.assertRaises(
+            UnknownPowerType,
+            PowerAction, powertype)
+
+    def test_init_stores_ether_wake_type(self):
+        # Using a power type that has a template file results in the
+        # power type stored on the object.
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        self.assertEqual(POWER_TYPE.WAKE_ON_LAN, pa.power_type)
+
+    def test_init_stores_template_path(self):
+        # Using a power type that has a template file results in the
+        # path to the template file being stored on the object.
+        power_type = POWER_TYPE.WAKE_ON_LAN
+        basedir = settings.POWER_TEMPLATES_DIR
+        path = os.path.join(basedir, power_type + ".template")
+        pa = PowerAction(power_type)
+        self.assertEqual(path, pa.path)
+
+    def test_get_template(self):
+        # get_template() should find and read the template file.
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        with open(pa.path, "r") as f:
+            template = f.read()
+        self.assertEqual(template, pa.get_template())
+
+    def test_render_template(self):
+        # render_template() should take a template string and substitue
+        # its variables.
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        template = "template: %(mac)s"
+        rendered = pa.render_template(template, mac="mymac")
+        self.assertEqual(
+            template % dict(mac="mymac"), rendered)
+
+    def test_render_template_raises_PowerActionFail(self):
+        # If not enough arguments are supplied to fill in template
+        # variables then a PowerActionFail is raised.
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        template = "template: %(mac)s"
+        exception = self.assertRaises(
+            PowerActionFail, pa.render_template, template)
+        self.assertEqual(
+            "Not enough parameters supplied to the template",
+            exception.message)
+
+    def _create_template_file(self, template):
+        tempdir = self.useFixture(TempDir()).path
+        path = os.path.join(tempdir, "testscript.sh")
+        with open(path, "w") as f:
+            f.write(template)
+        return path
+
+    def assertScriptOutput(self, path, output_file, expected, **kwargs):
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        pa.path = path
+        pa.execute(**kwargs)
+
+        # Check that it got executed by comparing the file it was
+        # supposed to write out.
+        with open(output_file, "r") as f:
+            output = f.read()
+
+        self.assertEqual("working test\n", output)
+
+    def test_execute(self):
+        # execute() should run the template through a shell.
+
+        # Create a template in a temp dir.
+        tempdir = self.useFixture(TempDir()).path
+        output_file = os.path.join(tempdir, "output")
+        template = dedent("""\
+            #!/bin/sh
+            echo working %(mac)s >""")
+        template += output_file
+        path = self._create_template_file(template)
+
+        self.assertScriptOutput(
+            path, output_file, "working test\n", mac="test")
+
+    def test_execute_raises_PowerActionFail_when_script_fails(self):
+        template = "this_is_not_valid_shell"
+        path = self._create_template_file(template)
+        pa = PowerAction(POWER_TYPE.WAKE_ON_LAN)
+        pa.path = path
+        exception = self.assertRaises(PowerActionFail, pa.execute)
+        self.assertEqual(
+            "ether_wake failed with return code 127", exception.message)


Follow ups