ソースを参照

One provisioner, and the recipes where it keeps them

trog-provisioner absorbed the generator: bin/provision runs new_config's main()
itself before it builds, because once which hypervisor a guest lands on became a
choice the two halves had to agree on one, and the only honest way to agree is to
be one program.  The recipes went the other way, out of the checkout and into
/etc/trog-provisioner, since they describe an installation rather than the
software.

So there is one path left to configure.  provisioner.provisioners is gone, the
lifecycle is a single command run from the checkout, and the recipe directory is
worked out by trog-provisioner's own rule -- its config dir, or
TROG_PROVISIONER_CONFIG -- rather than being told to tCMS a second time where it
could disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADwKSaBSvUTdA3RtGQeNau
George Baugh 1 ヶ月 前
親
コミット
e77703c6c6

+ 10 - 9
Readme.md

@@ -43,9 +43,9 @@ or hack on tCMS itself.
 Production Deployment
 ====================
 
-See trog-provisioner & related provisioners repository.
+See the trog-provisioner repository.
 
-In the latter you will be interested particularly in:
+You will be interested particularly in:
 
 - Provisioner::Recipe::tcms
 - Provisioner::Recipe::tpsgi
@@ -161,16 +161,17 @@ Sometimes you want to consider something else authoritative that isn't a datamod
 ProvisionedVirt is a subclass of Virt, and shows what subclassing one is for: it
 inherits the libvirt half unchanged and adds the other half of the story, which is
 that a guest on a Troglodyne hypervisor was provisioned from a recipe on disk.
-Tell it where the two repositories are and it shows each guest's
-recipes.d/$domain.yaml and offers a Reprovision button running the full
-lifecycle -- bin/new_config in provisioners, then bin/provision in
-trog-provisioner:
+Tell it where trog-provisioner is checked out and it shows each guest's
+recipes.d/$domain.yaml and offers a Reprovision button running bin/provision,
+which generates the guest's configuration from that recipe and then builds it:
 
     [provisioner]
-        provisioners     = /home/you/Code/provisioners
         trog_provisioner = /home/you/Code/trog-provisioner
 
-Leave those out and it degrades to a plain guest listing.
+Leave that out and it degrades to a plain guest listing.  The recipes themselves
+are not in the checkout -- they describe an installation rather than the software,
+so trog-provisioner keeps them in /etc/trog-provisioner, or wherever
+TROG_PROVISIONER_CONFIG says, and we read them from the same place.
 
 Reprovisioning is not recoverable: it destroys the guest and builds it again, and
 whatever the recipe does not describe does not come back.  It is POST and admin
@@ -190,7 +191,7 @@ run whose status still says 'running' but whose process is gone reads as
 interrupted rather than as going on forever -- that is a guess from the only
 evidence there is, and it is the honest one.
 
-bin/new_config asks for your KeePass passphrase on stdin, because every recipe
+bin/provision asks for your KeePass passphrase on stdin, because every recipe
 inherits secret: values from _base.  The form asks you for it per run and pipes it
 to that one process; it is not written to the config, the log, or the process
 table.  The alternative would be keeping the master password for every secret you

+ 5 - 4
config/default.cfg

@@ -12,11 +12,12 @@
     # this one.
     hostname=
 [provisioner]
-    # Where the two repositories which own the VM provisioning lifecycle are
+    # Where trog-provisioner, which owns the VM provisioning lifecycle, is
     # checked out.  Only needed by Trog::DataSource::ProvisionedVirt, which
-    # degrades to a plain guest listing without them.
-    # Example: /home/you/Code/provisioners
-    provisioners=
+    # degrades to a plain guest listing without it.  The recipes it builds from
+    # are not here: they are in /etc/trog-provisioner, or wherever
+    # TROG_PROVISIONER_CONFIG says, because that is where the provisioner keeps
+    # them.
     # Example: /home/you/Code/trog-provisioner
     trog_provisioner=
 [security]

+ 102 - 97
lib/Trog/DataSource/ProvisionedVirt.pm

@@ -29,19 +29,23 @@ cached.  What it adds is the other half of the story: a guest on a Troglodyne
 hypervisor was provisioned from a recipe, and the recipe is on disk next to the
 hypervisor rather than anywhere libvirt can tell you about.
 
-Two repositories between them own that lifecycle:
+One repository owns that lifecycle.  trog-provisioner's bin/provision generates
+a guest's configuration from its recipe and then builds the machine out of it.
+The generator used to be a second repository and a second run; the two had to
+become one program once which hypervisor a guest lands on became a choice rather
+than whichever machine you happened to be sat at.
 
-    provisioners        recipes.d/$domain.yaml, and bin/new_config which turns
-                        a recipe into a configuration package
-    trog-provisioner    bin/provision, which turns that package into a machine
-
-Say where they are in the tCMS configuration and this becomes useful; leave them
-out and it degrades to plain Virt, which is what it is.
+Say where that checkout is and this becomes useful; leave it out and it degrades
+to plain Virt, which is what it is.
 
     [provisioner]
-        provisioners     = /home/you/Code/provisioners
         trog_provisioner = /home/you/Code/trog-provisioner
 
+The recipes are not in the checkout.  They describe an installation rather than
+the software, so trog-provisioner keeps them in /etc/trog-provisioner, or
+wherever TROG_PROVISIONER_CONFIG says instead.  We find them by that same rule,
+which is one fewer thing to tell tCMS and one fewer thing to disagree about.
+
 =head1 A NOTE ON SAFETY
 
 Reprovisioning is not a recoverable operation.  It destroys the guest and builds
@@ -58,6 +62,10 @@ shell, so a guest name is an argument and can never be a command.
 our $recipe_dir = 'recipes.d';
 our $log_dir    = 'logs/reprovision';
 
+# Where an installation's own files live, which is trog-provisioner's rule
+# rather than ours -- see its Trog::Config.
+our $provisioner_config = '/etc/trog-provisioner';
+
 # What a reprovision can be, in the order it can be it.
 our %states = (
     running     => 'reprovisioning',
@@ -117,11 +125,11 @@ sub _with_recipe ( $post, $config ) {
         }
     }
 
-    # Both halves have to be there to run the lifecycle, and we will not rebuild
-    # the machine we are answering the request from -- see act() in the parent,
+    # There has to be something to run it with, and we will not rebuild the
+    # machine we are answering the request from -- see act() in the parent,
     # which refuses to power that one off for the same reason.
     $post->{can_reprovision} =
-      ( $post->{has_recipe} && $config->{provisioners} && $config->{trog_provisioner} && !$post->{is_self} ) ? 1 : 0;
+      ( $post->{has_recipe} && $config->{trog_provisioner} && !$post->{is_self} ) ? 1 : 0;
 
     my $status = status($domain);
     if ($status) {
@@ -216,33 +224,38 @@ sub _write_status ( $domain, %fields ) {
 
 =head2 _config() = HASHREF
 
-Where the two repositories are, from the tCMS configuration.
+Where the provisioner is checked out, from the tCMS configuration, and where the
+recipes it reads are, from trog-provisioner's own rule for that.
+
+    trog_provisioner    the checkout, or undef
+    recipes             the directory the guests' recipes are in
 
-Undef for either means the feature is off rather than broken: a site which has
-not configured them gets a plain guest listing, which is what it asked for.
+Undef for the checkout means the feature is off rather than broken: a site which
+has not configured it gets a plain guest listing, which is what it asked for.
 
 =cut
 
 sub _config {
     my $conf = Trog::Config::get();
 
-    my %config;
-    foreach my $key (qw{provisioners trog_provisioner}) {
-        my $dir = $conf->param("provisioner.$key");
-        next unless $dir && !ref $dir;
-
-        # Resolved, and checked to be there.  A path in a config file which does
-        # not exist is a typo, and finding that out here beats finding it out
-        # halfway through a reprovision.
-        my $absolute = Cwd::abs_path($dir);
-        if ( !$absolute || !-d $absolute ) {
-            WARN("provisioner.$key is set to '$dir', which is not a directory here");
-            next;
-        }
-
-        $config{$key} = $absolute;
+    # Nothing to configure here: the provisioner reads this env var or that
+    # directory, so a recipe we showed from anywhere else would be a recipe it
+    # is not going to build from.
+    my %config = ( recipes => ( $ENV{TROG_PROVISIONER_CONFIG} // $provisioner_config ) . "/$recipe_dir" );
+
+    my $dir = $conf->param('provisioner.trog_provisioner');
+    return \%config unless $dir && !ref $dir;
+
+    # Resolved, and checked to be there.  A path in a config file which does not
+    # exist is a typo, and finding that out here beats finding it out halfway
+    # through a reprovision.
+    my $absolute = Cwd::abs_path($dir);
+    if ( !$absolute || !-d $absolute ) {
+        WARN("provisioner.trog_provisioner is set to '$dir', which is not a directory here");
+        return \%config;
     }
 
+    $config{trog_provisioner} = $absolute;
     return \%config;
 }
 
@@ -255,13 +268,15 @@ sub _safe_domain ($domain) {
     return $safe;
 }
 
+# An installation's recipes are where they are whether tCMS knows about the
+# provisioner or not, but reading them is the feature, so it is off with it.
 sub _recipe_path ( $config, $domain ) {
-    return undef unless $config->{provisioners};
+    return undef unless $config->{trog_provisioner};
 
     my $safe = _safe_domain($domain);
     return undef unless $safe;
 
-    my $path = "$config->{provisioners}/$recipe_dir/$safe.yaml";
+    my $path = "$config->{recipes}/$safe.yaml";
     return -f $path ? $path : undef;    ## no critic (ProhibitFiletest_f) -- is there a recipe for this guest
 }
 
@@ -319,11 +334,11 @@ sub log_for ($domain) {
 
 =head2 reprovision(%args) = ($ok, $message)
 
-Run the full lifecycle for one guest: bin/new_config in provisioners, and then
-bin/provision in trog-provisioner.
+Run bin/provision for one guest, which generates its configuration from its
+recipe and then builds the machine.
 
     domain      which guest.  Required, and validated as a hostname.
-    passphrase  the KeePass passphrase new_config asks for on stdin.
+    passphrase  the KeePass passphrase the recipe's secrets need, on stdin.
     user        who asked, for the log.
 
 Returns immediately.  Provisioning a machine takes minutes and an HTTP worker
@@ -332,10 +347,10 @@ caller is told it started rather than told it finished.  What happened is in
 logs/reprovision/$domain.log, which is what _log_tail reads back onto the page.
 
 The passphrase is passed down the child's stdin and is never written anywhere:
-not to the log, not to the process table, not to the configuration.  new_config
-prompts for it because every recipe inherits secret: values from _base, and the
-alternative to asking each time is keeping the master password for every secret
-the operator holds in a file the webserver can read.
+not to the log, not to the process table, not to the configuration.  It is asked
+for because every recipe inherits secret: values from _base, and the alternative
+to asking each time is keeping the master password for every secret the operator
+holds in a file the webserver can read.
 
 =cut
 
@@ -352,53 +367,45 @@ sub reprovision (%args) {
     }
 
     my $config = _config();
-    return ( 0, 'provisioner.provisioners is not configured' )     unless $config->{provisioners};
     return ( 0, 'provisioner.trog_provisioner is not configured' ) unless $config->{trog_provisioner};
 
     my $recipe = _recipe_path( $config, $safe );
-    return ( 0, "there is no recipe for '$safe' in $recipe_dir" ) unless $recipe;
+    return ( 0, "there is no recipe for '$safe' in $config->{recipes}" ) unless $recipe;
 
-    return ( 0, 'a passphrase is required, as new_config asks for one' ) unless length( $args{passphrase} // '' );
+    return ( 0, 'a passphrase is required, as the provisioner asks for one' ) unless length( $args{passphrase} // '' );
 
     # One at a time.  Two provisions of one machine racing each other is not
     # something anybody should be able to start by double clicking.
     my $running = status($safe);
     return ( 0, 'a reprovision of this guest is already running' ) if $running && $running->{state} eq 'running';
 
-    my @lifecycle = (
-        [ $config->{provisioners},     "$config->{provisioners}/bin/new_config",    $safe ],
-        [ $config->{trog_provisioner}, "$config->{trog_provisioner}/bin/provision", $safe ],
-    );
-
-    foreach my $step (@lifecycle) {
-        my ( undef, $program ) = @$step;
-        return ( 0, "$program is not there to run" ) unless -x $program;    ## no critic (ProhibitFiletest_rwxRWX) -- refusing early beats finding out mid-lifecycle
-    }
+    my @command = ( "$config->{trog_provisioner}/bin/provision", $safe );
+    return ( 0, "$command[0] is not there to run" ) unless -x $command[0];    ## no critic (ProhibitFiletest_rwxRWX) -- refusing early beats finding out mid-provision
 
     INFO( "Reprovision of '$safe' requested by " . ( $args{user} // 'somebody' ) );
 
-    my ( $ok, $why ) = _spawn( $safe, $args{passphrase}, \@lifecycle, $args{user} );
+    my ( $ok, $why ) = _spawn( $safe, $args{passphrase}, $config->{trog_provisioner}, \@command, $args{user} );
     return ( 0, $why ) unless $ok;
 
     return ( 1, "reprovision started; watch $log_dir/$safe.log" );
 }
 
-=head2 _spawn($domain, $passphrase, $lifecycle, $user)
+=head2 _spawn($domain, $passphrase, $dir, $command, $user)
 
-Fork the lifecycle off where nothing is waiting for it.
+Fork the provisioner off where nothing is waiting for it.
 
 Double forked and setsid'd on purpose: a save signals the parent to reload the
 routing table, and a provision which died because a worker was recycled halfway
 through would leave a half-built machine.  This has to outlive the request, the
 worker, and a restart.
 
-Each step runs through open() with a list, so there is no shell between us and
-the program, and the guest name is an argument rather than a thing that could be
+It runs through open() with a list, so there is no shell between us and the
+program, and the guest name is an argument rather than a thing that could be
 read as one.
 
 =cut
 
-sub _spawn ( $domain, $passphrase, $lifecycle, $user ) {
+sub _spawn ( $domain, $passphrase, $dir, $command, $user ) {
     my $log = "$log_dir/$domain.log";
 
     local $@;
@@ -427,50 +434,48 @@ sub _spawn ( $domain, $passphrase, $lifecycle, $user ) {
     # alive, so it has to be the pid of whatever is actually doing the work.
     _write_status( $domain, state => 'running', pid => $$, started => $started, user => ( $user // '' ) );
 
-    foreach my $step (@$lifecycle) {
-        my ( $dir, @command ) = @$step;
-
-        print {$fh} "\n--- $command[0] $command[1] (in $dir)\n";
+    print {$fh} "\n--- $command->[0] $command->[1] (in $dir)\n";
 
-        if ( !chdir($dir) ) {
-            print {$fh} "could not chdir to $dir: $!\n";
-            POSIX::_exit(1);
-        }
+    # In the checkout, so that anything the provisioner writes relative to where
+    # it was run lands there rather than in the middle of the website.
+    if ( !chdir($dir) ) {
+        print {$fh} "could not chdir to $dir: $!\n";
+        POSIX::_exit(1);
+    }
 
-        # The child's own stdout and stderr are the log, so whatever the program
-        # says lands there without a shell redirect to get wrong.
-        open( STDOUT, '>&', $fh ) or POSIX::_exit(1);
-        open( STDERR, '>&', $fh ) or POSIX::_exit(1);
-
-        # List form on purpose, and the reason the whole thing is shaped this
-        # way: a piped open with a list never involves a shell, so the guest
-        # name is an argument and cannot be read as a command.  The single
-        # argument form of this would be the bug.
-        my $ok = open( my $stdin, '|-', @command );    ## no critic (ProhibitPipeOpen) -- list form, so no shell
-        if ( !$ok ) {
-            print {$fh} "could not run $command[0]: $!\n";
-            POSIX::_exit(1);
-        }
+    # The child's own stdout and stderr are the log, so whatever the program
+    # says lands there without a shell redirect to get wrong.
+    open( STDOUT, '>&', $fh ) or POSIX::_exit(1);
+    open( STDERR, '>&', $fh ) or POSIX::_exit(1);
+
+    # List form on purpose, and the reason the whole thing is shaped this way: a
+    # piped open with a list never involves a shell, so the guest name is an
+    # argument and cannot be read as a command.  The single argument form of
+    # this would be the bug.
+    my $ok = open( my $stdin, '|-', @$command );    ## no critic (ProhibitPipeOpen) -- list form, so no shell
+    if ( !$ok ) {
+        print {$fh} "could not run $command->[0]: $!\n";
+        POSIX::_exit(1);
+    }
 
-        # What new_config prompts for.  The second step ignores it, and giving
-        # it a closed stdin instead would make anything that asks hang forever.
-        print {$stdin} "$passphrase\n";
-        close($stdin);
-
-        if ($?) {
-            my $status = $? >> 8;
-            print {$fh} "\n--- $command[0] exited $status, stopping here\n";
-            _write_status(
-                $domain,
-                state    => 'failed',
-                started  => $started,
-                finished => time(),
-                exit     => $status,
-                failed   => $command[0],
-                user     => ( $user // '' ),
-            );
-            POSIX::_exit($status);
-        }
+    # What the provisioner prompts for on its way to the secrets database.
+    # Closing this instead would make anything that asks hang forever.
+    print {$stdin} "$passphrase\n";
+    close($stdin);
+
+    if ($?) {
+        my $status = $? >> 8;
+        print {$fh} "\n--- $command->[0] exited $status\n";
+        _write_status(
+            $domain,
+            state    => 'failed',
+            started  => $started,
+            finished => time(),
+            exit     => $status,
+            failed   => $command->[0],
+            user     => ( $user // '' ),
+        );
+        POSIX::_exit($status);
     }
 
     print {$fh} "\n=== finished ===\n";

+ 4 - 4
lib/Trog/Routes/HTML.pm

@@ -2446,13 +2446,13 @@ sub guest_act ($query) {
 
 Implements POST /guest/reprovision.  Admin only.
 
-Rebuilding a guest from the recipe it was provisioned with: bin/new_config in
-the provisioners repository, and then bin/provision in trog-provisioner.  The
-only route which reaches Trog::DataSource::ProvisionedVirt::reprovision.
+Rebuilding a guest from the recipe it was provisioned with: bin/provision in
+trog-provisioner.  The only route which reaches
+Trog::DataSource::ProvisionedVirt::reprovision.
 
 This is not a recoverable operation, so read that function before changing
 anything here.  It refuses for the guest tCMS is itself running on, and the
-passphrase new_config asks for arrives with the request and is not stored.
+passphrase the provisioner asks for arrives with the request and is not stored.
 
 =cut
 

+ 32 - 41
t/Trog-DataSource-ProvisionedVirt.t

@@ -29,19 +29,22 @@ BEGIN {
     $ENV{HOME} = $ROOT;
     File::Path::make_path("$ROOT/$_") for qw{config logs www/assets};
 
-    # Stand-ins for the two repositories, with the two programs the lifecycle
-    # runs.  Never executed -- _spawn is mocked -- but they have to be there and
+    # A stand-in for the checkout, with the one program a reprovision runs.
+    # Never executed -- _spawn is mocked -- but it has to be there and
     # executable, because reprovision() checks that before it commits to
     # anything.
-    File::Path::make_path("$ROOT/provisioners/$_") for qw{bin recipes.d};
     File::Path::make_path("$ROOT/trog-provisioner/bin");
-    foreach my $program ( "$ROOT/provisioners/bin/new_config", "$ROOT/trog-provisioner/bin/provision" ) {
-        open( my $fh, '>', $program ) or die $!;
-        print {$fh} "#!/bin/sh\nexit 0\n";
-        close $fh;
-        chmod( 0755, $program );
-    }
-    open( my $recipe, '>', "$ROOT/provisioners/recipes.d/guest.example.com.yaml" ) or die $!;
+    my $program = "$ROOT/trog-provisioner/bin/provision";
+    open( my $fh, '>', $program ) or die $!;
+    print {$fh} "#!/bin/sh\nexit 0\n";
+    close $fh;
+    chmod( 0755, $program );
+
+    # And a stand-in for the installation's own files, which live where the
+    # provisioner keeps them rather than anywhere tCMS is told about.
+    $ENV{TROG_PROVISIONER_CONFIG} = "$ROOT/etc";
+    File::Path::make_path("$ROOT/etc/recipes.d");
+    open( my $recipe, '>', "$ROOT/etc/recipes.d/guest.example.com.yaml" ) or die $!;
     print {$recipe} "---\n_global:\n  registrar: secret:x/y/z\n";
     close $recipe;
 
@@ -79,8 +82,8 @@ $log->redefine( INFO => sub { note(shift) } );
 our @SPAWNED;
 $log->redefine(
     _spawn => sub {
-        my ( $domain, $passphrase, $lifecycle, $user ) = @_;
-        push( @SPAWNED, { domain => $domain, passphrase => $passphrase, lifecycle => $lifecycle, user => $user } );
+        my ( $domain, $passphrase, $dir, $command, $user ) = @_;
+        push( @SPAWNED, { domain => $domain, passphrase => $passphrase, dir => $dir, command => $command, user => $user } );
         return ( 1, 'started' );
     }
 );
@@ -89,10 +92,7 @@ $log->redefine(
 my $virt = Test::MockModule->new('Trog::DataSource::Virt');
 $virt->redefine( _is_self => sub { return $_[0] eq 'thisserver.example.com' ? 1 : 0 } );
 
-write_config(
-    provisioners     => "$ROOT/provisioners",
-    trog_provisioner => "$ROOT/trog-provisioner",
-);
+write_config( trog_provisioner => "$ROOT/trog-provisioner" );
 
 sub reprovision {
     @SPAWNED = ();
@@ -169,7 +169,7 @@ subtest 'what it refuses to reprovision' => sub {
     like( $why, qr/no recipe/, 'and says why' );
 
     ( $ok, $why ) = reprovision( domain => 'guest.example.com', passphrase => '' );
-    is( $ok, 0, 'a run with no passphrase, which new_config would just hang waiting for' );
+    is( $ok, 0, 'a run with no passphrase, which the provisioner would just hang waiting for' );
     like( $why, qr/passphrase/, 'and says why' );
 
     # The name reaches the filesystem and an argument list.
@@ -182,7 +182,7 @@ subtest 'what it refuses to reprovision' => sub {
     is_deeply( \@SPAWNED, [], 'and none of that ran a provisioner' );
 };
 
-subtest 'the lifecycle it would run' => sub {
+subtest 'what it would run' => sub {
     my ( $ok, $why ) = reprovision( domain => 'guest.example.com' );
     is( $ok, 1, 'a guest with a recipe is accepted' ) or diag($why);
     like( $why, qr/started/, 'and reports that it started rather than that it finished' );
@@ -192,25 +192,18 @@ subtest 'the lifecycle it would run' => sub {
     is( $run->{domain}, 'guest.example.com', 'for the guest asked about' );
     is( $run->{user},   'admin',             'recording who asked' );
 
-    # new_config in provisioners, then provision in trog-provisioner, each in
-    # its own repository.
-    is( scalar( @{ $run->{lifecycle} } ), 2, 'two steps' );
-    my ( $first, $second ) = @{ $run->{lifecycle} };
-    is( $first->[0], "$ROOT/provisioners", 'the first runs in provisioners' );
-    like( $first->[1], qr{/bin/new_config$}, 'and is new_config' );
-    is( $first->[2],  'guest.example.com',      'passed the guest as an argument' );
-    is( $second->[0], "$ROOT/trog-provisioner", 'the second runs in trog-provisioner' );
-    like( $second->[1], qr{/bin/provision$}, 'and is provision' );
-    is( $second->[2], 'guest.example.com', 'passed the same' );
-
-    # Arguments, not a command line: there is no shell between us and these, so
-    # a guest name can never be read as one.
-    foreach my $step ( @{ $run->{lifecycle} } ) {
-        is( scalar(@$step), 3, 'a directory, a program and one argument -- nothing to be parsed' );
-    }
+    # One program does the whole of it now: bin/provision generates the guest's
+    # configuration from its recipe and then builds the machine.
+    is( $run->{dir}, "$ROOT/trog-provisioner", 'run from the checkout' );
+    like( $run->{command}[0], qr{/bin/provision$}, 'and it is bin/provision' );
+    is( $run->{command}[1], 'guest.example.com', 'passed the guest as an argument' );
+
+    # Arguments, not a command line: there is no shell between us and this, so a
+    # guest name can never be read as one.
+    is( scalar( @{ $run->{command} } ), 2, 'a program and one argument -- nothing to be parsed' );
 };
 
-subtest 'without the repositories configured it is just Virt' => sub {
+subtest 'without the provisioner configured it is just Virt' => sub {
     write_config();
 
     my ( $ok, $why ) = reprovision( domain => 'guest.example.com' );
@@ -225,14 +218,12 @@ subtest 'without the repositories configured it is just Virt' => sub {
 
     # A path that is not there is a typo, and better found here than halfway
     # through a reprovision.
-    write_config( provisioners => "$ROOT/nonesuch", trog_provisioner => "$ROOT/trog-provisioner" );
+    write_config( trog_provisioner => "$ROOT/nonesuch" );
     ( $ok, $why ) = reprovision( domain => 'guest.example.com' );
     is( $ok, 0, 'a configured path which is not there is refused' );
+    like( $why, qr/not configured/, 'the same as never having configured one' );
 
-    write_config(
-        provisioners     => "$ROOT/provisioners",
-        trog_provisioner => "$ROOT/trog-provisioner",
-    );
+    write_config( trog_provisioner => "$ROOT/trog-provisioner" );
 };
 
 subtest 'the log is read back onto the page' => sub {
@@ -402,7 +393,7 @@ subtest 'the route' => sub {
     my $query = { user => 'admin', user_acls => ['admin'], tpsgi => $tpsgi, guest => 'guest.example.com', passphrase => 'hunter2', to => '/vms' };
     Trog::Routes::HTML::guest_reprovision($query);
     is( scalar(@SPAWNED),        1,         'an admin gets their run' );
-    is( $SPAWNED[0]{passphrase}, 'hunter2', 'the passphrase reaches the lifecycle' );
+    is( $SPAWNED[0]{passphrase}, 'hunter2', 'the passphrase reaches the provisioner' );
     ok( !exists $query->{passphrase}, 'and is gone from the query afterwards' );
 
     # The log route.  It lives in logs/ rather than under www/, and the output

+ 1 - 1
www/templates/html/components/forms/provisioned_guests.tx

@@ -125,7 +125,7 @@
                         <label>
                             Secrets passphrase
                             <input required autofocus class="cooltext" type="password" name="passphrase" autocomplete="off"
-                                   title="new_config asks for this on stdin; it is used for this run and not stored" />
+                                   title="the provisioner asks for this on stdin; it is used for this run and not stored" />
                         </label>
                         <div class="guest-reprovision-actions">
                             <button class="coolbutton" type="button"