Date: Tue, 24 Jul 2018 13:21:44 +0000 (UTC) From: Kyle Evans <kevans@FreeBSD.org> To: src-committers@freebsd.org, svn-src-projects@freebsd.org Subject: svn commit: r336669 - in projects/bectl/sbin: . be bectl Message-ID: <201807241321.w6ODLifY030817@repo.freebsd.org>
next in thread | raw e-mail | index | archive | help
Author: kevans Date: Tue Jul 24 13:21:44 2018 New Revision: 336669 URL: https://svnweb.freebsd.org/changeset/base/336669 Log: Rename be(1) to bectl(8); continues to live in /sbin Discussed with: rpokala, allanjude Added: projects/bectl/sbin/bectl/ - copied from r336668, projects/bectl/sbin/be/ projects/bectl/sbin/bectl/bectl.8 - copied unchanged from r336668, projects/bectl/sbin/be/be.1 projects/bectl/sbin/bectl/bectl.c - copied unchanged from r336668, projects/bectl/sbin/be/be.c Deleted: projects/bectl/sbin/be/ projects/bectl/sbin/bectl/be.1 projects/bectl/sbin/bectl/be.c Modified: projects/bectl/sbin/Makefile projects/bectl/sbin/bectl/Makefile Modified: projects/bectl/sbin/Makefile ============================================================================== --- projects/bectl/sbin/Makefile Tue Jul 24 13:17:40 2018 (r336668) +++ projects/bectl/sbin/Makefile Tue Jul 24 13:21:44 2018 (r336669) @@ -86,7 +86,7 @@ SUBDIR.${MK_PF}+= pfctl SUBDIR.${MK_PF}+= pflogd SUBDIR.${MK_QUOTAS}+= quotacheck SUBDIR.${MK_ROUTED}+= routed -SUBDIR.${MK_ZFS}+= be +SUBDIR.${MK_ZFS}+= bectl SUBDIR.${MK_ZFS}+= zfsbootcfg SUBDIR.${MK_TESTS}+= tests Modified: projects/bectl/sbin/bectl/Makefile ============================================================================== --- projects/bectl/sbin/be/Makefile Tue Jul 24 13:17:40 2018 (r336668) +++ projects/bectl/sbin/bectl/Makefile Tue Jul 24 13:21:44 2018 (r336669) @@ -1,8 +1,8 @@ # @(#)Makefile 8.1 (Berkeley) 6/6/93 -PROG= be +PROG= bectl WARNS?= 1 -MAN= be.1 +MAN= bectl.8 LIBADD+= be LIBADD+= nvpair Copied: projects/bectl/sbin/bectl/bectl.8 (from r336668, projects/bectl/sbin/be/be.1) ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ projects/bectl/sbin/bectl/bectl.8 Tue Jul 24 13:21:44 2018 (r336669, copy of r336668, projects/bectl/sbin/be/be.1) @@ -0,0 +1,220 @@ +.\" +.\" be - Utility to manage Boot Environments on the ZFS filesystem +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" +.\" @(#)be.1 +.\" $FreeBSD$ +.\" +.Dd June 15, 2017 +.Dt BE 1 +.Os FreeBSD +.Sh NAME +.Nm be +.Nd Utility to manage Boot Environments on ZFS +.Sh SYNOPSIS +.Nm +activate +.Op Fl t +.Ao Ar beName Ac +.Nm +create +.Op Fl r +.Op Fl e Ar nonActiveBe | Fl e Ar beName@snapshot +.Ao Ar beName Ac +.Nm +create +.Op Fl r +.Ao Ar beName@snapshot Ac +.Nm +destroy +.Op Fl F +.Ao Ar beName | beName@snapshot Ac +.Nm +jail +.Ao Ar jailID | jailName Ac +.Ao Ar bootenv Ac +.Nm +list +.Op Fl a +.Op Fl D +.Op Fl H +.Op Fl s +.Nm +mount +.Ao Ar beName Ac +.Op mountpoint +.Nm +rename +.Ao Ar origBeName Ac +.Ao Ar newBeName Ac +.Nm +{ ujail | unjail } +.Ao Ar jailID | jailName Ac +.Ao Ar bootenv Ac +.Nm +{ umount | unmount } +.Op Fl f +.Ao Ar beName Ac +.Sh DESCRIPTION +The +.Nm +command is used to setup and interact with ZFS boot environments, which are bootable clones of datasets. +.Pp +.Em Boot Environments +allows the system to be upgraded, while preserving the old system environment in a separate ZFS dataset. +.Pp +.Sh COMMANDS +The following commands are supported by +.Nm : +.Bl -tag -width activate +.It Ic activate +.Op Fl t +.Ar <beName> +.Pp +Activate the given +.Ar beName +as the default boot filesystem. If the +.Op Fl t +flag is given, this takes effect only for the next boot. +.Pp +.It Ic create +.Op Fl r +.Op Fl e Ar nonActiveBe | Fl e Ar beName@snapshot +.Ao Ar beName Ac +.Pp +Creates a new boot environment named +.Ar beName . +If the -e param is specified, the new environment will be cloned from the given +.Ar nonActiveBe | Ar beName@snapshot . +If the +.Op Fl r +flag is given, a recursive boot environment will be made. +.Pp +.It Ic create +.Op Fl r +.Ao Ar beName@snapshot Ac +.Pp +Creates a snapshot of the existing boot environment named +.Ar beName . +If the +.Op Fl r +flag is given, a recursive boot environment will be made. +.Pp +.It Ic destroy +.Op Fl F +.Ao Ar beName | beName@snapshot Ac +.Pp +Destroys the given +.Ar beName +boot environment or +.Ar beName@snapshot +snapshot. +Specifying +.Fl F +will automatically unmount without confirmation. +.Pp +.It Ic jail +.Ao Ar jailID | jailName Ac +.Ao Ar bootenv Ac +.Pp +Creates a jail of the given boot environment. +.Pp +.It Ic list +.Op Fl a +.Op Fl D +.Op Fl H +.Op Fl s +.Pp +Displays all boot environments. +The Active field indicates whether the boot environment is active now (N); active on reboot (R); or both (NR). +.Pp +If +.Fl a +is used, display all datasets. +If +.Fl D +is used, display the full space usage for each boot environment, assuming all other boot environments were destroyed. +The +.Fl H +option is used for scripting. It does not print headers and separate fields by a single tab instead of arbitrary white space. +If +.Fl s +is used, display all snapshots as well. +.Pp +.It Ic mount +.Ao Ar beName Ac +.Op mountpoint +.Pp +Temporarily mount the boot environment. +Mount at the specified +.Ar mountpoint +if provided. +.Pp +.It Ic rename Ao Ar origBeName Ac Ao Ar newBeName Ac +.Pp +Renames the given nonactive +.Ar origBeName +to the given +.Ar newBeName +.Pp +.It Ic unmount +.Op Fl f +.Ao Ar beName Ac +.Pp +Unmount the given boot environment, if it is mounted. +Specifying +.Fl f +will force the unmount if busy. +.Pp +.It Ic unjail +.Ao Ar beName Ac +.Pp +Destroys the jail created from the given boot environment. +.Pp +.El +.Sh EXAMPLES +.Bl -bullet +.It +To fill in with jail upgrade example when behavior is firm. +.Pp +.Sh SEE ALSO +.Xr jail 8 , +.Xr zfs 8 , +.Xr zpool 8 +.Sh HISTORY +.Nm +is based on +.Xr beadm 1 +and was implemented as a project for the 2017 Summer of Code, along with +.Xr libbe 3 . +.Sh AUTHORS +.Bl -bullet +.It +Kyle Kneitinger (kneitinger) +.Ar kyle@kneit.in +.Pp +Creator of +.Nm . +.It +Slawomir Wojciech Wojtczak (vermaden) +.Ar vermaden@interia.pl +.Pp +Creator and maintainer of +.Xr beadm 1 . +.It +Bryan Drewery (bdrewery) +.Ar bryan@shatow.net +.Pp +Wrote the original +.Xr beadm 1 +manual page that this one is derived from. +.El Copied: projects/bectl/sbin/bectl/bectl.c (from r336668, projects/bectl/sbin/be/be.c) ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ projects/bectl/sbin/bectl/bectl.c Tue Jul 24 13:21:44 2018 (r336669, copy of r336668, projects/bectl/sbin/be/be.c) @@ -0,0 +1,664 @@ +/* + * be.c + * + * Copyright (c) 2017 Kyle J. Kneitinger <kyle@kneit.in> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/param.h> +#include <sys/jail.h> +#include <sys/mount.h> +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include <sys/nv.h> +#include <be.h> + +static int be_cmd_activate(int argc, char *argv[]); +static int be_cmd_create(int argc, char *argv[]); +static int be_cmd_destroy(int argc, char *argv[]); +static int be_cmd_export(int argc, char *argv[]); +static int be_cmd_import(int argc, char *argv[]); +static int be_cmd_add(int argc, char *argv[]); +static int be_cmd_jail(int argc, char *argv[]); +static int be_cmd_list(int argc, char *argv[]); +static int be_cmd_mount(int argc, char *argv[]); +static int be_cmd_rename(int argc, char *argv[]); +static int be_cmd_unjail(int argc, char *argv[]); +static int be_cmd_unmount(int argc, char *argv[]); + +libbe_handle_t *be; + +static int +usage(bool explicit) +{ + FILE *fp = explicit ? stdout : stderr; + + fprintf(fp, + "usage:\tbe ( -h | -? | subcommand [args...] )\n" + "\tbe activate [-t] beName\n" + "\tbe create [-e nonActiveBe | -e beName@snapshot] beName\n" + "\tbe create beName@snapshot\n" + "\tbe destroy [-F] beName | beName@snapshot⟩\n" + "\tbe export sourceBe\n" + "\tbe import targetBe\n" + "\tbe add (path)*\n" + "\tbe jail bootenv\n" + "\tbe list [-a] [-D] [-H] [-s]\n" + "\tbe mount beName [mountpoint]\n" + "\tbe rename origBeName newBeName\n" + "\tbe { ujail | unjail } ⟨jailID | jailName⟩ bootenv\n" + "\tbe { umount | unmount } [-f] beName\n"); + + return (explicit ? 0 : EX_USAGE); +} + + +/* + * Represents a relationship between the command name and the parser action + * that handles it. + */ +struct command_map_entry { + const char *command; + int (*fn)(int argc, char *argv[]); +}; + +static struct command_map_entry command_map[] = +{ + { "activate", be_cmd_activate }, + { "create", be_cmd_create }, + { "destroy", be_cmd_destroy }, + { "export", be_cmd_export }, + { "import", be_cmd_import }, + { "add", be_cmd_add }, + { "jail", be_cmd_jail }, + { "list", be_cmd_list }, + { "mount", be_cmd_mount }, + { "rename", be_cmd_rename }, + { "unjail", be_cmd_unjail }, + { "unmount", be_cmd_unmount }, +}; + +static int +get_cmd_index(char *cmd, int *index) +{ + int map_size = sizeof(command_map) / sizeof(struct command_map_entry); + + for (int i = 0; i < map_size; ++i) { + if (strcmp(cmd, command_map[i].command) == 0) { + *index = i; + return (0); + } + } + + return (1); +} + + +static int +be_cmd_activate(int argc, char *argv[]) +{ + int err, opt; + bool temp; + char *bootenv; + + temp = false; + while ((opt = getopt(argc, argv, "t")) != -1) { + switch (opt) { + case 't': + temp = true; + break; + default: + fprintf(stderr, "be activate: unknown option '-%c'\n", + optopt); + return (usage(false)); + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) { + fprintf(stderr, "be activate: wrong number of arguments\n"); + return (usage(false)); + } + + + /* activate logic goes here */ + if ((err = be_activate(be, argv[0], temp)) != 0) { + // TODO: more specific error msg based on err + printf("did not successfully activate boot environment %s\n", + argv[0]); + } else { + printf("successfully activated boot environment %s\n", argv[0]); + } + + if (temp) { + printf("for next boot\n"); + } + + return (err); +} + + +// TODO: when only one arg is given, and it contains an "@" the this should +// create that snapshot +static int +be_cmd_create(int argc, char *argv[]) +{ + int err, opt; + char *snapname; + char *bootenv; + char *source; + + snapname = NULL; + while ((opt = getopt(argc, argv, "e:")) != -1) { + switch (opt) { + case 'e': + snapname = optarg; + break; + default: + fprintf(stderr, "be create: unknown option '-%c'\n", + optopt); + return (usage(false)); + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) { + fprintf(stderr, "be create: wrong number of arguments\n"); + return (usage(false)); + } + + bootenv = *argv; + + + if (snapname != NULL) { + if (strchr(snapname, '@') != NULL) { + err = be_create_from_existing_snap(be, bootenv, + snapname); + } else { + err = be_create_from_existing(be, bootenv, snapname); + } + } else { + if ((snapname = strchr(bootenv, '@')) != NULL) { + *(snapname++) = '\0'; + if ((err = be_snapshot(be, (char *)be_active_path(be), + snapname, true, NULL)) != BE_ERR_SUCCESS) { + fprintf(stderr, "failed to create snapshot\n"); + } + asprintf(&source, "%s@%s", be_active_path(be), snapname); + err = be_create_from_existing_snap(be, bootenv, + source); + return (err); + } else { + err = be_create(be, bootenv); + } + } + + switch (err) { + case BE_ERR_SUCCESS: + break; + default: + if (snapname == NULL) { + fprintf(stderr, + "failed to create bootenv %s\n", bootenv); + } else { + fprintf(stderr, + "failed to create bootenv %s from snapshot %s\n", + bootenv, snapname); + } + } + + return (err); +} + + +static int +be_cmd_export(int argc, char *argv[]) +{ + int opt; + char *bootenv; + + + if (argc == 1) { + fprintf(stderr, "be export: missing boot environment name\n"); + return (usage(false)); + } + + if (argc > 2) { + fprintf(stderr, "be export: extra arguments provided\n"); + return (usage(false)); + } + + bootenv = argv[1]; + + if (isatty(STDOUT_FILENO)) { + fprintf(stderr, "be export: must redirect output\n"); + return (EX_USAGE); + } + + be_export(be, bootenv, STDOUT_FILENO); + + return (0); +} + + +static int +be_cmd_import(int argc, char *argv[]) +{ + char *bootenv; + int err; + + + if (argc == 1) { + fprintf(stderr, "be import: missing boot environment name\n"); + return (usage(false)); + } + + + if (argc > 2) { + fprintf(stderr, "be import: extra arguments provided\n"); + return (usage(false)); + } + + bootenv = argv[1]; + + if (isatty(STDIN_FILENO)) { + fprintf(stderr, "be import: input can not be from terminal\n"); + return (EX_USAGE); + } + + err = be_import(be, bootenv, STDIN_FILENO); + + return (err); +} + + +static int +be_cmd_add(int argc, char *argv[]) +{ + char *bootenv; + + if (argc < 2) { + fprintf(stderr, "be add: must provide at least one path\n"); + return (usage(false)); + } + + for (int i = 1; i < argc; ++i) { + printf("arg %d: %s\n", i, argv[i]); + // TODO catch err + be_add_child(be, argv[i], true); + } + + return (0); +} + + +static int +be_cmd_destroy(int argc, char *argv[]) +{ + int opt, err; + bool force; + char *target; + + force = false; + while ((opt = getopt(argc, argv, "F")) != -1) { + switch (opt) { + case 'F': + force = true; + break; + default: + fprintf(stderr, "be destroy: unknown option '-%c'\n", + optopt); + return (usage(false)); + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) { + fprintf(stderr, "be destroy: wrong number of arguments\n"); + return (usage(false)); + } + + target = argv[0]; + + err = be_destroy(be, target, force); + + return (err); +} + + +static int +be_cmd_jail(int argc, char *argv[]) +{ + char *bootenv; + char mnt_loc[BE_MAXPATHLEN]; + char buf[BE_MAXPATHLEN*2]; + int err, jid; + + //struct jail be_jail = { 0 }; + + if (argc == 1) { + fprintf(stderr, "be jail: missing boot environment name\n"); + return (usage(false)); + } + if (argc > 2) { + fprintf(stderr, "be jail: too many arguments\n"); + return (usage(false)); + } + + bootenv = argv[1]; + + // TODO: if its already mounted, perhaps there should be a flag to + // indicate its okay to proceed?? + if ((err = be_mount(be, bootenv, NULL, 0, mnt_loc)) != BE_ERR_SUCCESS) { + fprintf(stderr, "could not mount bootenv\n"); + } + + // NOTE: this is not quite functional: + // see https://github.com/vermaden/beadm/blob/master/HOWTO.htm on + // neccesary modifications to correctly boot the jail + + //snprintf(buf, BE_MAXPATHLEN*2, "jail %s %s %s /bin/sh /etc/rc", mnt_loc, bootenv, "192.168.1.123"); + snprintf(buf, BE_MAXPATHLEN*2, "jail %s %s %s /bin/sh", mnt_loc, + bootenv, "192.168.1.123"); + system(buf); + + unmount(mnt_loc, 0); + + /* + * be_jail.version = JAIL_API_VERSION; + * be_jail.path = "/tmp/be_mount.hCCk"; + * be_jail.jailname = "sdfs"; + * + * if ((jid = jail(&be_jail)) != -1) { + * printf("jail %d created at %s\n", jid, mnt_loc); + * err = 0; + * } else { + * fprintf(stderr, "unable to create jail. error: %d\n", errno); + * err = errno; + * } + */ + + return (0); +} + + +static int +be_cmd_list(int argc, char *argv[]) +{ + int opt; + bool show_all_datasets, show_space, hide_headers, show_snaps; + char *bootenv; + nvlist_t *props; + + show_all_datasets = show_space = hide_headers = show_snaps = false; + while ((opt = getopt(argc, argv, "aDHs")) != -1) { + switch (opt) { + case 'a': + show_all_datasets = true; + break; + case 'D': + show_space = true; + break; + case 'H': + hide_headers = true; + break; + case 's': + show_space = true; + break; + default: + fprintf(stderr, "be list: unknown option '-%c'\n", + optopt); + return (usage(false)); + } + } + + argc -= optind; + + if (argc != 0) { + fprintf(stderr, "be list: extra argument provided\n"); + return (usage(false)); + } + + //props = be_get_bootenv_props(be); + + return (0); +} + + +static int +be_cmd_mount(int argc, char *argv[]) +{ + int err; + char result_loc[BE_MAXPATHLEN]; + char *bootenv; + char *mountpoint; + + if (argc < 2) { + fprintf(stderr, "be mount: missing argument(s)\n"); + return (usage(false)); + } + + if (argc > 3) { + fprintf(stderr, "be mount: too many arguments\n"); + return (usage(false)); + } + + bootenv = argv[1]; + mountpoint = ((argc == 3) ? argv[2] : NULL); + + + err = be_mount(be, bootenv, mountpoint, 0, result_loc); + + switch (err) { + case BE_ERR_SUCCESS: + printf("successfully mounted %s at %s\n", bootenv, result_loc); + break; + default: + fprintf(stderr, + (argc == 3) ? "failed to mount bootenv %s at %s\n" : + "failed to mount bootenv %s at temporary path %s\n", + bootenv, mountpoint); + } + + return (err); +} + + +static int +be_cmd_rename(int argc, char *argv[]) +{ + char *src; + char *dest; + int err; + + if (argc < 3) { + fprintf(stderr, "be rename: missing argument\n"); + return (usage(false)); + } + + if (argc > 3) { + fprintf(stderr, "be rename: too many arguments\n"); + return (usage(false)); + } + + src = argv[1]; + dest = argv[2]; + + err = be_rename(be, src, dest); + + switch (err) { + case BE_ERR_SUCCESS: + break; + default: + fprintf(stderr, "failed to rename bootenv %s to %s\n", + src, dest); + } + + return (0); +} + + +static int +be_cmd_unjail(int argc, char *argv[]) +{ + int opt; + char *cmd, *target; + bool force; + + /* Store alias used */ + cmd = argv[0]; + + force = false; + while ((opt = getopt(argc, argv, "f")) != -1) { + switch (opt) { + case 'f': + force = true; + break; + default: + fprintf(stderr, "be %s: unknown option '-%c'\n", + cmd, optopt); + return (usage(false)); + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) { + fprintf(stderr, "be %s: wrong number of arguments\n", cmd); + return (usage(false)); + } + + target = argv[0]; + + /* unjail logic goes here */ + + return (0); +} + + +static int +be_cmd_unmount(int argc, char *argv[]) +{ + int err, flags, opt; + char *cmd, *bootenv; + + /* Store alias used */ + cmd = argv[0]; + + flags = 0; + while ((opt = getopt(argc, argv, "f")) != -1) { + switch (opt) { + case 'f': + flags |= BE_MNT_FORCE; + break; + default: + fprintf(stderr, "be %s: unknown option '-%c'\n", + cmd, optopt); + return (usage(false)); + } + } + + argc -= optind; + argv += optind; + + if (argc != 1) { + fprintf(stderr, "be %s: wrong number of arguments\n", cmd); + return (usage(false)); + } + + bootenv = argv[0]; + + err = be_unmount(be, bootenv, flags); + + switch (err) { + case BE_ERR_SUCCESS: + break; + default: + fprintf(stderr, "failed to unmount bootenv %s\n", bootenv); + } + + return (err); +} + + +int +main(int argc, char *argv[]) +{ + char *command; + int command_index, rc; + + if (argc < 2) { + fprintf(stderr, "missing command\n"); + return (usage(false)); + } + + command = argv[1]; + + /* Handle command aliases */ + if (strcmp(command, "umount") == 0) { + command = "unmount"; + } + + if (strcmp(command, "ujail") == 0) { + command = "unjail"; + } + + if ((strcmp(command, "-?") == 0) || (strcmp(command, "-h") == 0)) { + return (usage(true)); + } + + if (get_cmd_index(command, &command_index)) { + fprintf(stderr, "unknown command: %s\n", command); + return (usage(false)); + } + + + if ((be = libbe_init()) == NULL) { + return (-1); + } + + libbe_print_on_error(be, true); + + /* TODO: can be simplified if offset by 2 instead of one */ + rc = command_map[command_index].fn(argc-1, argv+1); + + libbe_close(be); + + + return (rc); +}
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201807241321.w6ODLifY030817>