diff mbox

[4/7] osstest: add a FreeBSD host install recipe

Message ID 20170523135148.77673-5-roger.pau@citrix.com (mailing list archive)
State New, archived
Headers show

Commit Message

Roger Pau Monne May 23, 2017, 1:51 p.m. UTC
The installation is performed using the bsdinstall tool, which is part of the
FreeBSD base system. The installer image is setup with the osstest ssh keys and
sshd enabled by default, which allows the test harness to just ssh into the
box, create the install config file and launch the scripted install.

Currently the installation is done with ZFS only, in stripe mode, and a single
disk.

In order to support the FreeBSD installer a new method is added, that allows
setting the pxe boot of a host using a memdisk. Also, a new tftp path is added
in order to point to the place where the FreeBSD installed images should be
stored.

The install script either picks the binary images from the output of a previous
FreeBSD buildjob (yet to be introduced), or from the freebsd_image and
freebsd_sets runvars, that should point to a FreeBSD installer image and to the
folder that contain the install sets respectively.

When relying on the output from a previous FreeBSD buildjob (freebsd_buildjob
runvar is set), the install image is picked from the path_freebsd-image runvar,
and the sets from path_freebsd-{base,kernel,manifest} of the previous job.

Signed-off-by: Roger Pau Monné <roger.pau@citrix.com>
---
 Osstest.pm              |   1 +
 Osstest/TestSupport.pm  |  18 +++-
 ts-freebsd-host-install | 259 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 276 insertions(+), 2 deletions(-)
 create mode 100755 ts-freebsd-host-install

Comments

Ian Jackson May 23, 2017, 4:04 p.m. UTC | #1
Roger Pau Monne writes ("[PATCH 4/7] osstest: add a FreeBSD host install recipe"):
> The installation is performed using the bsdinstall tool, which is
> part of the FreeBSD base system. The installer image is setup with
> the osstest ssh keys and sshd enabled by default, which allows the
> test harness to just ssh into the box, create the install config
> file and launch the scripted install.

Thanks.  I've read this in some detail now.  My comments follow.

> In order to support the FreeBSD installer a new method is added,
> that allows setting the pxe boot of a host using a memdisk. Also, a
> new tftp path is added in order to point to the place where the
> FreeBSD installed images should be stored.

Can you please add to the commit message a deployment note that the
tftp root needs to be provide with a copy of memdisk, eg
   /home/tftp/memdisk -> /usr/lib/syslinux/memdisk

> diff --git a/Osstest.pm b/Osstest.pm
> index a78728cd..a0c0339c 100644
> --- a/Osstest.pm
> +++ b/Osstest.pm
> @@ -227,6 +227,7 @@ sub readglobalconfig () {
>      $c{TftpTmpDir} ||= "$c{TftpPlayDir}tmp/";
>  
>      $c{TftpDiBase} ||= "$c{TftpPlayDir}debian-installer";
> +    $c{TftpFreeBSDBase} ||= "$c{TftpPlayDir}freebsd-installer";

You need to document this in README.

> +    foreach (@sets, "MANIFEST") {

I think it would be better to give this loop control variable a name.
When the scope of a use of $_ is too big, $_ has a tendency to get
trashed; this makes the code fragile in the face of future changes.

> +        if (!defined($r{"freebsd_sets"})) {
> +            # Get everything before the "." (ie: get base from base.txz)
> +            my $stash_name = lc((split /\./)[0]);

IMO that's not very idomatic perl.  I won't insist you change it, but
maybe this would be better

               my $stash_name = $set;
               $stash_name =~ s/[^.]+$//;

or

               $set =~ m/^[^.]+/ or die "$set ?";
               $stash_name = $&;

(Do you intend to split on the first `.' ?)

> +sub setup_netboot_installer () {
> +    my $pxeimg = "$ho->{Tftp}{FreeBSDBase}/install-$ho->{Name}.img";

I think this would be better under TftpTmpDir.  Cf ts-host-install's
placement of $ho--initrd.gz.

Maybe we want a function target_tftp_prefix or something which returns
   $ho->{Tftp}{TmpDir}".hostnamepath($ho)
?

Also the sha names could be in {TmpDir}/freebsd-images.  Does this, in
fact, need to be configurable at all ?  I think probably not.

> +    my $script = <<END;
> +set -ex
> +basedir=$ho->{Tftp}{Path}/$ho->{Tftp}{FreeBSDBase}/
> +hash=`sha256sum $image|head -c 64`
> +localpath="$r{arch}/\$hash/install.img";
> +mkdir -p \$basedir
> +(
> +flock -x -w 600 200

In osstest we generally use with-lock-ex from chiark-utils, rather
than flock from util-linux.  (Bonus: it should be more portable for if
someone wants to run a controller on BSD...)

with-lock-ex needs to be used in the wrapper way.
(Also, calling flock on a directory is rather outre'.)

You've written all this in bash because you found it too hard or
annoying in Perl ?  I don't much mind that, but the escaping is a bit
irritating.  Maybe you want to pass the script its parameters as
arguments so that you can use <<'END' rather than <<END ?

However,

> +    logm("Executing:\n$script");
> +    my $ret = system("/bin/bash -c '$script'");

This is quite wrong.  Your unparsing is unsound.  Amazingly your
script doesn't currently contain ' so this doesn't matter right now,
but that could change.

You should use the multi-argument form of system(3perl).  And, of
course, you should use Osstest::system_checked which does the error
handling for you.

You might want to build a command in an array @cmd, like copydir in
cr-publish-flight-logs does.  (Not sure why that doesn't use
system_checked...)

> +sub install () {
> +    my $authkeys = authorized_keys();
> +    my $knownhosts = known_hosts();
> +    my $sshd_keys_url = create_ssh_overlay();
> +    my $disk_names = "ada0 da0 ad0";

This should probably be
       my @disk_names = qw(ada0 da0 ad0);
in case anyone wants to manipulate it in perl.  You can substitute
it straight into the here document; perl will put in the " " again.

> +    my $installer_sets = join(" ", @sets);
> +    my $target_sets = "/tmp/osstest_sets";

Hardcoded /tmp antipattern.  Maybe this is technicallty OK because
it's an installer environment, but I think it sets a very bad
example.  Is there some other path you could use ?

> +    target_cmd_root($ho, 'chsh -s /bin/sh', 10);

!!  What's the default ?

> +for disk in $disk_names; do
> +    if [ -c "/dev/\$disk" ]; then
> +        echo \$disk
> +        exit 0
> +    fi
> +done
> +exit 1
> +END
> +    defined($disk) or die "Unable to find a valid disk";
> +    logm("Using $disk as destination disk device");

I have found that on some hosts, when installing Debian GNU/Linux, I
have to expect a nonstandard disk name.  Currently in the DB I can
only find this
   hydrazine          disk-device                    /dev/cciss/c0d0
(in Cambridge; referring to a gone-away machine; NB that the property
name is in the obsolete containing-spaces syntax and is equivalent to
DiskDevice.)

I think you may want to check a host property.  It should probably be
called Freebsd_DiskDevice or something.  (Weird capitalisation required
by the word splitting name transformation rules for host propertiess.)

> +    logm("Trying to figure out primary nic device name");
> +    $nic = target_cmd_output_root($ho, <<END, 30);
> +nics=`ifconfig -l`
> +for nic in \$nics; do
> +    addr=`ifconfig \$nic inet|grep inet|awk {'print \$2'}`
> +    if [ "\$addr" = "$ho->{Ip}" ]; then
> +        echo \$nic
> +        exit 0
> +    fi
> +done
> +exit 1

I have quite a lot of this kind of thing:

  gall-mite           'interface force'              eth0
  itch-mite           'interface force'              eth0
  grain-weevil        'interface force'              eth1
  rice-weevil         'interface force'              eth1
  arndale-bluewater   Interface_Force                eth0
  arndale-lakeside    Interface_Force                eth0
  arndale-metrocentre Interface_Force                eth0
  arndale-westfield   Interface_Force                eth0

This may not be needed for FreeBSD, but I thought I would mention it.

What would happen if you tried to run this setup on a host where
FreeBSD's idea of the first network interface is not the one which
osstest did the install on ?

IIRC there is a way for pxelinux to pass the NIC MAC address to the OS
it is running.  Also, the host database contains the MAC address of
the host (and this is generally necessary for osstest to work even in
standalone mode I think), so you might use that rather than the IP
address ?

> +    foreach (get_sets_path()) {
> +        target_putfile_root($ho, 600, $_->{path}, "$target_sets/$_->{name}"
);

Needs linewrap.

> +    logm("Creating the installer script");
> +    target_cmd_root($ho, <<END, 60);
> +        set -e
> +        cat << ENDSCRIPT > installscript

OK, my brain is fully bent now.  Can we not create installscript on
the controller and transfer it with target_putfilecontents_root_stash ?
That way the logs would contain a copy, too.

> +# Setup serial console
> +printf "%s" "-h -S$c{Baud}" >> /boot.config
> +cat << ENDBOOT >> /boot/loader.conf
> +boot_serial="YES"
> +comconsole_speed="$c{Baud}"
> +console="comconsole"
> +boot_verbose="YES"
> +beastie_disable="YES"
> +ENDBOOT

Where does the installer's output go ?  Ie, does booting the installer
image produce serial log output ?

Ian.
Ian Jackson May 23, 2017, 5:14 p.m. UTC | #2
Roger Pau Monne writes ("[PATCH 4/7] osstest: add a FreeBSD host install recipe"):
> The installation is performed using the bsdinstall tool, which is
> part of the FreeBSD base system. The installer image is setup with
> the osstest ssh keys and sshd enabled by default, which allows the
> test harness to just ssh into the box, create the install config
> file and launch the scripted install.
...
> +our $image = $r{"freebsd_image"} ||
> +             get_stashed("path_freebsd-image", $r{"freebsd_buildjob"});

I've had a thought about this.

Maybe it would be worth mentioning near the top of the script the
runvars it consumes (and any it sets) ?

I realise I haven't documented the runvars very much so far but maybe
we should start.  What do you think ?

Ian.
Roger Pau Monne May 24, 2017, 10:48 a.m. UTC | #3
On Tue, May 23, 2017 at 05:04:10PM +0100, Ian Jackson wrote:
> Roger Pau Monne writes ("[PATCH 4/7] osstest: add a FreeBSD host install recipe"):
> > The installation is performed using the bsdinstall tool, which is
> > part of the FreeBSD base system. The installer image is setup with
> > the osstest ssh keys and sshd enabled by default, which allows the
> > test harness to just ssh into the box, create the install config
> > file and launch the scripted install.
> 
> Thanks.  I've read this in some detail now.  My comments follow.
> 
> > In order to support the FreeBSD installer a new method is added,
> > that allows setting the pxe boot of a host using a memdisk. Also, a
> > new tftp path is added in order to point to the place where the
> > FreeBSD installed images should be stored.
> 
> Can you please add to the commit message a deployment note that the
> tftp root needs to be provide with a copy of memdisk, eg
>    /home/tftp/memdisk -> /usr/lib/syslinux/memdisk

Done.

> > diff --git a/Osstest.pm b/Osstest.pm
> > index a78728cd..a0c0339c 100644
> > --- a/Osstest.pm
> > +++ b/Osstest.pm
> > @@ -227,6 +227,7 @@ sub readglobalconfig () {
> >      $c{TftpTmpDir} ||= "$c{TftpPlayDir}tmp/";
> >  
> >      $c{TftpDiBase} ||= "$c{TftpPlayDir}debian-installer";
> > +    $c{TftpFreeBSDBase} ||= "$c{TftpPlayDir}freebsd-installer";
> 
> You need to document this in README.

(not needed anymore since we got rid of this).

> > +    foreach (@sets, "MANIFEST") {
> 
> I think it would be better to give this loop control variable a name.
> When the scope of a use of $_ is too big, $_ has a tendency to get
> trashed; this makes the code fragile in the face of future changes.

Done.

> 
> > +        if (!defined($r{"freebsd_sets"})) {
> > +            # Get everything before the "." (ie: get base from base.txz)
> > +            my $stash_name = lc((split /\./)[0]);
> 
> IMO that's not very idomatic perl.  I won't insist you change it, but
> maybe this would be better
> 
>                my $stash_name = $set;
>                $stash_name =~ s/[^.]+$//;
> 
> or
> 
>                $set =~ m/^[^.]+/ or die "$set ?";
>                $stash_name = $&;
> 
> (Do you intend to split on the first `.' ?)

I don't expect this names to ever have more than one dot, this is not
something that changes, so there should really be only one dot (or
none in the MANIFEST case).

I prefer the second one because it strips the ".", while the first one
doesn't.

(as you probably already guessed my perl/regexp is not very good)

> 
> > +sub setup_netboot_installer () {
> > +    my $pxeimg = "$ho->{Tftp}{FreeBSDBase}/install-$ho->{Name}.img";
> 
> I think this would be better under TftpTmpDir.  Cf ts-host-install's
> placement of $ho--initrd.gz.
> 
> Maybe we want a function target_tftp_prefix or something which returns
>    $ho->{Tftp}{TmpDir}".hostnamepath($ho)
> ?

Sounds fine, I guess I will add this as a pre-patch since there's already a
consumer in ts-host-install.

> Also the sha names could be in {TmpDir}/freebsd-images.  Does this, in
> fact, need to be configurable at all ?  I think probably not.

OK, so then I will just drop FreeBSDBase and just use
$ho->{Tftp}{TmpDir}/freebsd-images. I don't think there's a lot of
value in have it in a standard folder, since those are just random
builds. I guess this makes more sense for Debian because they are
actual releases, and thus can be used for other stuff also?

> > +    my $script = <<END;
> > +set -ex
> > +basedir=$ho->{Tftp}{Path}/$ho->{Tftp}{FreeBSDBase}/
> > +hash=`sha256sum $image|head -c 64`
> > +localpath="$r{arch}/\$hash/install.img";
> > +mkdir -p \$basedir
> > +(
> > +flock -x -w 600 200
> 
> In osstest we generally use with-lock-ex from chiark-utils, rather
> than flock from util-linux.  (Bonus: it should be more portable for if
> someone wants to run a controller on BSD...)
> 
> with-lock-ex needs to be used in the wrapper way.
> (Also, calling flock on a directory is rather outre'.)

I don't have any problems using with-lock-ex, I just didn't know it
existed :).

> You've written all this in bash because you found it too hard or
> annoying in Perl ?  I don't much mind that, but the escaping is a bit
> irritating.  Maybe you want to pass the script its parameters as
> arguments so that you can use <<'END' rather than <<END ?

Hm, yes, I wasn't really confident of writing this in perl and being
sure that the lock was always released. I'm more confident with shell.

> However,
> 
> > +    logm("Executing:\n$script");
> > +    my $ret = system("/bin/bash -c '$script'");
> 
> This is quite wrong.  Your unparsing is unsound.  Amazingly your
> script doesn't currently contain ' so this doesn't matter right now,
> but that could change.
> 
> You should use the multi-argument form of system(3perl).  And, of
> course, you should use Osstest::system_checked which does the error
> handling for you.
> 
> You might want to build a command in an array @cmd, like copydir in
> cr-publish-flight-logs does.  (Not sure why that doesn't use
> system_checked...)

I've changed this to use an array with all the parameters and
system_checked, it looks much better now IMHO, thanks.

> > +sub install () {
> > +    my $authkeys = authorized_keys();
> > +    my $knownhosts = known_hosts();
> > +    my $sshd_keys_url = create_ssh_overlay();
> > +    my $disk_names = "ada0 da0 ad0";
> 
> This should probably be
>        my @disk_names = qw(ada0 da0 ad0);
> in case anyone wants to manipulate it in perl.  You can substitute
> it straight into the here document; perl will put in the " " again.
> 
> > +    my $installer_sets = join(" ", @sets);
> > +    my $target_sets = "/tmp/osstest_sets";
> 
> Hardcoded /tmp antipattern.  Maybe this is technicallty OK because
> it's an installer environment, but I think it sets a very bad
> example.  Is there some other path you could use ?

I'm open to suggestions. We could also use ~/osstest_sets. I don't
really have a preference. I've used /tmp because I though it would be
less controversial.

> > +    target_cmd_root($ho, 'chsh -s /bin/sh', 10);
> 
> !!  What's the default ?

csh.

> > +for disk in $disk_names; do
> > +    if [ -c "/dev/\$disk" ]; then
> > +        echo \$disk
> > +        exit 0
> > +    fi
> > +done
> > +exit 1
> > +END
> > +    defined($disk) or die "Unable to find a valid disk";
> > +    logm("Using $disk as destination disk device");
> 
> I have found that on some hosts, when installing Debian GNU/Linux, I
> have to expect a nonstandard disk name.  Currently in the DB I can
> only find this
>    hydrazine          disk-device                    /dev/cciss/c0d0
> (in Cambridge; referring to a gone-away machine; NB that the property
> name is in the obsolete containing-spaces syntax and is equivalent to
> DiskDevice.)
> 
> I think you may want to check a host property.  It should probably be
> called Freebsd_DiskDevice or something.  (Weird capitalisation required
> by the word splitting name transformation rules for host propertiess.)

Yes, I can do that, but we would have to fill the DB manually. Would
you be OK with leaving this as-is in this patch and me adding the
property fetching later on?

> > +    logm("Trying to figure out primary nic device name");
> > +    $nic = target_cmd_output_root($ho, <<END, 30);
> > +nics=`ifconfig -l`
> > +for nic in \$nics; do
> > +    addr=`ifconfig \$nic inet|grep inet|awk {'print \$2'}`
> > +    if [ "\$addr" = "$ho->{Ip}" ]; then
> > +        echo \$nic
> > +        exit 0
> > +    fi
> > +done
> > +exit 1
> 
> I have quite a lot of this kind of thing:
> 
>   gall-mite           'interface force'              eth0
>   itch-mite           'interface force'              eth0
>   grain-weevil        'interface force'              eth1
>   rice-weevil         'interface force'              eth1
>   arndale-bluewater   Interface_Force                eth0
>   arndale-lakeside    Interface_Force                eth0
>   arndale-metrocentre Interface_Force                eth0
>   arndale-westfield   Interface_Force                eth0
> 
> This may not be needed for FreeBSD, but I thought I would mention it.
> 
> What would happen if you tried to run this setup on a host where
> FreeBSD's idea of the first network interface is not the one which
> osstest did the install on ?

FreeBSD doesn't really have an idea of the first network interface
(unless you count the order in the output from ifconfig -l).

Also, on FreeBSD each driver has it's own device, with different name,
ie: there are em0, em1, bge0, bce0... interfaces. We could add
a host property like Freebsd_NicDevice, but this fallback should stay
in case the parameter is not defined?

> IIRC there is a way for pxelinux to pass the NIC MAC address to the OS
> it is running.  Also, the host database contains the MAC address of
> the host (and this is generally necessary for osstest to work even in
> standalone mode I think), so you might use that rather than the IP
> address ?

This is not suitable in this case because from FreeBSD installer PoV
it has been booted from a disk (because memdisk is used), so none of
the PXE variables are propagated.

> > +    foreach (get_sets_path()) {
> > +        target_putfile_root($ho, 600, $_->{path}, "$target_sets/$_->{name}"
> );
> 
> Needs linewrap.
> 
> > +    logm("Creating the installer script");
> > +    target_cmd_root($ho, <<END, 60);
> > +        set -e
> > +        cat << ENDSCRIPT > installscript
> 
> OK, my brain is fully bent now.  Can we not create installscript on
> the controller and transfer it with target_putfilecontents_root_stash ?
> That way the logs would contain a copy, too.

Yes, that's much better, thanks!

> > +# Setup serial console
> > +printf "%s" "-h -S$c{Baud}" >> /boot.config
> > +cat << ENDBOOT >> /boot/loader.conf
> > +boot_serial="YES"
> > +comconsole_speed="$c{Baud}"
> > +console="comconsole"
> > +boot_verbose="YES"
> > +beastie_disable="YES"
> > +ENDBOOT
> 
> Where does the installer's output go ?  Ie, does booting the installer
> image produce serial log output ?

Yes, the installer image is built with serial output already.

The output of the installer itself (bsdinstall) goes to the job log
file.

Thanks, Roger.
Ian Jackson May 24, 2017, 11:12 a.m. UTC | #4
Roger Pau Monne writes ("Re: [PATCH 4/7] osstest: add a FreeBSD host install recipe"):
> OK, so then I will just drop FreeBSDBase and just use
> $ho->{Tftp}{TmpDir}/freebsd-images. I don't think there's a lot of
> value in have it in a standard folder, since those are just random
> builds. I guess this makes more sense for Debian because they are
> actual releases, and thus can be used for other stuff also?

Yes.  Indeed they might not be managed by osstest at all.

> > > +    my $installer_sets = join(" ", @sets);
> > > +    my $target_sets = "/tmp/osstest_sets";
> > 
> > Hardcoded /tmp antipattern.  Maybe this is technicallty OK because
> > it's an installer environment, but I think it sets a very bad
> > example.  Is there some other path you could use ?
> 
> I'm open to suggestions. We could also use ~/osstest_sets. I don't
> really have a preference. I've used /tmp because I though it would be
> less controversial.

Hah :-).

I'm fine with almost any path other than /tmp/FIXED_STRING.

> > > +    target_cmd_root($ho, 'chsh -s /bin/sh', 10);
> > 
> > !!  What's the default ?
> 
> csh.

omg wtf bbq.  Right.  Fine.

> > I have found that on some hosts, when installing Debian GNU/Linux, I
> > have to expect a nonstandard disk name.  Currently in the DB I can
> > only find this
> >    hydrazine          disk-device                    /dev/cciss/c0d0
> > (in Cambridge; referring to a gone-away machine; NB that the property
> > name is in the obsolete containing-spaces syntax and is equivalent to
> > DiskDevice.)
> > 
> > I think you may want to check a host property.  It should probably be
> > called Freebsd_DiskDevice or something.  (Weird capitalisation required
> > by the word splitting name transformation rules for host propertiess.)
> 
> Yes, I can do that, but we would have to fill the DB manually. Would
> you be OK with leaving this as-is in this patch and me adding the
> property fetching later on?

OK.  Hopefully it won't come up.

> > What would happen if you tried to run this setup on a host where
> > FreeBSD's idea of the first network interface is not the one which
> > osstest did the install on ?
> 
> FreeBSD doesn't really have an idea of the first network interface
> (unless you count the order in the output from ifconfig -l).
> 
> Also, on FreeBSD each driver has it's own device, with different name,
> ie: there are em0, em1, bge0, bce0... interfaces. We could add
> a host property like Freebsd_NicDevice, but this fallback should stay
> in case the parameter is not defined?

I think this is too complicated and hypothetical.  Let's leave it for
now, as you have it.

> > > +    logm("Creating the installer script");
> > > +    target_cmd_root($ho, <<END, 60);
> > > +        set -e
> > > +        cat << ENDSCRIPT > installscript
> > 
> > OK, my brain is fully bent now.  Can we not create installscript on
> > the controller and transfer it with target_putfilecontents_root_stash ?
> > That way the logs would contain a copy, too.
> 
> Yes, that's much better, thanks!

:-)

> > > +# Setup serial console
> > > +printf "%s" "-h -S$c{Baud}" >> /boot.config
> > > +cat << ENDBOOT >> /boot/loader.conf
> > > +boot_serial="YES"
> > > +comconsole_speed="$c{Baud}"
> > > +console="comconsole"
> > > +boot_verbose="YES"
> > > +beastie_disable="YES"
> > > +ENDBOOT
> > 
> > Where does the installer's output go ?  Ie, does booting the installer
> > image produce serial log output ?
> 
> Yes, the installer image is built with serial output already.
> 
> The output of the installer itself (bsdinstall) goes to the job log
> file.

Great.  Looking good :-).

Thanks,
Ian./
diff mbox

Patch

diff --git a/Osstest.pm b/Osstest.pm
index a78728cd..a0c0339c 100644
--- a/Osstest.pm
+++ b/Osstest.pm
@@ -227,6 +227,7 @@  sub readglobalconfig () {
     $c{TftpTmpDir} ||= "$c{TftpPlayDir}tmp/";
 
     $c{TftpDiBase} ||= "$c{TftpPlayDir}debian-installer";
+    $c{TftpFreeBSDBase} ||= "$c{TftpPlayDir}freebsd-installer";
     $c{TftpDiVersion} ||= $c{ "TftpDiVersion_$c{DebianSuite}" } // 'current';
 
     $c{TftpGrubBase} ||= "$c{TftpPlayDir}grub";
diff --git a/Osstest/TestSupport.pm b/Osstest/TestSupport.pm
index 8c7078c5..4418f024 100644
--- a/Osstest/TestSupport.pm
+++ b/Osstest/TestSupport.pm
@@ -118,7 +118,7 @@  BEGIN {
                       await_webspace_fetch_byleaf create_webfile
                       file_link_contents get_timeout
                       setup_netboot_di setup_netboot_local host_netboot_file
-		      subst_netboot_template
+                      subst_netboot_template setup_netboot_memdisk
 
                       ether_prefix
 
@@ -1032,7 +1032,7 @@  sub selecthost ($) {
     $ho->{Tftp} = { };
     $ho->{Tftp}{$_} = $c{"Tftp${_}_${tftpscope}"} || $c{"Tftp${_}"}
         foreach qw(Path TmpDir PxeDir NetbootGroup PxeTemplates PxeTemplatesReal
-                   DiBase GrubBase
+                   DiBase GrubBase FreeBSDBase
                    NetGrubDir NetGrubTemplates NetGrubTemplatesReal
                    PxeGroup);
     $ho->{TftpNetbootGroup} //= $ho->{TftpPxeGroup}; # compatibility
@@ -2553,6 +2553,20 @@  default local
 END
 }
 
+sub setup_netboot_memdisk ($$) {
+    my ($ho, $img) = @_;
+    setup_netboot_bootcfg($ho, <<END);
+serial 0 $c{Baud}
+timeout 5
+label overwrite
+	menu label ^Overwrite
+	menu default
+	kernel memdisk
+	append initrd=$img
+default overwrite
+END
+}
+
 # uboot emulates pxelinux, so reuse BIOS stuff
 sub setup_netboot_di_uboot ($$$$$;%) { return &setup_netboot_di_bios; }
 sub setup_netboot_local_uboot ($) { return &setup_netboot_local_bios; }
diff --git a/ts-freebsd-host-install b/ts-freebsd-host-install
new file mode 100755
index 00000000..d59b66a4
--- /dev/null
+++ b/ts-freebsd-host-install
@@ -0,0 +1,259 @@ 
+#!/usr/bin/perl -w
+# This is part of "osstest", an automated testing framework for Xen.
+# Copyright (C) 2017 Citrix Inc.
+# 
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use strict qw(vars);
+use DBI;
+use POSIX;
+
+unshift @INC, qw(.);
+use Osstest;
+use Osstest::TestSupport;
+
+tsreadconfig();
+
+our %xopts;
+
+our ($whhost) = @ARGV;
+$whhost ||= 'host';
+our $ho= selecthost($whhost);
+exit 0 if $ho->{Flags}{'no-reinstall'};
+exit 0 if $ho->{SharedReady};
+
+our %timeout= qw(Sshd 1000);
+
+our @sets = qw(base.txz kernel.txz);
+
+our $image = $r{"freebsd_image"} ||
+             get_stashed("path_freebsd-image", $r{"freebsd_buildjob"});
+
+sub get_sets_path () {
+    my @paths;
+
+    foreach (@sets, "MANIFEST") {
+        my $path;
+
+        if (!defined($r{"freebsd_sets"})) {
+            # Get everything before the "." (ie: get base from base.txz)
+            my $stash_name = lc((split /\./)[0]);
+            $path = get_stashed("path_freebsd-$stash_name",
+                                $r{"freebsd_buildjob"});
+        } else {
+            $path = $r{"freebsd_sets"} . "/$_";
+        }
+
+        push @paths, { name => "$_", path => "$path" };
+    }
+
+    return @paths;
+}
+
+sub create_ssh_overlay () {
+    my $url = create_webfile($ho, "ssh.tar", sub {
+        my ($fh) = @_;
+        contents_make_cpio($fh, 'ustar',  "$c{OverlayLocal}/etc/ssh/");
+    });
+
+    return $url;
+}
+
+sub setup_netboot_installer () {
+    my $pxeimg = "$ho->{Tftp}{FreeBSDBase}/install-$ho->{Name}.img";
+    my $script = <<END;
+set -ex
+basedir=$ho->{Tftp}{Path}/$ho->{Tftp}{FreeBSDBase}/
+hash=`sha256sum $image|head -c 64`
+localpath="$r{arch}/\$hash/install.img";
+mkdir -p \$basedir
+(
+flock -x -w 600 200
+cd \$basedir
+if [ ! -f \$localpath ]; then
+    mkdir -p `dirname \$localpath`
+    cp $image \$localpath
+fi
+rm -f install-$ho->{Name}.img
+ln \$localpath install-$ho->{Name}.img
+for hash in `ls $r{arch}/`; do
+    count=`stat -c %h $r{arch}/\$hash/install.img`
+    if [ \$count -eq 1 ]; then
+        rm -rf $r{arch}/\$hash
+    fi
+done
+) 200<\$basedir
+END
+
+    logm("Executing:\n$script");
+    my $ret = system("/bin/bash -c '$script'");
+    $ret == 0 or die "Unable to setup netboot";
+
+    # Setup the pxelinux config file
+    logm("Booting from installer image at $pxeimg");
+    setup_netboot_memdisk($ho, $pxeimg);
+}
+
+sub install () {
+    my $authkeys = authorized_keys();
+    my $knownhosts = known_hosts();
+    my $sshd_keys_url = create_ssh_overlay();
+    my $disk_names = "ada0 da0 ad0";
+    my $installer_sets = join(" ", @sets);
+    my $target_sets = "/tmp/osstest_sets";
+    my $disk;
+    my $nic;
+
+    target_cmd_root($ho, 'chsh -s /bin/sh', 10);
+
+    logm("Trying to find a disk to install to");
+    $disk = target_cmd_output_root($ho, <<END, 30);
+for disk in $disk_names; do
+    if [ -c "/dev/\$disk" ]; then
+        echo \$disk
+        exit 0
+    fi
+done
+exit 1
+END
+    defined($disk) or die "Unable to find a valid disk";
+    logm("Using $disk as destination disk device");
+
+    logm("Trying to figure out primary nic device name");
+    $nic = target_cmd_output_root($ho, <<END, 30);
+nics=`ifconfig -l`
+for nic in \$nics; do
+    addr=`ifconfig \$nic inet|grep inet|awk {'print \$2'}`
+    if [ "\$addr" = "$ho->{Ip}" ]; then
+        echo \$nic
+        exit 0
+    fi
+done
+exit 1
+END
+    defined($nic) or die "Unable to find primary network interface";
+    logm("Using $nic as primary network interface");
+
+    logm("Uploading the install sets to the system");
+    target_cmd_root($ho, <<END, 30);
+mkdir -p $target_sets
+mount -o size=1G -t tmpfs tmpfs $target_sets
+END
+
+    foreach (get_sets_path()) {
+        target_putfile_root($ho, 600, $_->{path}, "$target_sets/$_->{name}");
+    }
+
+    logm("Creating the installer script");
+    target_cmd_root($ho, <<END, 60);
+        set -e
+        cat << ENDSCRIPT > installscript
+set -a
+BSDINSTALL_DISTDIR="$target_sets"
+ZFSBOOT_DISKS="$disk"
+DISTRIBUTIONS="$installer_sets"
+nonInteractive=1
+
+#!/bin/sh
+set -ex
+
+# Setup nic and sshd
+echo "ifconfig_$nic=DHCP" >> /etc/rc.conf
+echo "sshd_enable=YES" >> /etc/rc.conf
+
+# Disable sendmail
+echo "sendmail_enable=NONE" >> /etc/rc.conf
+
+# Set proxy for the pkg manager
+mkdir -p /usr/local/etc/
+cat << ENDPKG >> /usr/local/etc/pkg.conf
+pkg_env: { http_proxy = $c{HttpProxy} }
+default_always_yes: true
+assume_always_yes: true
+ENDPKG
+
+# Bootstap the package manager
+export HTTP_PROXY=$c{HttpProxy}
+export ASSUME_ALWAYS_YES=yes
+pkg update
+
+# Allow root user login and setup ssh keys
+chsh -s /bin/sh root
+echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
+mkdir -p /root/.ssh
+cat << ENDKEYS > /root/.ssh/authorized_keys
+$authkeys
+ENDKEYS
+cat << ENDHOSTS > /root/.ssh/known_hosts
+$knownhosts
+ENDHOSTS
+
+# Fetch host keys
+fetch $sshd_keys_url -o - | tar -xf - -C /etc/ssh/
+# Set correct permissions
+chown root:wheel /etc/ssh/ssh_host_*_key*
+chmod 0600 /etc/ssh/ssh_host_*_key
+chmod 0644 /etc/ssh/ssh_host_*_key.pub
+
+# Add a osstest user
+pw useradd osstest -m
+chsh -s /bin/sh osstest
+mkdir -p /home/osstest/.ssh
+cat << ENDKEYS > /home/osstest/.ssh/authorized_keys
+$authkeys
+ENDKEYS
+cat << ENDHOSTS > /home/osstest/.ssh/known_hosts
+$knownhosts
+ENDHOSTS
+
+# Setup serial console
+printf "%s" "-h -S$c{Baud}" >> /boot.config
+cat << ENDBOOT >> /boot/loader.conf
+boot_serial="YES"
+comconsole_speed="$c{Baud}"
+console="comconsole"
+boot_verbose="YES"
+beastie_disable="YES"
+ENDBOOT
+
+ENDSCRIPT
+END
+
+    logm("Launch the installer");
+    target_cmd_root($ho, 'bsdinstall script installscript', 1200);
+
+    target_reboot($ho);
+
+    logm("Waiting for the host to boot");
+    await_tcp(get_timeout($ho,'reboot',$timeout{Sshd}), 14, $ho);
+
+    logm("FreeBSD installed succesfully");
+}
+
+# Switch off, setup PXE and switch on to the installer
+power_state($ho, 0);
+setup_netboot_installer();
+power_cycle_sleep($ho);
+power_state($ho, 1);
+
+# Wait for the host to finish booting
+logm("Waiting for the installer to boot");
+await_tcp(get_timeout($ho,'reboot',$timeout{Sshd}), 14, $ho);
+
+# Next boot will be from local disk
+setup_netboot_local($ho);
+
+# Proceed with the install
+install();
+