← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/remove-obsolete-generate-enlistment-pxe into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/remove-obsolete-generate-enlistment-pxe into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/remove-obsolete-generate-enlistment-pxe/+merge/116362

Removes the obsolete generate_enlistment_pxe command. This has been replaced by dynamic PXE generation. I think it's safe to remove this, because it was part of the Cobbler replacement work (and won't, therefore, break the existing Cobbler integration).
-- 
https://code.launchpad.net/~allenap/maas/remove-obsolete-generate-enlistment-pxe/+merge/116362
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/remove-obsolete-generate-enlistment-pxe into lp:maas.
=== modified file 'etc/celeryconfig.py'
--- etc/celeryconfig.py	2012-07-17 09:51:38 +0000
+++ etc/celeryconfig.py	2012-07-23 20:00:30 +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 20:00:30 +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 20:00:30 +0000
@@ -34,8 +34,8 @@
 # Supported architectures.
 ARCHES=${ARCHES:-amd64 i386}
 
-# TFTP root directory.  (Don't let the "root" vs. "boot" confuse you.)
-TFTPROOT=${TFTPROOT:-/var/lib/tftpboot}
+# 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 +70,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 +93,7 @@
 
     maas-provision install-pxe-image \
         --arch=$arch --release=$release --purpose="install" \
-        --image="install" --tftproot=$TFTPROOT
+        --image="install"
 }
 
 
@@ -116,11 +116,6 @@
         do
             update_install_files $arch $release
         done
-
-        # TODO: Pass sub-architecture once we support those.
-        maas generate_enlistment_pxe \
-            --arch=$arch --release=$CURRENT_RELEASE \
-            --tftproot=$TFTPROOT
     done
 
     rm -rf -- $DOWNLOAD_DIR

=== modified file 'src/maas/development.py'
--- src/maas/development.py	2012-07-11 09:06:57 +0000
+++ src/maas/development.py	2012-07-23 20:00:30 +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 20:00:30 +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-23 17:40:50 +0000
+++ src/maasserver/api.py	2012-07-23 20:00:30 +0000
@@ -133,6 +133,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,
@@ -1084,18 +1085,33 @@
     :param kernelimage: The path to the kernel in the TFTP server
     :param append: Kernel parameters to append.
     """
+<<<<<<< TREE
     menutitle = get_mandatory_param(request.GET, 'menutitle')
     kernelimage = get_mandatory_param(request.GET, 'kernelimage')
     append = get_mandatory_param(request.GET, 'append')
+=======
+    provisioning_config = (
+        provisioningserver.config.Config.load_from_cache(
+            settings.PROVISIONING_SETTINGS))
+>>>>>>> MERGE-SOURCE
     arch = get_mandatory_param(request.GET, 'arch')
     subarch = request.GET.get('subarch', None)
     mac = request.GET.get('mac', None)
+<<<<<<< TREE
     config = PXEConfig(arch, subarch, mac)
 
     # In addition to the "append" parameter, also add a URL for the
     # node's preseed to the kernel command line.
     append = "%s %s" % (append, compose_preseed_kernel_opt(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')
+    append = get_mandatory_param(request.GET, 'append')
+>>>>>>> MERGE-SOURCE
     try:
         return HttpResponse(
             config.get_config(

=== removed 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	1970-01-01 00:00:00 +0000
@@ -1,74 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Django command: generate a PXE configuration file for node enlistment.
-
-Produces the "default" PXE configuration that we provide to nodes that
-MAAS is not yet aware of.  A node that netboots using this configuration
-will then register itself with the MAAS.
-"""
-
-from __future__ import (
-    absolute_import,
-    print_function,
-    unicode_literals,
-    )
-
-__metaclass__ = type
-__all__ = [
-    'Command',
-    ]
-
-
-from optparse import make_option
-
-from django.core.management.base import BaseCommand
-from provisioningserver.pxe.pxeconfig import PXEConfig
-from provisioningserver.pxe.tftppath import compose_image_path
-
-
-class Command(BaseCommand):
-    """Print out enlistment PXE config."""
-
-    option_list = BaseCommand.option_list + (
-        make_option(
-            '--arch', dest='arch', default=None,
-            help="Main system architecture to generate config for."),
-        make_option(
-            '--subarch', dest='subarch', default='generic',
-            help="Sub-architecture of the main architecture."),
-        make_option(
-            '--release', dest='release', default=None,
-            help="Ubuntu release to run when enlisting nodes."),
-        make_option(
-            '--tftproot', dest='tftproot', default=None,
-            help="Root of TFTP directory hierarchy to place config into."),
-        )
-
-    def handle(self, arch=None, subarch='generic', release=None,
-               tftproot=None, **kwargs):
-        image_path = compose_image_path(arch, subarch, release, 'install')
-        # TODO: This needs to go somewhere more appropriate, and
-        # probably contain more appropriate options.
-        kernel_opts = ' '.join([
-            # Default kernel options (similar to those used by Cobbler):
-            'initrd=%s' % '/'.join([image_path, 'initrd.gz']),
-            'ksdevice=bootif',
-            'lang=  text ',
-            'hostname=%s-%s' % (release, arch),
-            'domain=local.lan',
-            'suite=%s' % release,
-
-            # MAAS-specific options:
-            'priority=critical',
-            'local=en_US',
-            'netcfg/choose_interface=auto',
-            ])
-        template_args = {
-            'menutitle': "Enlisting with MAAS",
-            # Enlistment uses the same kernel as installation.
-            'kernelimage': '/'.join([image_path, 'linux']),
-            'append': kernel_opts,
-        }
-        writer = PXEConfig(arch, subarch, tftproot=tftproot)
-        writer.write_config(**template_args)

=== removed file 'src/maasserver/tests/test_commands_generate_enlistment_pxe.py'
--- src/maasserver/tests/test_commands_generate_enlistment_pxe.py	2012-06-26 07:27:06 +0000
+++ src/maasserver/tests/test_commands_generate_enlistment_pxe.py	1970-01-01 00:00:00 +0000
@@ -1,52 +0,0 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for the generate-enlistment-pxe command."""
-
-from __future__ import (
-    absolute_import,
-    print_function,
-    unicode_literals,
-    )
-
-__metaclass__ = type
-__all__ = []
-
-from django.core.management import call_command
-from maasserver.enum import ARCHITECTURE_CHOICES
-from maasserver.testing.factory import factory
-from maasserver.testing.testcase import TestCase
-from provisioningserver.pxe.pxeconfig import PXEConfig
-from provisioningserver.pxe.tftppath import (
-    compose_config_path,
-    compose_image_path,
-    locate_tftp_path,
-    )
-from testtools.matchers import (
-    Contains,
-    FileContains,
-    )
-
-
-class TestGenerateEnlistmentPXE(TestCase):
-
-    def test_generates_default_pxe_config(self):
-        arch = factory.getRandomChoice(ARCHITECTURE_CHOICES)
-        subarch = 'generic'
-        release = 'precise'
-        tftproot = self.make_dir()
-        self.patch(PXEConfig, 'target_basedir', tftproot)
-        call_command(
-            'generate_enlistment_pxe', arch=arch, release=release,
-            tftproot=tftproot)
-        # This produces a "default" PXE config file in the right place.
-        # It refers to the kernel and initrd for the requested
-        # architecture and release.
-        result_path = locate_tftp_path(
-            compose_config_path(arch, subarch, 'default'),
-            tftproot=tftproot)
-        self.assertThat(
-            result_path,
-            FileContains(matcher=Contains(
-                compose_image_path(arch, subarch, release, 'install') +
-                    '/linux')))

=== modified file 'src/provisioningserver/__main__.py'
--- src/provisioningserver/__main__.py	2012-07-13 16:32:05 +0000
+++ src/provisioningserver/__main__.py	2012-07-23 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +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,8 @@
             # Substitute curl for wget; it accepts file:// URLs.
             'DOWNLOAD': 'curl -O --silent',
             'PATH': os.pathsep.join(path),
-            'TFTPROOT': tftproot,
         }
+        env.update(self.config_fixture.environ)
         if arch is not None:
             env['ARCHES'] = arch
         if release is not None:
@@ -116,9 +124,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 +136,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 +157,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 +167,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 +182,10 @@
         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.assertThat(
-            os.path.join(
-                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 20:00:30 +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 20:00:30 +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 20:00:30 +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 20:00:30 +0000
@@ -13,7 +13,7 @@
 __metaclass__ = type
 __all__ = []
 
-from maasserver.testing.testcase import TestCase
+from maastesting.testcase import TestCase
 
 
 class TestSomething(TestCase):