launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #08001
[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