launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #10173
[Merge] lp:~allenap/maas/tftp-path-in-pserv into lp:maas
Gavin Panella has proposed merging lp:~allenap/maas/tftp-path-in-pserv into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~allenap/maas/tftp-path-in-pserv/+merge/116318
The diff is big, but there's a lot of movement and context. It's all
as a result of removing TFTPROOT from celeryconfig:
- Sets tftp/root in pserv.yaml to /var/lib/tftpboot.
- Makes TFTPROOT and MAAS_PROVISIONING_SETTINGS both mandatory
parameters in scripts/maas-import-pxe-files. I'm hopeful that we can
absorb this script into Python at some point, so this slight
ugliness is transient.
- Define PROVISIONING_SETTINGS in the Django configs. This is so the
Django app can load provisioning configuration info.
- Split out the configuration stuff from provisioningserver.plugin
into its own .config module (and corresponding test module).
- Create a new fixture, ConfigFixture.
- Create a new subclass of ActionScript, MainScript, which takes a
config file parameter. This is so all commands will uniformally be
able to consume a configuration file.
- Lots of other sed-like changes.
--
https://code.launchpad.net/~allenap/maas/tftp-path-in-pserv/+merge/116318
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/tftp-path-in-pserv into lp:maas.
=== modified file 'etc/celeryconfig.py'
--- etc/celeryconfig.py 2012-07-17 09:51:38 +0000
+++ etc/celeryconfig.py 2012-07-23 16:06:48 +0000
@@ -26,9 +26,6 @@
# None to use the templates installed with the running version of MAAS.
PXE_TEMPLATES_DIR = None
-# TFTP server's root directory.
-TFTPROOT = "/var/lib/tftpboot"
-
# Location of MAAS' bind configuration files.
DNS_CONFIG_DIR = '/var/cache/bind/maas'
=== modified file 'etc/pserv.yaml'
--- etc/pserv.yaml 2012-07-05 20:19:59 +0000
+++ etc/pserv.yaml 2012-07-23 16:06:48 +0000
@@ -60,7 +60,7 @@
## TFTP configuration.
#
tftp:
- # root: <current directory>
+ root: /var/lib/tftpboot
# port: 5244
## The URL to be contacted to generate PXE configurations.
# generator: http://localhost:5243/api/1.0/pxeconfig
=== modified file 'scripts/maas-import-pxe-files'
--- scripts/maas-import-pxe-files 2012-07-13 22:42:54 +0000
+++ scripts/maas-import-pxe-files 2012-07-23 16:06:48 +0000
@@ -34,8 +34,13 @@
# Supported architectures.
ARCHES=${ARCHES:-amd64 i386}
-# TFTP root directory. (Don't let the "root" vs. "boot" confuse you.)
-TFTPROOT=${TFTPROOT:-/var/lib/tftpboot}
+# TFTP root directory. Mandatory.
+# TODO: Remove this; it's here to support the obsolete
+# generate_enlistment_pxe command.
+TFTPROOT=${TFTPROOT?}
+
+# Path to the provisioning configuration. Mandatory.
+MAAS_PROVISIONING_SETTINGS=${MAAS_PROVISIONING_SETTINGS?}
# Command line to download a resource at a given URL into the current
# directory. A wget command line will work here, but curl will do as well.
@@ -70,7 +75,7 @@
then
# TODO: Pass sub-architecture once we support those.
maas-provision install-pxe-bootloader \
- --arch=$arch --loader='pxelinux.0' --tftproot=$TFTPROOT
+ --arch=$arch --loader='pxelinux.0'
fi
}
@@ -93,7 +98,7 @@
maas-provision install-pxe-image \
--arch=$arch --release=$release --purpose="install" \
- --image="install" --tftproot=$TFTPROOT
+ --image="install"
}
@@ -118,6 +123,7 @@
done
# TODO: Pass sub-architecture once we support those.
+ # TODO: Remove this; it's obsolete.
maas generate_enlistment_pxe \
--arch=$arch --release=$CURRENT_RELEASE \
--tftproot=$TFTPROOT
=== modified file 'src/maas/development.py'
--- src/maas/development.py 2012-07-11 09:06:57 +0000
+++ src/maas/development.py 2012-07-23 16:06:48 +0000
@@ -98,6 +98,8 @@
COMMISSIONING_SCRIPT = os.path.join(
DEV_ROOT_DIRECTORY, 'etc/maas/commissioning-user-data')
+PROVISIONING_SETTINGS = abspath("etc/pserv.yaml")
+
# Set up celery to use the demo settings.
os.environ['CELERY_CONFIG_MODULE'] = 'democeleryconfig'
=== modified file 'src/maas/settings.py'
--- src/maas/settings.py 2012-07-06 10:12:25 +0000
+++ src/maas/settings.py 2012-07-23 16:06:48 +0000
@@ -307,5 +307,11 @@
"/usr/share/maas/preseeds",
)
+# Settings used for provisioning.
+# TODO: un-cargo-cult this from provisioningserver.utils.MainScript.
+PROVISIONING_SETTINGS = os.environ.get(
+ "MAAS_PROVISIONING_SETTINGS", "/etc/maas/pserv.yaml")
+
+
# Allow the user to override settings in maas_local_settings.
import_local_settings()
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-07-12 12:33:57 +0000
+++ src/maasserver/api.py 2012-07-23 16:06:48 +0000
@@ -132,6 +132,7 @@
from piston.models import Token
from piston.resource import Resource
from piston.utils import rc
+import provisioningserver.config
from provisioningserver.pxe.pxeconfig import (
PXEConfig,
PXEConfigFail,
@@ -1037,10 +1038,14 @@
:param kernelimage: The path to the kernel in the TFTP server
:param append: Kernel parameters to append.
"""
+ provisioning_config = (
+ provisioningserver.config.Config.load_from_cache(
+ settings.PROVISIONING_SETTINGS))
arch = get_mandatory_param(request.GET, 'arch')
subarch = request.GET.get('subarch', None)
mac = request.GET.get('mac', None)
- config = PXEConfig(arch, subarch, mac)
+ tftproot = provisioning_config["tftp"]["root"]
+ config = PXEConfig(arch, subarch, mac, tftproot)
# Rendering parameters.
menutitle = get_mandatory_param(request.GET, 'menutitle')
kernelimage = get_mandatory_param(request.GET, 'kernelimage')
=== modified file 'src/maasserver/management/commands/generate_enlistment_pxe.py'
--- src/maasserver/management/commands/generate_enlistment_pxe.py 2012-06-26 07:27:06 +0000
+++ src/maasserver/management/commands/generate_enlistment_pxe.py 2012-07-23 16:06:48 +0000
@@ -29,6 +29,7 @@
class Command(BaseCommand):
"""Print out enlistment PXE config."""
+ # TODO: Remove this; it's obsolete.
option_list = BaseCommand.option_list + (
make_option(
=== modified file 'src/provisioningserver/__main__.py'
--- src/provisioningserver/__main__.py 2012-07-13 16:32:05 +0000
+++ src/provisioningserver/__main__.py 2012-07-23 16:06:48 +0000
@@ -15,10 +15,10 @@
import provisioningserver.dhcp.writer
import provisioningserver.pxe.install_bootloader
import provisioningserver.pxe.install_image
-from provisioningserver.utils import ActionScript
-
-
-main = ActionScript(__doc__)
+from provisioningserver.utils import MainScript
+
+
+main = MainScript(__doc__)
main.register(
"generate-dhcp-config",
provisioningserver.dhcp.writer)
=== added file 'src/provisioningserver/config.py'
--- src/provisioningserver/config.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/config.py 2012-07-23 16:06:48 +0000
@@ -0,0 +1,129 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""MAAS Provisioning Configuration."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ "Config",
+ ]
+
+from getpass import getuser
+from os.path import abspath
+from threading import RLock
+
+from formencode import Schema
+from formencode.validators import (
+ Int,
+ RequireIfPresent,
+ String,
+ URL,
+ )
+import yaml
+
+
+class ConfigOops(Schema):
+ """Configuration validator for OOPS options."""
+
+ if_key_missing = None
+
+ directory = String(if_missing=b"")
+ reporter = String(if_missing=b"")
+
+ chained_validators = (
+ RequireIfPresent("reporter", present="directory"),
+ )
+
+
+class ConfigBroker(Schema):
+ """Configuration validator for message broker options."""
+
+ if_key_missing = None
+
+ host = String(if_missing=b"localhost")
+ port = Int(min=1, max=65535, if_missing=5673)
+ username = String(if_missing=getuser())
+ password = String(if_missing=b"test")
+ vhost = String(if_missing="/")
+
+
+class ConfigCobbler(Schema):
+ """Configuration validator for connecting to Cobbler."""
+
+ if_key_missing = None
+
+ url = URL(
+ add_http=True, require_tld=False,
+ if_missing=b"http://localhost/cobbler_api",
+ )
+ username = String(if_missing=getuser())
+ password = String(if_missing=b"test")
+
+
+class ConfigTFTP(Schema):
+ """Configuration validator for the TFTP service."""
+
+ if_key_missing = None
+
+ root = String(if_missing="/var/lib/tftpboot")
+ port = Int(min=1, max=65535, if_missing=5244)
+ generator = URL(
+ add_http=True, require_tld=False,
+ if_missing=b"http://localhost:5243/api/1.0/pxeconfig",
+ )
+
+
+class Config(Schema):
+ """Configuration validator."""
+
+ if_key_missing = None
+
+ interface = String(if_empty=b"", if_missing=b"127.0.0.1")
+ port = Int(min=1, max=65535, if_missing=5241)
+ username = String(not_empty=True, if_missing=getuser())
+ password = String(not_empty=True)
+ logfile = String(if_empty=b"pserv.log", if_missing=b"pserv.log")
+ oops = ConfigOops
+ broker = ConfigBroker
+ cobbler = ConfigCobbler
+ tftp = ConfigTFTP
+
+ @classmethod
+ def parse(cls, stream):
+ """Load a YAML configuration from `stream` and validate."""
+ return cls.to_python(yaml.safe_load(stream))
+
+ @classmethod
+ def load(cls, filename):
+ """Load a YAML configuration from `filename` and validate."""
+ with open(filename, "rb") as stream:
+ return cls.parse(stream)
+
+ _cache = {}
+ _cache_lock = RLock()
+
+ @classmethod
+ def load_from_cache(cls, filename):
+ """Load or return a previously loaded configuration.
+
+ This is thread-safe, so is okay to use from Django, for example.
+ """
+ filename = abspath(filename)
+ with cls._cache_lock:
+ if filename not in cls._cache:
+ with open(filename, "rb") as stream:
+ cls._cache[filename] = cls.parse(stream)
+ return cls._cache[filename]
+
+ @classmethod
+ def field(target, *steps):
+ """Obtain a field by following `steps`."""
+ for step in steps:
+ target = target.fields[step]
+ return target
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py 2012-07-06 19:53:41 +0000
+++ src/provisioningserver/plugin.py 2012-07-23 16:06:48 +0000
@@ -12,18 +12,9 @@
__metaclass__ = type
__all__ = []
-from getpass import getuser
-
-from formencode import Schema
-from formencode.validators import (
- Int,
- RequireIfPresent,
- String,
- URL,
- )
from provisioningserver.amqpclient import AMQFactory
from provisioningserver.cobblerclient import CobblerSession
-from provisioningserver.pxe.tftppath import locate_tftp_path
+from provisioningserver.config import Config
from provisioningserver.remote import ProvisioningAPI_XMLRPC
from provisioningserver.services import (
LogService,
@@ -65,7 +56,6 @@
Resource,
)
from twisted.web.server import Site
-import yaml
from zope.interface import implementer
@@ -107,84 +97,6 @@
raise NotImplementedError()
-class ConfigOops(Schema):
- """Configuration validator for OOPS options."""
-
- if_key_missing = None
-
- directory = String(if_missing=b"")
- reporter = String(if_missing=b"")
-
- chained_validators = (
- RequireIfPresent("reporter", present="directory"),
- )
-
-
-class ConfigBroker(Schema):
- """Configuration validator for message broker options."""
-
- if_key_missing = None
-
- host = String(if_missing=b"localhost")
- port = Int(min=1, max=65535, if_missing=5673)
- username = String(if_missing=getuser())
- password = String(if_missing=b"test")
- vhost = String(if_missing="/")
-
-
-class ConfigCobbler(Schema):
- """Configuration validator for connecting to Cobbler."""
-
- if_key_missing = None
-
- url = URL(
- add_http=True, require_tld=False,
- if_missing=b"http://localhost/cobbler_api",
- )
- username = String(if_missing=getuser())
- password = String(if_missing=b"test")
-
-
-class ConfigTFTP(Schema):
- """Configuration validator for the TFTP service."""
-
- if_key_missing = None
-
- root = String(if_missing=locate_tftp_path())
- port = Int(min=1, max=65535, if_missing=5244)
- generator = URL(
- add_http=True, require_tld=False,
- if_missing=b"http://localhost:5243/api/1.0/pxeconfig",
- )
-
-
-class Config(Schema):
- """Configuration validator."""
-
- if_key_missing = None
-
- interface = String(if_empty=b"", if_missing=b"127.0.0.1")
- port = Int(min=1, max=65535, if_missing=5241)
- username = String(not_empty=True, if_missing=getuser())
- password = String(not_empty=True)
- logfile = String(if_empty=b"pserv.log", if_missing=b"pserv.log")
- oops = ConfigOops
- broker = ConfigBroker
- cobbler = ConfigCobbler
- tftp = ConfigTFTP
-
- @classmethod
- def parse(cls, stream):
- """Load a YAML configuration from `stream` and validate."""
- return cls.to_python(yaml.safe_load(stream))
-
- @classmethod
- def load(cls, filename):
- """Load a YAML configuration from `filename` and validate."""
- with open(filename, "rb") as stream:
- return cls.parse(stream)
-
-
class Options(usage.Options):
"""Command line options for the provisioning server."""
=== modified file 'src/provisioningserver/pxe/install_bootloader.py'
--- src/provisioningserver/pxe/install_bootloader.py 2012-07-13 16:01:59 +0000
+++ src/provisioningserver/pxe/install_bootloader.py 2012-07-23 16:06:48 +0000
@@ -19,7 +19,7 @@
import os.path
from shutil import copyfile
-from celeryconfig import TFTPROOT
+from provisioningserver.config import Config
from provisioningserver.pxe.tftppath import (
compose_bootloader_path,
locate_tftp_path,
@@ -97,10 +97,6 @@
parser.add_argument(
'--loader', dest='loader', default=None,
help="PXE pre-boot loader to install.")
- parser.add_argument(
- '--tftproot', dest='tftproot', default=TFTPROOT, help=(
- "Store to this TFTP directory tree instead of the "
- "default [%(default)s]."))
def run(args):
@@ -109,7 +105,9 @@
This won't overwrite an existing loader if its contents are unchanged.
However the new loader you give it will be deleted regardless.
"""
- destination = make_destination(args.tftproot, args.arch, args.subarch)
+ config = Config.load(args.config_file)
+ tftproot = config["tftp"]["root"]
+ destination = make_destination(tftproot, args.arch, args.subarch)
install_bootloader(args.loader, destination)
if os.path.exists(args.loader):
os.remove(args.loader)
=== modified file 'src/provisioningserver/pxe/install_image.py'
--- src/provisioningserver/pxe/install_image.py 2012-07-13 16:32:05 +0000
+++ src/provisioningserver/pxe/install_image.py 2012-07-23 16:06:48 +0000
@@ -22,7 +22,7 @@
rmtree,
)
-from celeryconfig import TFTPROOT
+from provisioningserver.config import Config
from provisioningserver.pxe.tftppath import (
compose_image_path,
locate_tftp_path,
@@ -130,10 +130,6 @@
parser.add_argument(
'--image', dest='image', default=None,
help="Netboot image directory, containing kernel & initrd.")
- parser.add_argument(
- '--tftproot', dest='tftproot', default=TFTPROOT, help=(
- "Store to this TFTP directory tree instead of the "
- "default [%(default)s]."))
def run(args):
@@ -144,8 +140,10 @@
containing identical files, the new image is deleted and the old one
is left untouched.
"""
+ config = Config.load(args.config_file)
+ tftproot = config["tftp"]["root"]
destination = make_destination(
- args.tftproot, args.arch, args.subarch, args.release, args.purpose)
+ tftproot, args.arch, args.subarch, args.release, args.purpose)
if not are_identical_dirs(destination, args.image):
# Image has changed. Move the new version into place.
install_dir(args.image, destination)
=== modified file 'src/provisioningserver/pxe/tests/test_install_bootloader.py'
--- src/provisioningserver/pxe/tests/test_install_bootloader.py 2012-07-13 16:01:59 +0000
+++ src/provisioningserver/pxe/tests/test_install_bootloader.py 2012-07-23 16:06:48 +0000
@@ -29,7 +29,8 @@
compose_bootloader_path,
locate_tftp_path,
)
-from provisioningserver.utils import ActionScript
+from provisioningserver.testing.config import ConfigFixture
+from provisioningserver.utils import MainScript
from testtools.matchers import (
DirExists,
FileContains,
@@ -41,21 +42,26 @@
class TestInstallPXEBootloader(TestCase):
def test_integration(self):
+ tftproot = self.make_dir()
+ config = {"tftp": {"root": tftproot}}
+ config_fixture = ConfigFixture(config)
+ self.useFixture(config_fixture)
+
loader = self.make_file()
- tftproot = self.make_dir()
arch = factory.make_name('arch')
subarch = factory.make_name('subarch')
action = factory.make_name("action")
- script = ActionScript(action)
+ script = MainScript(action)
script.register(action, provisioningserver.pxe.install_bootloader)
script.execute(
- (action, "--arch", arch, "--subarch", subarch,
- "--loader", loader, "--tftproot", tftproot))
+ ("--config-file", config_fixture.filename, action, "--arch", arch,
+ "--subarch", subarch, "--loader", loader))
self.assertThat(
locate_tftp_path(
- compose_bootloader_path(arch, subarch), tftproot=tftproot),
+ compose_bootloader_path(arch, subarch),
+ tftproot=tftproot),
FileExists())
self.assertThat(loader, Not(FileExists()))
=== modified file 'src/provisioningserver/pxe/tests/test_install_image.py'
--- src/provisioningserver/pxe/tests/test_install_image.py 2012-07-13 16:32:05 +0000
+++ src/provisioningserver/pxe/tests/test_install_image.py 2012-07-23 16:06:48 +0000
@@ -26,7 +26,8 @@
compose_image_path,
locate_tftp_path,
)
-from provisioningserver.utils import ActionScript
+from provisioningserver.testing.config import ConfigFixture
+from provisioningserver.utils import MainScript
from testtools.matchers import (
DirExists,
FileContains,
@@ -48,20 +49,24 @@
class TestInstallPXEImage(TestCase):
def test_integration(self):
+ tftproot = self.make_dir()
+ config = {"tftp": {"root": tftproot}}
+ config_fixture = ConfigFixture(config)
+ self.useFixture(config_fixture)
+
download_dir = self.make_dir()
image_dir = os.path.join(download_dir, 'image')
os.makedirs(image_dir)
factory.make_file(image_dir, 'kernel')
- tftproot = self.make_dir()
arch, subarch, release, purpose = make_arch_subarch_release_purpose()
action = factory.make_name("action")
- script = ActionScript(action)
+ script = MainScript(action)
script.register(action, provisioningserver.pxe.install_image)
script.execute(
- (action, "--arch", arch, "--subarch", subarch, "--release",
- release, "--purpose", purpose, "--image", image_dir,
- "--tftproot", tftproot))
+ ("--config-file", config_fixture.filename, action, "--arch", arch,
+ "--subarch", subarch, "--release", release, "--purpose", purpose,
+ "--image", image_dir))
self.assertThat(
os.path.join(
=== modified file 'src/provisioningserver/pxe/tests/test_pxeconfig.py'
--- src/provisioningserver/pxe/tests/test_pxeconfig.py 2012-07-04 13:07:31 +0000
+++ src/provisioningserver/pxe/tests/test_pxeconfig.py 2012-07-23 16:06:48 +0000
@@ -26,6 +26,7 @@
compose_config_path,
locate_tftp_path,
)
+from provisioningserver.testing.config import ConfigFixture
import tempita
from testtools.matchers import (
Contains,
@@ -37,47 +38,62 @@
class TestPXEConfig(TestCase):
"""Tests for PXEConfig."""
+ def setUp(self):
+ super(TestPXEConfig, self).setUp()
+ self.tftproot = self.make_dir()
+ self.config = {"tftp": {"root": self.tftproot}}
+ self.useFixture(ConfigFixture(self.config))
+
def configure_templates_dir(self, path=None):
"""Configure PXE_TEMPLATES_DIR to `path`."""
self.patch(
provisioningserver.pxe.pxeconfig, 'PXE_TEMPLATES_DIR', path)
def test_init_sets_up_paths(self):
- pxeconfig = PXEConfig("armhf", "armadaxp")
+ pxeconfig = PXEConfig("armhf", "armadaxp", tftproot=self.tftproot)
expected_template = os.path.join(
pxeconfig.template_basedir, 'maas.template')
- expected_target = os.path.dirname(locate_tftp_path(
- compose_config_path('armhf', 'armadaxp', 'default')))
+ expected_target = os.path.dirname(
+ locate_tftp_path(
+ compose_config_path('armhf', 'armadaxp', 'default'),
+ tftproot=self.tftproot))
self.assertEqual(expected_template, pxeconfig.template)
self.assertEqual(
expected_target, os.path.dirname(pxeconfig.target_file))
def test_init_with_no_subarch_makes_path_with_generic(self):
- pxeconfig = PXEConfig("i386")
- expected_target = os.path.dirname(locate_tftp_path(
- compose_config_path('i386', 'generic', 'default')))
+ pxeconfig = PXEConfig("i386", tftproot=self.tftproot)
+ expected_target = os.path.dirname(
+ locate_tftp_path(
+ compose_config_path('i386', 'generic', 'default'),
+ tftproot=self.tftproot))
self.assertEqual(
expected_target, os.path.dirname(pxeconfig.target_file))
def test_init_with_no_mac_sets_default_filename(self):
- pxeconfig = PXEConfig("armhf", "armadaxp")
+ pxeconfig = PXEConfig("armhf", "armadaxp", tftproot=self.tftproot)
expected_filename = locate_tftp_path(
- compose_config_path('armhf', 'armadaxp', 'default'))
+ compose_config_path('armhf', 'armadaxp', 'default'),
+ tftproot=self.tftproot)
self.assertEqual(expected_filename, pxeconfig.target_file)
def test_init_with_dodgy_mac(self):
# !=5 colons is bad.
bad_mac = "aa:bb:cc:dd:ee"
exception = self.assertRaises(
- PXEConfigFail, PXEConfig, "armhf", "armadaxp", bad_mac)
+ PXEConfigFail, PXEConfig, "armhf", "armadaxp", bad_mac,
+ tftproot=self.tftproot)
self.assertEqual(
exception.message, "Expecting exactly five ':' chars, found 4")
def test_init_with_mac_sets_filename(self):
- pxeconfig = PXEConfig("armhf", "armadaxp", mac="00:a1:b2:c3:e4:d5")
+ pxeconfig = PXEConfig(
+ "armhf", "armadaxp", mac="00:a1:b2:c3:e4:d5",
+ tftproot=self.tftproot)
expected_filename = locate_tftp_path(
- compose_config_path('armhf', 'armadaxp', '00-a1-b2-c3-e4-d5'))
+ compose_config_path('armhf', 'armadaxp', '00-a1-b2-c3-e4-d5'),
+ tftproot=self.tftproot)
self.assertEqual(expected_filename, pxeconfig.target_file)
def test_template_basedir_defaults_to_local_dir(self):
@@ -86,7 +102,7 @@
self.assertEqual(
os.path.join(
os.path.dirname(os.path.dirname(__file__)), 'templates'),
- PXEConfig(arch).template_basedir)
+ PXEConfig(arch, tftproot=self.tftproot).template_basedir)
def test_template_basedir_prefers_configured_value(self):
temp_dir = self.make_dir()
@@ -94,11 +110,11 @@
arch = factory.make_name('arch')
self.assertEqual(
temp_dir,
- PXEConfig(arch).template_basedir)
+ PXEConfig(arch, tftproot=self.tftproot).template_basedir)
def test_get_template_retrieves_template(self):
self.configure_templates_dir()
- pxeconfig = PXEConfig("i386")
+ pxeconfig = PXEConfig("i386", tftproot=self.tftproot)
template = pxeconfig.get_template()
self.assertIsInstance(template, tempita.Template)
self.assertThat(pxeconfig.template, FileContains(template.content))
@@ -108,10 +124,12 @@
template = self.make_file(name='maas.template', contents=contents)
self.configure_templates_dir(os.path.dirname(template))
arch = factory.make_name('arch')
- self.assertEqual(contents, PXEConfig(arch).get_template().content)
+ self.assertEqual(
+ contents, PXEConfig(
+ arch, tftproot=self.tftproot).get_template().content)
def test_render_template(self):
- pxeconfig = PXEConfig("i386")
+ pxeconfig = PXEConfig("i386", tftproot=self.tftproot)
template = tempita.Template("template: {{kernelimage}}")
rendered = pxeconfig.render_template(template, kernelimage="myimage")
self.assertEqual("template: myimage", rendered)
@@ -119,7 +137,7 @@
def test_render_template_raises_PXEConfigFail(self):
# If not enough arguments are supplied to fill in template
# variables then a PXEConfigFail is raised.
- pxeconfig = PXEConfig("i386")
+ pxeconfig = PXEConfig("i386", tftproot=self.tftproot)
template_name = factory.getRandomString()
template = tempita.Template(
"template: {{kernelimage}}", name=template_name)
=== modified file 'src/provisioningserver/pxe/tests/test_tftppath.py'
--- src/provisioningserver/pxe/tests/test_tftppath.py 2012-07-06 19:53:41 +0000
+++ src/provisioningserver/pxe/tests/test_tftppath.py 2012-07-23 16:06:48 +0000
@@ -14,7 +14,6 @@
import os.path
-from celeryconfig import TFTPROOT
from maastesting.factory import factory
from maastesting.testcase import TestCase
from provisioningserver.pxe.tftppath import (
@@ -23,6 +22,7 @@
compose_image_path,
locate_tftp_path,
)
+from provisioningserver.testing.config import ConfigFixture
from testtools.matchers import (
Not,
StartsWith,
@@ -31,6 +31,12 @@
class TestTFTPPath(TestCase):
+ def setUp(self):
+ super(TestTFTPPath, self).setUp()
+ self.tftproot = self.make_dir()
+ self.config = {"tftp": {"root": self.tftproot}}
+ self.useFixture(ConfigFixture(self.config))
+
def test_compose_config_path_follows_maas_pxe_directory_layout(self):
arch = factory.make_name('arch')
subarch = factory.make_name('subarch')
@@ -45,7 +51,7 @@
name = factory.make_name('config')
self.assertThat(
compose_config_path(arch, subarch, name),
- Not(StartsWith(TFTPROOT)))
+ Not(StartsWith(self.tftproot)))
def test_compose_image_path_follows_maas_pxe_directory_layout(self):
arch = factory.make_name('arch')
@@ -63,7 +69,7 @@
purpose = factory.make_name('purpose')
self.assertThat(
compose_image_path(arch, subarch, release, purpose),
- Not(StartsWith(TFTPROOT)))
+ Not(StartsWith(self.tftproot)))
def test_compose_bootloader_path_follows_maas_pxe_directory_layout(self):
arch = factory.make_name('arch')
@@ -77,13 +83,13 @@
subarch = factory.make_name('subarch')
self.assertThat(
compose_bootloader_path(arch, subarch),
- Not(StartsWith(TFTPROOT)))
+ Not(StartsWith(self.tftproot)))
def test_locate_tftp_path_prefixes_tftp_root_by_default(self):
pxefile = factory.make_name('pxefile')
self.assertEqual(
- os.path.join(TFTPROOT, pxefile),
- locate_tftp_path(pxefile))
+ os.path.join(self.tftproot, pxefile),
+ locate_tftp_path(pxefile, tftproot=self.tftproot))
def test_locate_tftp_path_overrides_default_tftproot(self):
tftproot = '/%s' % factory.make_name('tftproot')
@@ -93,4 +99,5 @@
locate_tftp_path(pxefile, tftproot=tftproot))
def test_locate_tftp_path_returns_root_by_default(self):
- self.assertEqual(TFTPROOT, locate_tftp_path())
+ self.assertEqual(
+ self.tftproot, locate_tftp_path(tftproot=self.tftproot))
=== modified file 'src/provisioningserver/pxe/tftppath.py'
--- src/provisioningserver/pxe/tftppath.py 2012-07-09 16:03:46 +0000
+++ src/provisioningserver/pxe/tftppath.py 2012-07-23 16:06:48 +0000
@@ -19,8 +19,6 @@
import os.path
-from celeryconfig import TFTPROOT
-
def compose_bootloader_path(arch, subarch):
"""Compose the TFTP path for a PXE pre-boot loader."""
@@ -73,8 +71,7 @@
:param tftproot: Optional TFTP root directory to override the
configured default.
"""
- if tftproot is None:
- tftproot = TFTPROOT
+ assert tftproot is not None, "tftproot must be defined."
if tftp_path is None:
return tftproot
return os.path.join(tftproot, tftp_path.lstrip('/'))
=== added file 'src/provisioningserver/testing/config.py'
--- src/provisioningserver/testing/config.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/testing/config.py 2012-07-23 16:06:48 +0000
@@ -0,0 +1,52 @@
+# Copyright 2005-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the psmaas TAP."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ "ConfigFixture",
+ ]
+
+from os import path
+
+from fixtures import (
+ EnvironmentVariableFixture,
+ Fixture,
+ TempDir,
+ )
+from maastesting.factory import factory
+import yaml
+
+
+class ConfigFixture(Fixture):
+
+ def __init__(self, config=None):
+ super(ConfigFixture, self).__init__()
+ # The smallest config snippet that will validate.
+ self.config = {
+ "password": factory.getRandomString(),
+ }
+ if config is not None:
+ self.config.update(config)
+
+ def setUp(self):
+ super(ConfigFixture, self).setUp()
+ # Create a real configuration file, and populate it.
+ self.dir = self.useFixture(TempDir()).path
+ self.filename = path.join(self.dir, "config.yaml")
+ with open(self.filename, "wb") as stream:
+ yaml.safe_dump(self.config, stream=stream)
+ # Export this filename to the environment, so that subprocesses will
+ # pick up this configuration. Define the new environment as an
+ # instance variable so that users of this fixture can use this to
+ # extend custom subprocess environments.
+ self.environ = {"MAAS_PROVISIONING_SETTINGS": self.filename}
+ for name, value in self.environ.items():
+ self.useFixture(EnvironmentVariableFixture(name, value))
=== added file 'src/provisioningserver/tests/test_config.py'
--- src/provisioningserver/tests/test_config.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/tests/test_config.py 2012-07-23 16:06:48 +0000
@@ -0,0 +1,155 @@
+# Copyright 2005-2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for provisioning configuration."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+from functools import partial
+from getpass import getuser
+import os
+from textwrap import dedent
+
+import formencode
+from maastesting.factory import factory
+from maastesting.testcase import TestCase
+from provisioningserver.config import Config
+from provisioningserver.testing.config import ConfigFixture
+from testtools.matchers import (
+ DirExists,
+ FileExists,
+ MatchesException,
+ Raises,
+ )
+import yaml
+
+
+class TestConfigFixture(TestCase):
+ """Tests for `provisioningserver.testing.config.ConfigFixture`."""
+
+ def exercise_fixture(self, fixture):
+ # ConfigFixture arranges a minimal configuration on disk, and exports
+ # the configuration filename to the environment so that subprocesses
+ # can find it.
+ with fixture:
+ self.assertThat(fixture.dir, DirExists())
+ self.assertThat(fixture.filename, FileExists())
+ self.assertEqual(
+ {"MAAS_PROVISIONING_SETTINGS": fixture.filename},
+ fixture.environ)
+ self.assertEqual(
+ fixture.filename, os.environ["MAAS_PROVISIONING_SETTINGS"])
+ with open(fixture.filename, "rb") as stream:
+ self.assertEqual(fixture.config, yaml.safe_load(stream))
+
+ def test_use_minimal(self):
+ # With no arguments, ConfigFixture arranges a minimal configuration.
+ fixture = ConfigFixture()
+ self.exercise_fixture(fixture)
+
+ def test_use_with_config(self):
+ # Given a configuration, ConfigFixture can arrange a minimal global
+ # configuration with the additional options merged in.
+ dummy_logfile = factory.make_name("logfile")
+ fixture = ConfigFixture({"logfile": dummy_logfile})
+ self.assertEqual(dummy_logfile, fixture.config["logfile"])
+ self.exercise_fixture(fixture)
+
+
+class TestConfig(TestCase):
+ """Tests for `provisioningserver.config.Config`."""
+
+ def test_defaults(self):
+ mandatory = {
+ 'password': 'killing_joke',
+ }
+ expected = {
+ 'broker': {
+ 'host': 'localhost',
+ 'port': 5673,
+ 'username': getuser(),
+ 'password': 'test',
+ 'vhost': '/',
+ },
+ 'cobbler': {
+ 'url': 'http://localhost/cobbler_api',
+ 'username': getuser(),
+ 'password': 'test',
+ },
+ 'logfile': 'pserv.log',
+ 'oops': {
+ 'directory': '',
+ 'reporter': '',
+ },
+ 'tftp': {
+ 'generator': 'http://localhost:5243/api/1.0/pxeconfig',
+ 'port': 5244,
+ 'root': "/var/lib/tftpboot",
+ },
+ 'interface': '127.0.0.1',
+ 'port': 5241,
+ 'username': getuser(),
+ }
+ expected.update(mandatory)
+ observed = Config.to_python(mandatory)
+ self.assertEqual(expected, observed)
+
+ def test_parse(self):
+ # Configuration can be parsed from a snippet of YAML.
+ observed = Config.parse(
+ b'logfile: "/some/where.log"\n'
+ b'password: "black_sabbath"\n'
+ )
+ self.assertEqual("/some/where.log", observed["logfile"])
+
+ def test_load(self):
+ # Configuration can be loaded and parsed from a file.
+ config = dedent("""
+ logfile: "/some/where.log"
+ password: "megadeth"
+ """)
+ filename = self.make_file(name="config.yaml", contents=config)
+ observed = Config.load(filename)
+ self.assertEqual("/some/where.log", observed["logfile"])
+
+ def test_load_example(self):
+ # The example configuration can be loaded and validated.
+ filename = os.path.join(
+ os.path.dirname(__file__), os.pardir,
+ os.pardir, os.pardir, "etc", "pserv.yaml")
+ Config.load(filename)
+
+ def test_load_from_cache(self):
+ # A config loaded by Config.load_from_cache() is never reloaded.
+ filename = self.make_file(
+ name="config.yaml", contents='password: irrelevant')
+ config_before = Config.load_from_cache(filename)
+ os.unlink(filename)
+ config_after = Config.load_from_cache(filename)
+ self.assertIs(config_before, config_after)
+
+ def test_oops_directory_without_reporter(self):
+ # It is an error to omit the OOPS reporter if directory is specified.
+ config = (
+ 'oops:\n'
+ ' directory: /tmp/oops\n'
+ )
+ expected = MatchesException(
+ formencode.Invalid, "oops: You must give a value for reporter")
+ self.assertThat(
+ partial(Config.parse, config),
+ Raises(expected))
+
+ def test_field(self):
+ self.assertIs(Config, Config.field())
+ self.assertIs(Config.fields["tftp"], Config.field("tftp"))
+ self.assertIs(
+ Config.fields["tftp"].fields["root"],
+ Config.field("tftp", "root"))
=== modified file 'src/provisioningserver/tests/test_maas_import_pxe_files.py'
--- src/provisioningserver/tests/test_maas_import_pxe_files.py 2012-07-13 22:42:28 +0000
+++ src/provisioningserver/tests/test_maas_import_pxe_files.py 2012-07-23 16:06:48 +0000
@@ -21,6 +21,7 @@
age_file,
get_write_time,
)
+from provisioningserver.testing.config import ConfigFixture
from testtools.matchers import (
Contains,
FileContains,
@@ -67,6 +68,13 @@
class TestImportPXEFiles(TestCase):
+ def setUp(self):
+ super(TestImportPXEFiles, self).setUp()
+ self.tftproot = self.make_dir()
+ self.config = {"tftp": {"root": self.tftproot}}
+ self.config_fixture = ConfigFixture(self.config)
+ self.useFixture(self.config_fixture)
+
def make_downloads(self, release=None, arch=None):
"""Set up a directory with an image for "download" by the script.
@@ -102,8 +110,11 @@
# Substitute curl for wget; it accepts file:// URLs.
'DOWNLOAD': 'curl -O --silent',
'PATH': os.pathsep.join(path),
- 'TFTPROOT': tftproot,
+ # TODO: Remove TFTPROOT; it's here to support the obsolete
+ # generate_enlistment_pxe command.
+ 'TFTPROOT': self.tftproot,
}
+ env.update(self.config_fixture.environ)
if arch is not None:
env['ARCHES'] = arch
if release is not None:
@@ -116,9 +127,8 @@
arch = factory.make_name('arch')
release = 'precise'
archive = self.make_downloads(arch=arch, release=release)
- tftproot = self.make_dir()
- self.call_script(archive, tftproot, arch=arch, release=release)
- tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
+ tftp_path = compose_tftp_path(self.tftproot, arch, 'pxelinux.0')
download_path = compose_download_dir(archive, arch, release)
expected_contents = read_file(download_path, 'pxelinux.0')
self.assertThat(tftp_path, FileContains(expected_contents))
@@ -129,21 +139,19 @@
archive = self.make_downloads(arch=arch, release=release)
download_path = compose_download_dir(archive, arch, release)
os.remove(os.path.join(download_path, 'pxelinux.0'))
- tftproot = self.make_dir()
- self.call_script(archive, tftproot, arch=arch, release=release)
- tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
+ tftp_path = compose_tftp_path(self.tftproot, arch, 'pxelinux.0')
self.assertThat(tftp_path, Not(FileExists()))
def test_updates_pre_boot_loader(self):
arch = factory.make_name('arch')
release = 'precise'
- tftproot = self.make_dir()
- tftp_path = compose_tftp_path(tftproot, arch, 'pxelinux.0')
+ tftp_path = compose_tftp_path(self.tftproot, arch, 'pxelinux.0')
os.makedirs(os.path.dirname(tftp_path))
with open(tftp_path, 'w') as existing_file:
existing_file.write(factory.getRandomString())
archive = self.make_downloads(arch=arch, release=release)
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
download_path = compose_download_dir(archive, arch, release)
expected_contents = read_file(download_path, 'pxelinux.0')
self.assertThat(tftp_path, FileContains(expected_contents))
@@ -152,10 +160,9 @@
arch = factory.make_name('arch')
release = 'precise'
archive = self.make_downloads(arch=arch, release=release)
- tftproot = self.make_dir()
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
tftp_path = compose_tftp_path(
- tftproot, arch, release, 'install', 'linux')
+ self.tftproot, arch, release, 'install', 'linux')
download_path = compose_download_dir(archive, arch, release)
expected_contents = read_file(download_path, 'linux')
self.assertThat(tftp_path, FileContains(expected_contents))
@@ -163,14 +170,13 @@
def test_updates_install_image(self):
arch = factory.make_name('arch')
release = 'precise'
- tftproot = self.make_dir()
tftp_path = compose_tftp_path(
- tftproot, arch, release, 'install', 'linux')
+ self.tftproot, arch, release, 'install', 'linux')
os.makedirs(os.path.dirname(tftp_path))
with open(tftp_path, 'w') as existing_file:
existing_file.write(factory.getRandomString())
archive = self.make_downloads(arch=arch, release=release)
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
download_path = compose_download_dir(archive, arch, release)
expected_contents = read_file(download_path, 'linux')
self.assertThat(tftp_path, FileContains(expected_contents))
@@ -179,22 +185,21 @@
arch = factory.make_name('arch')
release = 'precise'
archive = self.make_downloads(arch=arch, release=release)
- tftproot = self.make_dir()
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
tftp_path = compose_tftp_path(
- tftproot, arch, release, 'install', 'linux')
+ self.tftproot, arch, release, 'install', 'linux')
backdate(tftp_path)
original_timestamp = get_write_time(tftp_path)
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
self.assertEqual(original_timestamp, get_write_time(tftp_path))
def test_generates_default_pxe_config(self):
arch = factory.make_name('arch')
release = 'precise'
- tftproot = self.make_dir()
archive = self.make_downloads(arch=arch, release=release)
- self.call_script(archive, tftproot, arch=arch, release=release)
+ self.call_script(archive, self.tftproot, arch=arch, release=release)
self.assertThat(
os.path.join(
- tftproot, 'maas', arch, 'generic', 'pxelinux.cfg', 'default'),
+ self.tftproot, 'maas', arch, 'generic',
+ 'pxelinux.cfg', 'default'),
FileContains(matcher=Contains("MENU TITLE")))
=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py 2012-07-06 19:53:41 +0000
+++ src/provisioningserver/tests/test_plugin.py 2012-07-23 16:06:48 +0000
@@ -14,24 +14,19 @@
from base64 import b64encode
from functools import partial
-from getpass import getuser
import httplib
import os
from StringIO import StringIO
-from textwrap import dedent
import xmlrpclib
-import formencode
from maastesting.factory import factory
from maastesting.testcase import TestCase
from provisioningserver.plugin import (
- Config,
Options,
ProvisioningRealm,
ProvisioningServiceMaker,
SingleUsernamePasswordChecker,
)
-from provisioningserver.pxe.tftppath import locate_tftp_path
from provisioningserver.testing.fakecobbler import make_fake_cobbler_session
from provisioningserver.tftp import TFTPBackend
from testtools.deferredruntest import (
@@ -59,82 +54,6 @@
import yaml
-class TestConfig(TestCase):
- """Tests for `provisioningserver.plugin.Config`."""
-
- def test_defaults(self):
- mandatory = {
- 'password': 'killing_joke',
- }
- expected = {
- 'broker': {
- 'host': 'localhost',
- 'port': 5673,
- 'username': getuser(),
- 'password': 'test',
- 'vhost': '/',
- },
- 'cobbler': {
- 'url': 'http://localhost/cobbler_api',
- 'username': getuser(),
- 'password': 'test',
- },
- 'logfile': 'pserv.log',
- 'oops': {
- 'directory': '',
- 'reporter': '',
- },
- 'tftp': {
- 'generator': 'http://localhost:5243/api/1.0/pxeconfig',
- 'port': 5244,
- 'root': locate_tftp_path(),
- },
- 'interface': '127.0.0.1',
- 'port': 5241,
- 'username': getuser(),
- }
- expected.update(mandatory)
- observed = Config.to_python(mandatory)
- self.assertEqual(expected, observed)
-
- def test_parse(self):
- # Configuration can be parsed from a snippet of YAML.
- observed = Config.parse(
- b'logfile: "/some/where.log"\n'
- b'password: "black_sabbath"\n'
- )
- self.assertEqual("/some/where.log", observed["logfile"])
-
- def test_load(self):
- # Configuration can be loaded and parsed from a file.
- config = dedent("""
- logfile: "/some/where.log"
- password: "megadeth"
- """)
- filename = self.make_file(name="config.yaml", contents=config)
- observed = Config.load(filename)
- self.assertEqual("/some/where.log", observed["logfile"])
-
- def test_load_example(self):
- # The example configuration can be loaded and validated.
- filename = os.path.join(
- os.path.dirname(__file__), os.pardir,
- os.pardir, os.pardir, "etc", "pserv.yaml")
- Config.load(filename)
-
- def test_oops_directory_without_reporter(self):
- # It is an error to omit the OOPS reporter if directory is specified.
- config = (
- 'oops:\n'
- ' directory: /tmp/oops\n'
- )
- expected = MatchesException(
- formencode.Invalid, "oops: You must give a value for reporter")
- self.assertThat(
- partial(Config.parse, config),
- Raises(expected))
-
-
class TestOptions(TestCase):
"""Tests for `provisioningserver.plugin.Options`."""
=== modified file 'src/provisioningserver/tests/test_utils.py'
--- src/provisioningserver/tests/test_utils.py 2012-07-18 10:06:55 +0000
+++ src/provisioningserver/tests/test_utils.py 2012-07-23 16:06:48 +0000
@@ -16,10 +16,10 @@
ArgumentParser,
Namespace,
)
-from io import BytesIO
import os
import random
from random import randint
+import StringIO
from subprocess import CalledProcessError
import sys
import types
@@ -31,6 +31,7 @@
atomic_write,
increment_age,
incremental_write,
+ MainScript,
Safe,
ShellTemplate,
)
@@ -132,17 +133,21 @@
class TestActionScript(TestCase):
"""Test `ActionScript`."""
+ factory = ActionScript
+
def setUp(self):
super(TestActionScript, self).setUp()
# ActionScript.setup() is not safe to run in the test suite.
self.patch(ActionScript, "setup", lambda self: None)
- # ArgumentParser sometimes likes to print to stdout/err.
- self.patch(sys, "stdout", BytesIO())
- self.patch(sys, "stderr", BytesIO())
+ # ArgumentParser sometimes likes to print to stdout/err. Use
+ # StringIO.StringIO to be relaxed about str/unicode (argparse uses
+ # str). When moving to Python 3 this will need to be tightened up.
+ self.patch(sys, "stdout", StringIO.StringIO())
+ self.patch(sys, "stderr", StringIO.StringIO())
def test_init(self):
description = factory.getRandomString()
- script = ActionScript(description)
+ script = self.factory(description)
self.assertIsInstance(script.parser, ArgumentParser)
self.assertEqual(description, script.parser.description)
@@ -152,7 +157,7 @@
self.assertIsInstance(parser, ArgumentParser))
handler.run = lambda args: (
self.assertIsInstance(args, int))
- script = ActionScript("Description")
+ script = self.factory("Description")
script.register("slay", handler)
self.assertIn("slay", script.subparsers.choices)
action_parser = script.subparsers.choices["slay"]
@@ -163,7 +168,7 @@
# add_arguments() callable.
handler = types.ModuleType(b"handler")
handler.run = lambda args: None
- script = ActionScript("Description")
+ script = self.factory("Description")
error = self.assertRaises(
AttributeError, script.register, "decapitate", handler)
self.assertIn("'add_arguments'", "%s" % error)
@@ -173,7 +178,7 @@
# callable.
handler = types.ModuleType(b"handler")
handler.add_arguments = lambda parser: None
- script = ActionScript("Description")
+ script = self.factory("Description")
error = self.assertRaises(
AttributeError, script.register, "decapitate", handler)
self.assertIn("'run'", "%s" % error)
@@ -183,7 +188,7 @@
handler = types.ModuleType(b"handler")
handler.add_arguments = lambda parser: None
handler.run = handler_calls.append
- script = ActionScript("Description")
+ script = self.factory("Description")
script.register("amputate", handler)
error = self.assertRaises(SystemExit, script, ["amputate"])
self.assertEqual(0, error.code)
@@ -191,7 +196,7 @@
self.assertIsInstance(handler_calls[0], Namespace)
def test_call_invalid_choice(self):
- script = ActionScript("Description")
+ script = self.factory("Description")
self.assertRaises(SystemExit, script, ["disembowel"])
self.assertIn(b"invalid choice", sys.stderr.getvalue())
@@ -200,7 +205,7 @@
handler = types.ModuleType(b"handler")
handler.add_arguments = lambda parser: None
handler.run = lambda args: 0 / 0
- script = ActionScript("Description")
+ script = self.factory("Description")
script.register("eviscerate", handler)
self.assertRaises(ZeroDivisionError, script, ["eviscerate"])
@@ -216,7 +221,7 @@
handler = types.ModuleType(b"handler")
handler.add_arguments = lambda parser: None
handler.run = lambda args: raise_exception()
- script = ActionScript("Description")
+ script = self.factory("Description")
script.register("sever", handler)
error = self.assertRaises(SystemExit, script, ["sever"])
self.assertEqual(exception.returncode, error.code)
@@ -231,7 +236,31 @@
handler = types.ModuleType(b"handler")
handler.add_arguments = lambda parser: None
handler.run = lambda args: raise_exception()
- script = ActionScript("Description")
+ script = self.factory("Description")
script.register("smash", handler)
error = self.assertRaises(SystemExit, script, ["smash"])
self.assertEqual(1, error.code)
+
+
+class TestMainScript(TestActionScript):
+
+ factory = MainScript
+
+ def test_default_arguments(self):
+ # MainScript accepts a --config-file parameter. The value of this is
+ # passed through into the args namespace object as config_file.
+ handler_calls = []
+ handler = types.ModuleType(b"handler")
+ handler.add_arguments = lambda parser: None
+ handler.run = handler_calls.append
+ script = self.factory("Description")
+ script.register("dislocate", handler)
+ dummy_config_file = factory.make_name("config-file")
+ # --config-file is specified before the action.
+ args = ["--config-file", dummy_config_file, "dislocate"]
+ error = self.assertRaises(SystemExit, script, args)
+ self.assertEqual(0, error.code)
+ namespace = handler_calls[0]
+ self.assertEqual(
+ {"config_file": dummy_config_file, "handler": handler},
+ vars(namespace))
=== modified file 'src/provisioningserver/utils.py'
--- src/provisioningserver/utils.py 2012-07-18 10:06:55 +0000
+++ src/provisioningserver/utils.py 2012-07-23 16:06:48 +0000
@@ -14,15 +14,19 @@
"ActionScript",
"atomic_write",
"deferred",
+ "incremental_write",
+ "MainScript",
"ShellTemplate",
- "incremental_write",
"xmlrpc_export",
]
from argparse import ArgumentParser
from functools import wraps
import os
-from os import fdopen
+from os import (
+ fdopen,
+ environ,
+ )
from pipes import quote
import signal
from subprocess import CalledProcessError
@@ -239,3 +243,21 @@
raise SystemExit(1)
else:
raise SystemExit(0)
+
+
+class MainScript(ActionScript):
+ """An `ActionScript` that always accepts a `--config-file` option.
+
+ The `--config-file` option defaults to the value of
+ `MAAS_PROVISIONING_SETTINGS` in the process's environment, otherwise
+ `/etc/maas/pserv.yaml`.
+ """
+
+ def __init__(self, description):
+ super(MainScript, self).__init__(description)
+ self.parser.add_argument(
+ "-c", "--config-file", metavar="FILENAME",
+ help="Configuration file to load [%(default)s].",
+ default=environ.get(
+ "MAAS_PROVISIONING_SETTINGS",
+ "/etc/maas/pserv.yaml"))
=== modified file 'templates/test_module.py'
--- templates/test_module.py 2012-07-20 02:35:14 +0000
+++ templates/test_module.py 2012-07-23 16:06:48 +0000
@@ -13,7 +13,7 @@
__metaclass__ = type
__all__ = []
-from maasserver.testing.testcase import TestCase
+from maastesting.testcase import TestCase
class TestSomething(TestCase):