diff options
Diffstat (limited to '')
-rw-r--r-- | src/postmulti/postmulti.c | 1843 |
1 files changed, 1843 insertions, 0 deletions
diff --git a/src/postmulti/postmulti.c b/src/postmulti/postmulti.c new file mode 100644 index 0000000..5adcd27 --- /dev/null +++ b/src/postmulti/postmulti.c @@ -0,0 +1,1843 @@ +/*++ +/* NAME +/* postmulti 1 +/* SUMMARY +/* Postfix multi-instance manager +/* SYNOPSIS +/* .fi +/* .ti -4 +/* \fBEnabling multi-instance management:\fR +/* +/* \fBpostmulti\fR \fB-e init\fR [\fB-v\fR] +/* +/* .ti -4 +/* \fBIterator mode:\fR +/* +/* \fBpostmulti\fR \fB-l\fR [\fB-aRv\fR] [\fB-g \fIgroup\fR] +/* [\fB-i \fIname\fR] +/* +/* \fBpostmulti\fR \fB-p\fR [\fB-av\fR] [\fB-g \fIgroup\fR] +/* [\fB-i \fIname\fR] \fIpostfix-command...\fR +/* +/* \fBpostmulti\fR \fB-x\fR [\fB-aRv\fR] [\fB-g \fIgroup\fR] +/* [\fB-i \fIname\fR] \fIunix-command...\fR +/* +/* .ti -4 +/* \fBLife-cycle management:\fR +/* +/* \fBpostmulti\fR \fB-e create\fR [\fB-av\fR] +/* [\fB-g \fIgroup\fR] [\fB-i \fIname\fR] [\fB-G \fIgroup\fR] +/* [\fB-I \fIname\fR] [\fIparam=value\fR ...] +/* +/* \fBpostmulti\fR \fB-e import\fR [\fB-av\fR] +/* [\fB-g \fIgroup\fR] [\fB-i \fIname\fR] [\fB-G \fIgroup\fR] +/* [\fB-I \fIname\fR] [\fBconfig_directory=\fI/path\fR] +/* +/* \fBpostmulti\fR \fB-e destroy\fR [\fB-v\fR] \fB-i \fIname\fR +/* +/* \fBpostmulti\fR \fB-e deport\fR [\fB-v\fR] \fB-i \fIname\fR +/* +/* \fBpostmulti\fR \fB-e enable\fR [\fB-v\fR] \fB-i \fIname\fR +/* +/* \fBpostmulti\fR \fB-e disable\fR [\fB-v\fR] \fB-i \fIname\fR +/* +/* \fBpostmulti\fR \fB-e assign\fR [\fB-v\fR] \fB-i \fIname\fR +/* [\fB-I \fIname\fR] [-G \fIgroup\fR] +/* DESCRIPTION +/* The \fBpostmulti\fR(1) command allows a Postfix administrator +/* to manage multiple Postfix instances on a single host. +/* +/* \fBpostmulti\fR(1) implements two fundamental modes of +/* operation. In \fBiterator\fR mode, it executes the same +/* command for multiple Postfix instances. In \fBlife-cycle +/* management\fR mode, it adds or deletes one instance, or +/* changes the multi-instance status of one instance. +/* +/* Each mode of operation has its own command syntax. For this +/* reason, each mode is documented in separate sections below. +/* BACKGROUND +/* .ad +/* .fi +/* A multi-instance configuration consists of one primary +/* Postfix instance, and one or more secondary instances whose +/* configuration directory pathnames are recorded in the primary +/* instance's main.cf file. Postfix instances share program +/* files and documentation, but have their own configuration, +/* queue and data directories. +/* +/* Currently, only the default Postfix instance can be used +/* as primary instance in a multi-instance configuration. The +/* \fBpostmulti\fR(1) command does not currently support a \fB-c\fR +/* option to select an alternative primary instance, and exits +/* with a fatal error if the \fBMAIL_CONFIG\fR environment +/* variable is set to a non-default configuration directory. +/* +/* See the MULTI_INSTANCE_README tutorial for a more detailed +/* discussion of multi-instance management with \fBpostmulti\fR(1). +/* ITERATOR MODE +/* .ad +/* .fi +/* In iterator mode, \fBpostmulti\fR performs the same operation +/* on all Postfix instances in turn. +/* +/* If multi-instance support is not enabled, the requested +/* command is performed just for the primary instance. +/* .PP +/* Iterator mode implements the following command options: +/* .SH "Instance selection" +/* .IP \fB-a\fR +/* Perform the operation on all instances. This is the default. +/* .IP "\fB-g \fIgroup\fR" +/* Perform the operation only for members of the named \fIgroup\fR. +/* .IP "\fB-i \fIname\fR" +/* Perform the operation only for the instance with the specified +/* \fIname\fR. You can specify either the instance name +/* or the absolute pathname of the instance's configuration +/* directory. Specify "-" to select the primary Postfix instance. +/* .IP \fB-R\fR +/* Reverse the iteration order. This may be appropriate when +/* updating a multi-instance system, where "sink" instances +/* are started before "source" instances. +/* .sp +/* This option cannot be used with \fB-p\fR. +/* .SH "List mode" +/* .IP \fB-l\fR +/* List Postfix instances with their instance name, instance +/* group name, enable/disable status and configuration directory. +/* .SH "Postfix-wrapper mode" +/* .IP "\fB-p \fIpostfix-command\fR" +/* Invoke \fBpostfix(1)\fR to execute \fIpostfix-command\fR. +/* This option implements the \fBpostfix-wrapper\fR(5) interface. +/* .RS +/* .IP \(bu +/* With "start"-like commands, "postfix check" is executed for +/* instances that are not enabled. The full list of commands +/* is specified with the postmulti_start_commands parameter. +/* .IP \(bu +/* With "stop"-like commands, the iteration order is reversed, +/* and disabled instances are skipped. The full list of commands +/* is specified with the postmulti_stop_commands parameter. +/* .IP \(bu +/* With "reload" and other commands that require a started +/* instance, disabled instances are skipped. The full list of +/* commands is specified with the postmulti_control_commands +/* parameter. +/* .IP \(bu +/* With "status" and other commands that don't require a started +/* instance, the command is executed for all instances. +/* .RE +/* .IP +/* The \fB-p\fR option can also be used interactively to +/* start/stop/etc. a named instance or instance group. For +/* example, to start just the instances in the group "msa", +/* invoke \fBpostmulti\fR(1) as follows: +/* .RS +/* .IP +/* # postmulti -g msa -p start +/* .RE +/* .SH "Command mode" +/* .IP "\fB-x \fIunix-command\fR" +/* Execute the specified \fIunix-command\fR for all Postfix instances. +/* The command runs with appropriate environment settings for +/* MAIL_CONFIG, command_directory, daemon_directory, +/* config_directory, queue_directory, data_directory, +/* multi_instance_name, multi_instance_group and +/* multi_instance_enable. +/* .SH "Other options" +/* .IP \fB-v\fR +/* Enable verbose logging for debugging purposes. Multiple +/* \fB-v\fR options make the software increasingly verbose. +/* LIFE-CYCLE MANAGEMENT MODE +/* .ad +/* .fi +/* With the \fB-e\fR option \fBpostmulti\fR(1) can be used to +/* add or delete a Postfix instance, and to manage the +/* multi-instance status of an existing instance. +/* .PP +/* The following options are implemented: +/* .SH "Existing instance selection" +/* .IP \fB-a\fR +/* When creating or importing an instance, place the new +/* instance at the front of the secondary instance list. +/* .IP "\fB-g \fIgroup\fR" +/* When creating or importing an instance, place the new +/* instance before the first secondary instance that is a +/* member of the specified group. +/* .IP "\fB-i \fIname\fR" +/* When creating or importing an instance, place the new +/* instance before the matching secondary instance. +/* .sp +/* With other life-cycle operations, apply the operation to +/* the named existing instance. Specify "-" to select the +/* primary Postfix instance. +/* .SH "New or existing instance name assignment" +/* .IP "\fB-I \fIname\fR" +/* Assign the specified instance \fIname\fR to an existing +/* instance, newly-created instance, or imported instance. +/* Instance +/* names other than "-" (which makes the instance "nameless") +/* must start with "postfix-". This restriction reduces the +/* likelihood of name collisions with system files. +/* .IP "\fB-G \fIgroup\fR" +/* Assign the specified \fIgroup\fR name to an existing instance +/* or to a newly created or imported instance. +/* .SH "Instance creation/deletion/status change" +/* .IP "\fB-e \fIaction\fR" +/* "Edit" managed instances. The following actions are supported: +/* .RS +/* .IP \fBinit\fR +/* This command is required before \fBpostmulti\fR(1) can be +/* used to manage Postfix instances. The "postmulti -e init" +/* command updates the primary instance's main.cf file by +/* setting: +/* .RS +/* .IP +/* .nf +/* multi_instance_wrapper = +/* ${command_directory}/postmulti -p -- +/* multi_instance_enable = yes +/* .fi +/* .RE +/* .IP +/* You can set these by other means if you prefer. +/* .IP \fBcreate\fR +/* Create a new Postfix instance and add it to the +/* multi_instance_directories parameter of the primary instance. +/* The "\fB-I \fIname\fR" option is recommended to give the +/* instance a short name that is used to construct default +/* values for the private directories of the new instance. The +/* "\fB-G \fIgroup\fR" option may be specified to assign the +/* instance to a group, otherwise, the new instance is not a +/* member of any group. +/* .sp +/* The new instance main.cf is the stock main.cf with the +/* parameters that specify the locations of shared files cloned +/* from the primary instance. For "nameless" instances, you +/* should manually adjust "syslog_name" to yield a unique +/* "logtag" starting with "postfix-" that will uniquely identify +/* the instance in the mail logs. It is simpler to assign the +/* instance a short name with the "\fB-I \fIname\fR" option. +/* .sp +/* Optional "name=value" arguments specify the instance +/* config_directory, queue_directory and data_directory. +/* For example: +/* .RS +/* .IP +/* .nf +/* # postmulti -I postfix-mumble \e +/* -G mygroup -e create \e +/* config_directory=/my/config/dir \e +/* queue_directory=/my/queue/dir \e +/* data_directory=/my/data/dir +/* .fi +/* .RE +/* .IP +/* If any of these pathnames is not supplied, the program +/* attempts to generate the missing pathname(s) by taking the +/* corresponding primary instance pathname, and replacing the +/* last pathname component by the value of the \fB-I\fR option. +/* .sp +/* If the instance configuration directory already exists, and +/* contains both a main.cf and master.cf file, \fBcreate\fR +/* will "import" the instance as-is. For existing instances, +/* \fBcreate\fR and \fBimport\fR are identical. +/* .IP \fBimport\fR +/* Import an existing instance into the list of instances +/* managed by the \fBpostmulti\fR(1) multi-instance manager. +/* This adds the instance to the multi_instance_directories +/* list of the primary instance. If the "\fB-I \fIname\fR" +/* option is provided it specifies the new name for the instance +/* and is used to define a default location for the instance +/* configuration directory (as with \fBcreate\fR above). The +/* "\fB-G \fIgroup\fR" option may be used to assign the instance +/* to a group. Add a "\fBconfig_directory=\fI/path\fR" argument +/* to override a default pathname based on "\fB-I \fIname\fR". +/* .IP \fBdestroy\fR +/* Destroy a secondary Postfix instance. To be a candidate for +/* destruction an instance must be disabled, stopped and its +/* queue must not contain any messages. Attempts to destroy +/* the primary Postfix instance trigger a fatal error, without +/* destroying the instance. +/* .sp +/* The instance is removed from the primary instance main.cf +/* file's alternate_config_directories parameter and its data, +/* queue and configuration directories are cleaned of files +/* and directories created by the Postfix system. The main.cf +/* and master.cf files are removed from the configuration +/* directory even if they have been modified since initial +/* creation. Finally, the instance is "deported" from the list +/* of managed instances. +/* .sp +/* If other files are present in instance private directories, +/* the directories may not be fully removed, a warning is +/* logged to alert the administrator. It is expected that an +/* instance built using "fresh" directories via the \fBcreate\fR +/* action will be fully removed by the \fBdestroy\fR action +/* (if first disabled). If the instance configuration and queue +/* directories are populated with additional files (access and +/* rewriting tables, chroot jail content, etc.) the instance +/* directories will not be fully removed. +/* .sp +/* The \fBdestroy\fR action triggers potentially dangerous +/* file removal operations. Make sure the instance's data, +/* queue and configuration directories are set correctly and +/* do not contain any valuable files. +/* .IP \fBdeport\fR +/* Deport a secondary instance from the list of managed +/* instances. This deletes the instance configuration directory +/* from the primary instance's multi_instance_directories list, +/* but does not remove any files or directories. +/* .IP \fBassign\fR +/* Assign a new instance name or a new group name to the +/* selected instance. Use "\fB-G -\fR" to specify "no group" +/* and "\fB-I -\fR" to specify "no name". If you choose to +/* make an instance "nameless", set a suitable syslog_name in +/* the corresponding main.cf file. +/* .IP \fBenable\fR +/* Mark the selected instance as enabled. This just sets the +/* multi_instance_enable parameter to "yes" in the instance's +/* main.cf file. +/* .IP \fBdisable\fR +/* Mark the selected instance as disabled. This means that +/* the instance will not be started etc. with "postfix start", +/* "postmulti -p start" and so on. The instance can still be +/* started etc. with "postfix -c config-directory start". +/* .SH "Other options" +/* .IP \fB-v\fR +/* Enable verbose logging for debugging purposes. Multiple +/* \fB-v\fR options make the software increasingly verbose. +/* .RE +/* ENVIRONMENT +/* .ad +/* .fi +/* The \fBpostmulti\fR(1) command exports the following environment +/* variables before executing the requested \fIcommand\fR for a given +/* instance: +/* .IP \fBMAIL_VERBOSE\fR +/* This is set when the -v command-line option is present. +/* .IP \fBMAIL_CONFIG\fR +/* The location of the configuration directory of the instance. +/* CONFIGURATION PARAMETERS +/* .ad +/* .fi +/* .IP "\fBconfig_directory (see 'postconf -d' output)\fR" +/* The default location of the Postfix main.cf and master.cf +/* configuration files. +/* .IP "\fBdaemon_directory (see 'postconf -d' output)\fR" +/* The directory with Postfix support programs and daemon programs. +/* .IP "\fBimport_environment (see 'postconf -d' output)\fR" +/* The list of environment variables that a privileged Postfix +/* process will import from a non-Postfix parent process, or name=value +/* environment overrides. +/* .IP "\fBmulti_instance_directories (empty)\fR" +/* An optional list of non-default Postfix configuration directories; +/* these directories belong to additional Postfix instances that share +/* the Postfix executable files and documentation with the default +/* Postfix instance, and that are started, stopped, etc., together +/* with the default Postfix instance. +/* .IP "\fBmulti_instance_group (empty)\fR" +/* The optional instance group name of this Postfix instance. +/* .IP "\fBmulti_instance_name (empty)\fR" +/* The optional instance name of this Postfix instance. +/* .IP "\fBmulti_instance_enable (no)\fR" +/* Allow this Postfix instance to be started, stopped, etc., by a +/* multi-instance manager. +/* .IP "\fBpostmulti_start_commands (start)\fR" +/* The \fBpostfix\fR(1) commands that the \fBpostmulti\fR(1) instance manager treats +/* as "start" commands. +/* .IP "\fBpostmulti_stop_commands (see 'postconf -d' output)\fR" +/* The \fBpostfix\fR(1) commands that the \fBpostmulti\fR(1) instance manager treats +/* as "stop" commands. +/* .IP "\fBpostmulti_control_commands (reload flush)\fR" +/* The \fBpostfix\fR(1) commands that the \fBpostmulti\fR(1) instance manager +/* treats as "control" commands, that operate on running instances. +/* .IP "\fBsyslog_facility (mail)\fR" +/* The syslog facility of Postfix logging. +/* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" +/* A prefix that is prepended to the process name in syslog +/* records, so that, for example, "smtpd" becomes "prefix/smtpd". +/* .PP +/* Available in Postfix 3.0 and later: +/* .IP "\fBmeta_directory (see 'postconf -d' output)\fR" +/* The location of non-executable files that are shared among +/* multiple Postfix instances, such as postfix-files, dynamicmaps.cf, +/* and the multi-instance template files main.cf.proto and master.cf.proto. +/* .IP "\fBshlib_directory (see 'postconf -d' output)\fR" +/* The location of Postfix dynamically-linked libraries +/* (libpostfix-*.so), and the default location of Postfix database +/* plugins (postfix-*.so) that have a relative pathname in the +/* dynamicmaps.cf file. +/* FILES +/* $meta_directory/main.cf.proto, stock configuration file +/* $meta_directory/master.cf.proto, stock configuration file +/* $daemon_directory/postmulti-script, life-cycle helper program +/* SEE ALSO +/* postfix(1), Postfix control program +/* postfix-wrapper(5), Postfix multi-instance API +/* README FILES +/* .ad +/* .fi +/* Use "\fBpostconf readme_directory\fR" or "\fBpostconf +/* html_directory\fR" to locate this information. +/* .nf +/* .na +/* MULTI_INSTANCE_README, Postfix multi-instance management +/* HISTORY +/* .ad +/* .fi +/* The \fBpostmulti\fR(1) command was introduced with Postfix +/* version 2.6. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Victor Duchovni +/* Morgan Stanley +/* +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <vstream.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <fcntl.h> +#include <errno.h> +#include <ctype.h> +#ifdef USE_PATHS_H +#include <paths.h> +#endif +#include <stddef.h> + +/* Utility library. */ + +#include <msg.h> +#include <msg_vstream.h> +#include <vstream.h> +#include <vstring_vstream.h> +#include <stringops.h> +#include <clean_env.h> +#include <argv.h> +#include <safe.h> +#include <mymalloc.h> +#include <htable.h> +#include <name_code.h> +#include <ring.h> +#include <warn_stat.h> + +/* Global library. */ + +#include <mail_version.h> +#include <mail_params.h> +#include <mail_conf.h> +#include <mail_parm_split.h> +#include <maillog_client.h> + +/* Application-specific. */ + + /* + * Configuration parameters, specific to postmulti(1). + */ +char *var_multi_start_cmds; +char *var_multi_stop_cmds; +char *var_multi_cntrl_cmds; + + /* + * Shared directory pathnames. + */ +typedef struct { + const char *param_name; + char **param_value; +} SHARED_PATH; + +static SHARED_PATH shared_dir_table[] = { + VAR_COMMAND_DIR, &var_command_dir, + VAR_DAEMON_DIR, &var_daemon_dir, + VAR_META_DIR, &var_meta_dir, + VAR_SHLIB_DIR, &var_shlib_dir, + 0, +}; + + /* + * Actions. + */ +#define ITER_CMD_POSTFIX (1<<0) /* postfix(1) iterator mode */ +#define ITER_CMD_LIST (1<<1) /* listing iterator mode */ +#define ITER_CMD_GENERIC (1<<2) /* generic command iterator mode */ + +#define ITER_CMD_MASK_ALL \ + (ITER_CMD_POSTFIX | ITER_CMD_LIST | ITER_CMD_GENERIC) + +#define EDIT_CMD_CREATE (1<<4) /* create new instance */ +#define EDIT_CMD_IMPORT (1<<5) /* import existing instance */ +#define EDIT_CMD_DESTROY (1<<6) /* destroy instance */ +#define EDIT_CMD_DEPORT (1<<7) /* export instance */ +#define EDIT_CMD_ENABLE (1<<8) /* enable start/stop */ +#define EDIT_CMD_DISABLE (1<<9) /* disable start/stop */ +#define EDIT_CMD_ASSIGN (1<<10) /* assign name/group */ +#define EDIT_CMD_INIT (1<<11) /* hook into main.cf */ + +#define EDIT_CMD_MASK_ADD (EDIT_CMD_CREATE | EDIT_CMD_IMPORT) +#define EDIT_CMD_MASK_DEL (EDIT_CMD_DESTROY | EDIT_CMD_DEPORT) +#define EDIT_CMD_MASK_ASSIGN (EDIT_CMD_MASK_ADD | EDIT_CMD_ASSIGN) +#define EDIT_CMD_MASK_ENB (EDIT_CMD_ENABLE | EDIT_CMD_DISABLE) +#define EDIT_CMD_MASK_ALL \ + (EDIT_CMD_MASK_ASSIGN | EDIT_CMD_MASK_DEL | EDIT_CMD_MASK_ENB | \ + EDIT_CMD_INIT) + + /* + * Edit command to number mapping, and vice versa. + */ +static NAME_CODE edit_command_table[] = { + "create", EDIT_CMD_CREATE, + "import", EDIT_CMD_IMPORT, + "destroy", EDIT_CMD_DESTROY, + "deport", EDIT_CMD_DEPORT, + "enable", EDIT_CMD_ENABLE, + "disable", EDIT_CMD_DISABLE, + "assign", EDIT_CMD_ASSIGN, + "init", EDIT_CMD_INIT, + 0, -1, +}; + +#define EDIT_CMD_CODE(str) \ + name_code(edit_command_table, NAME_CODE_FLAG_STRICT_CASE, (str)) +#define EDIT_CMD_STR(code) str_name_code(edit_command_table, (code)) + + /* + * Mandatory prefix for non-empty instance names. + */ +#ifndef NAME_PREFIX +#define NAME_PREFIX "postfix-" +#endif +#define HAS_NAME_PREFIX(name) \ + (strncmp((name), NAME_PREFIX, sizeof(NAME_PREFIX)-1) == 0) +#define NEED_NAME_PREFIX(name) \ + ((name) != 0 && strcmp((name), "-") != 0 && !HAS_NAME_PREFIX(name)) +#define NAME_SUFFIX(name) ((name) + sizeof(NAME_PREFIX) - 1) + + /* + * In-core instance structure. Only private information is kept here. + */ +typedef struct instance { + RING ring; /* linkage. */ + char *config_dir; /* private */ + char *queue_dir; /* private */ + char *data_dir; /* private */ + char *name; /* null or name */ + char *gname; /* null or group */ + int enabled; /* start/stop enable */ + int primary; /* special */ +} INSTANCE; + + /* + * Managed instance list (edit mode and iterator mode). + */ +static RING instance_hd[1]; /* instance list head */ + +#define RING_TO_INSTANCE(ring_ptr) RING_TO_APPL(ring_ptr, INSTANCE, ring) +#define RING_PTR_OF(x) (&((x)->ring)) + +#define FOREACH_INSTANCE(entry) \ + for ((entry) = instance_hd; \ + ((entry) = ring_succ(entry)) != instance_hd;) + +#define FOREACH_SECONDARY_INSTANCE(entry) \ + for ((entry) = ring_succ(instance_hd); \ + ((entry) = ring_succ(entry)) != instance_hd;) + +#define NEXT_ITERATOR_INSTANCE(flags, entry) \ + (((flags) & ITER_FLAG_REVERSE) ? ring_pred(entry) : ring_succ(entry)) + +#define FOREACH_ITERATOR_INSTANCE(flags, entry) \ + for ((entry) = instance_hd; \ + ((entry) = NEXT_ITERATOR_INSTANCE(flags, (entry))) != instance_hd;) + + /* + * Instance selection. One can either select all instances, select by + * instance name, or select by instance group. + */ +typedef struct { + int type; /* see below */ + char *name; /* undefined or name */ +} INST_SELECTION; + +#define INST_SEL_NONE 0 /* default: no selection */ +#define INST_SEL_ALL 1 /* select all instances */ +#define INST_SEL_NAME 2 /* select instance name */ +#define INST_SEL_GROUP 3 /* select instance group */ + + /* + * Instance name assignment. Each instance may be assigned an instance name + * (this must be globally unique within a multi-instance cluster) or an + * instance group name (this is intended to be shared). Externally, empty + * names may be represented as "-". Internally, we use "" only, to simplify + * the code. + */ +typedef struct { + char *name; /* null or assigned instance name */ + char *gname; /* null or assigned group name */ +} NAME_ASSIGNMENT; + + /* + * Iterator controls for non-edit commands. One can reverse the iteration + * order, or give special treatment to disabled instances. + */ +#define ITER_FLAG_DEFAULT 0 /* default setting */ +#define ITER_FLAG_REVERSE (1<<0) /* reverse iteration order */ +#define ITER_FLAG_CHECK_DISABLED (1<<1) /* check disabled instances */ +#define ITER_FLAG_SKIP_DISABLED (1<<2) /* skip disabled instances */ + + /* + * Environment export controls for edit commands. postmulti(1) exports only + * things that need to be updated. + */ +#define EXP_FLAG_MULTI_DIRS (1<<0) /* export multi_instance_directories */ +#define EXP_FLAG_MULTI_NAME (1<<1) /* export multi_instance_name */ +#define EXP_FLAG_MULTI_GROUP (1<<2) /* export multi_instance_group */ + + /* + * To detect conflicts, each instance name and each shared or private + * pathname is registered in one place, with its owner. Everyone must + * register their claims when they join, and will be rejected in case of + * conflict. + * + * Each claim value involves a parameter value (either a directory name or an + * instance name). Each claim owner is the config_directory pathname plus + * the parameter name. + * + * XXX: No multi.cf lock file, so this is not race-free. + */ +static HTABLE *claim_table; + +#define IS_CLAIMED_BY(name) \ + (claim_table ? htable_find(claim_table, (name)) : 0) + + /* + * Forward references. + */ +static int iterate_command(int, int, char **, INST_SELECTION *); +static int match_instance_selection(INSTANCE *, INST_SELECTION *); + + /* + * Convenience. + */ +#define INSTANCE_NAME(i) ((i)->name ? (i)->name : (i)->config_dir) +#define STR(buf) vstring_str(buf) + +/* register_claim - register claim or bust */ + +static void register_claim(const char *instance_path, const char *param_name, + const char *param_value) +{ + const char *myname = "register_claim"; + char *requestor; + const char *owner; + + /* + * Sanity checks. + */ + if (instance_path == 0 || *instance_path == 0) + msg_panic("%s: no or empty instance pathname", myname); + if (param_name == 0 || *param_name == 0) + msg_panic("%s: no or empty parameter name", myname); + if (param_value == 0) + msg_panic("%s: no parameter value", myname); + + /* + * Make a claim or report a conflict. + */ + if (claim_table == 0) + claim_table = htable_create(100); + requestor = concatenate(instance_path, ", ", param_name, (char *) 0); + if ((owner = htable_find(claim_table, param_value)) == 0) { + (void) htable_enter(claim_table, param_value, requestor); + } else if (strcmp(owner, requestor) == 0) { + myfree(requestor); + } else { + msg_fatal("instance %s, %s=%s conflicts with instance %s=%s", + instance_path, param_name, param_value, owner, param_value); + } +} + +/* claim_instance_attributes - claim multiple private instance attributes */ + +static void claim_instance_attributes(INSTANCE *ip) +{ + + /* + * Detect instance name or pathname conflicts between this instance and + * other instances. XXX: No multi.cf lock file, so this is not race-free. + */ + if (ip->name) + register_claim(ip->config_dir, VAR_MULTI_NAME, ip->name); + register_claim(ip->config_dir, VAR_CONFIG_DIR, ip->config_dir); + register_claim(ip->config_dir, VAR_QUEUE_DIR, ip->queue_dir); + register_claim(ip->config_dir, VAR_DATA_DIR, ip->data_dir); +} + +/* alloc_instance - allocate a single instance object */ + +static INSTANCE *alloc_instance(const char *config_dir) +{ + INSTANCE *ip = (INSTANCE *) mymalloc(sizeof(INSTANCE)); + + ring_init(RING_PTR_OF(ip)); + ip->config_dir = config_dir ? mystrdup(config_dir) : 0; + ip->queue_dir = 0; + ip->data_dir = 0; + ip->name = 0; + ip->gname = 0; + ip->enabled = 0; + ip->primary = 0; + + return (ip); +} + +#if 0 + +/* free_instance - free a single instance object */ + +static void free_instance(INSTANCE *ip) +{ + + /* + * If we continue after secondary main.cf file read error, we must be + * prepared for the case that some parameters may be missing. + */ + if (ip->name) + myfree(ip->name); + if (ip->gname) + myfree(ip->gname); + if (ip->config_dir) + myfree(ip->config_dir); + if (ip->queue_dir) + myfree(ip->queue_dir); + if (ip->data_dir) + myfree(ip->data_dir); + myfree((void *) ip); +} + +#endif + +/* insert_instance - insert instance before selected location, claim names */ + +static void insert_instance(INSTANCE *ip, INST_SELECTION *selection) +{ + RING *old; + +#define append_instance(ip) insert_instance((ip), (INST_SELECTION *) 0) + + /* + * Insert instance before the selected site. + */ + claim_instance_attributes(ip); + if (ring_succ(instance_hd) == 0) + ring_init(instance_hd); + if (selection && selection->type != INST_SEL_NONE) { + FOREACH_SECONDARY_INSTANCE(old) { + if (match_instance_selection(RING_TO_INSTANCE(old), selection)) { + ring_prepend(old, RING_PTR_OF(ip)); + return; + } + } + if (selection->type != INST_SEL_ALL) + msg_fatal("No matching secondary instances"); + } + ring_prepend(instance_hd, RING_PTR_OF(ip)); +} + +/* create_primary_instance - synthetic entry for primary instance */ + +static INSTANCE *create_primary_instance(void) +{ + INSTANCE *ip = alloc_instance(var_config_dir); + + /* + * There is no need to load primary instance parameter settings from + * file. We already have the main.cf parameters of interest in memory. + */ +#define SAVE_INSTANCE_NAME(val) (*(val) ? mystrdup(val) : 0) + + ip->name = SAVE_INSTANCE_NAME(var_multi_name); + ip->gname = SAVE_INSTANCE_NAME(var_multi_group); + ip->enabled = var_multi_enable; + ip->queue_dir = mystrdup(var_queue_dir); + ip->data_dir = mystrdup(var_data_dir); + ip->primary = 1; + return (ip); +} + +/* load_instance - read instance parameters from config_dir/main.cf */ + +static INSTANCE *load_instance(INSTANCE *ip) +{ + VSTREAM *pipe; + VSTRING *buf; + char *name; + char *value; + ARGV *cmd; + int count = 0; + static NAME_CODE bool_code[] = { + CONFIG_BOOL_YES, 1, + CONFIG_BOOL_NO, 0, + 0, -1, + }; + + /* + * Expand parameter values in the context of the target main.cf file. + */ +#define REQUEST_PARAM_COUNT 5 /* # of requested parameters */ + + cmd = argv_alloc(REQUEST_PARAM_COUNT + 3); + name = concatenate(var_command_dir, "/", "postconf", (char *) 0); + argv_add(cmd, name, "-xc", ip->config_dir, + VAR_QUEUE_DIR, VAR_DATA_DIR, + VAR_MULTI_NAME, VAR_MULTI_GROUP, VAR_MULTI_ENABLE, + (char *) 0); + myfree(name); + pipe = vstream_popen(O_RDONLY, CA_VSTREAM_POPEN_ARGV(cmd->argv), + CA_VSTREAM_POPEN_END); + argv_free(cmd); + if (pipe == 0) + msg_fatal("Cannot parse %s/main.cf file: %m", ip->config_dir); + + /* + * Read parameter settings from postconf. See also comments below on + * whether we should continue or skip groups after error instead of + * bailing out immediately. + */ + buf = vstring_alloc(100); + while (vstring_get_nonl(buf, pipe) != VSTREAM_EOF) { + if (split_nameval(STR(buf), &name, &value)) + msg_fatal("Invalid %s/main.cf parameter: %s", + ip->config_dir, STR(buf)); + if (strcmp(name, VAR_QUEUE_DIR) == 0 && ++count) + ip->queue_dir = mystrdup(value); + else if (strcmp(name, VAR_DATA_DIR) == 0 && ++count) + ip->data_dir = mystrdup(value); + else if (strcmp(name, VAR_MULTI_NAME) == 0 && ++count) + ip->name = SAVE_INSTANCE_NAME(value); + else if (strcmp(name, VAR_MULTI_GROUP) == 0 && ++count) + ip->gname = SAVE_INSTANCE_NAME(value); + else if (strcmp(name, VAR_MULTI_ENABLE) == 0 && ++count) { + /* mail_conf_bool(3) is case insensitive! */ + ip->enabled = name_code(bool_code, NAME_CODE_FLAG_NONE, value); + if (ip->enabled < 0) + msg_fatal("Unexpected %s/main.cf entry: %s = %s", + ip->config_dir, VAR_MULTI_ENABLE, value); + } + } + vstring_free(buf); + + /* + * XXX We should not bail out while reading a bad secondary main.cf file. + * When we manage dozens or more instances, the likelihood increases that + * some file will be damaged or missing after a system crash. That is not + * a good reason to prevent undamaged Postfix instances from starting. + */ + if (count != REQUEST_PARAM_COUNT) + msg_fatal("Failed to obtain all required %s/main.cf parameters", + ip->config_dir); + + if (vstream_pclose(pipe)) + msg_fatal("Cannot parse %s/main.cf file", ip->config_dir); + return (ip); +} + +/* load_all_instances - compute list of Postfix instances */ + +static void load_all_instances(void) +{ + INSTANCE *primary_instance; + char **cpp; + ARGV *secondary_names; + + /* + * Avoid unexpected behavior when $multi_instance_directories contains + * only comma characters. Count the actual number of elements, before we + * decide that the list is empty. + */ + secondary_names = argv_split(var_multi_conf_dirs, CHARS_COMMA_SP); + + /* + * First, the primary instance. This is synthesized out of thin air. + */ + primary_instance = create_primary_instance(); + if (secondary_names->argc == 0) + primary_instance->enabled = 1; /* Single-instance mode */ + append_instance(primary_instance); + + /* + * Next, instances defined in $multi_instance_directories. Note: + * load_instance() has side effects on the global config dictionary, but + * this does not affect the values that have already been extracted into + * C variables. + */ + for (cpp = secondary_names->argv; *cpp != 0; cpp++) + append_instance(load_instance(alloc_instance(*cpp))); + + argv_free(secondary_names); +} + +/* match_instance_selection - match all/name/group constraints */ + +static int match_instance_selection(INSTANCE *ip, INST_SELECTION *selection) +{ + char *iname; + char *name; + + /* + * When selecting (rather than assigning names) an instance, we match by + * the instance name, config_directory path, or the instance name suffix + * (name without mandatory prefix). Selecting "-" selects the primary + * instance. + */ + switch (selection->type) { + case INST_SEL_NONE: + return (0); + case INST_SEL_ALL: + return (1); + case INST_SEL_GROUP: + return (ip->gname != 0 && strcmp(selection->name, ip->gname) == 0); + case INST_SEL_NAME: + name = selection->name; + if (*name == '/' || ip->name == 0) + iname = ip->config_dir; + else if (!HAS_NAME_PREFIX(name) && HAS_NAME_PREFIX(ip->name)) + iname = NAME_SUFFIX(ip->name); + else + iname = ip->name; + return (strcmp(name, iname) == 0 + || (ip->primary && strcmp(name, "-") == 0)); + default: + msg_panic("match_instance_selection: unknown selection type: %d", + selection->type); + } +} + +/* check_setenv - setenv() with extreme prejudice */ + +static void check_setenv(const char *name, const char *value) +{ +#define CLOBBER 1 + if (setenv(name, value, CLOBBER) < 0) + msg_fatal("setenv: %m"); +} + +/* prepend_command_path - prepend command_directory to PATH */ + +static void prepend_command_path(void) +{ + char *cmd_path; + + /* + * Carefully prepend "$command_directory:" to PATH. We can free the + * buffer after check_setenv(), since the value is copied there. + */ + cmd_path = safe_getenv("PATH"); + cmd_path = concatenate(var_command_dir, ":", (cmd_path && *cmd_path) ? + cmd_path : ROOT_PATH, (char *) 0); + check_setenv("PATH", cmd_path); + myfree(cmd_path); +} + +/* check_shared_dir_status - check and claim shared directories */ + +static void check_shared_dir_status(void) +{ + struct stat st; + const SHARED_PATH *sp; + + /* + * XXX Avoid false conflicts with meta_directory. This usually overlaps + * with other directories, typically config_directory, shlib_directory or + * daemon_directory. + */ + for (sp = shared_dir_table; sp->param_name; ++sp) { + if (sp->param_value[0][0] != '/') /* "no" or other special */ + continue; + if (stat(sp->param_value[0], &st) < 0) + msg_fatal("%s = '%s': directory not found: %m", + sp->param_name, sp->param_value[0]); + if (!S_ISDIR(st.st_mode)) + msg_fatal("%s = '%s' is not a directory", + sp->param_name, sp->param_value[0]); + if (strcmp(sp->param_name, VAR_META_DIR) == 0) + continue; + register_claim(var_config_dir, sp->param_name, sp->param_value[0]); + } +} + +/* check_safe_name - allow instance or group name with only "safe" characters */ + +static int check_safe_name(const char *s) +{ +#define SAFE_PUNCT "!@%-_=+:./" + if (*s == 0) + return (0); + for (; *s; ++s) { + if (!ISALNUM(*s) && !strchr(SAFE_PUNCT, *s)) + return (0); + } + return (1); +} + +/* check_name_assignments - Check validity of assigned instance or group name */ + +static void check_name_assignments(NAME_ASSIGNMENT *assignment) +{ + + /* + * Syntax check the assigned instance name. This name is also used to + * generate directory pathnames, so we must not allow "/" characters. + * + * The value "" will clear the name and is always valid. The command-line + * parser has already converted "-" into "", to simplify implementation. + */ + if (assignment->name && *assignment->name) { + if (!check_safe_name(assignment->name)) + msg_fatal("Unsafe characters in new instance name: '%s'", + assignment->name); + if (strchr(assignment->name, '/')) + msg_fatal("Illegal '/' character in new instance name: '%s'", + assignment->name); + if (NEED_NAME_PREFIX(assignment->name)) + msg_fatal("New instance name must start with '%s'", + NAME_PREFIX); + } + + /* + * Syntax check the assigned group name. + */ + if (assignment->gname && *assignment->gname) { + if (!check_safe_name(assignment->gname)) + msg_fatal("Unsafe characters in '-G %s'", assignment->gname); + } +} + +/* do_name_assignments - assign instance/group names */ + +static int do_name_assignments(INSTANCE *target, NAME_ASSIGNMENT *assignment) +{ + int export_flags = 0; + + /* + * The command-line parser has already converted "-" into "", to simplify + * implementation. + */ + if (assignment->name + && strcmp(assignment->name, target->name ? target->name : "")) { + register_claim(target->config_dir, VAR_MULTI_NAME, assignment->name); + if (target->name) + myfree(target->name); + target->name = SAVE_INSTANCE_NAME(assignment->name); + export_flags |= EXP_FLAG_MULTI_NAME; + } + if (assignment->gname + && strcmp(assignment->gname, target->gname ? target->gname : "")) { + if (target->gname) + myfree(target->gname); + target->gname = SAVE_INSTANCE_NAME(assignment->gname); + export_flags |= EXP_FLAG_MULTI_GROUP; + } + return (export_flags); +} + +/* make_private_path - generate secondary pathname using primary as template */ + +static char *make_private_path(const char *param_name, + const char *primary_value, + NAME_ASSIGNMENT *assignment) +{ + char *path; + char *base; + char *end; + + /* + * The command-line parser has already converted "-" into "", to simplify + * implementation. + */ + if (assignment->name == 0 || *assignment->name == 0) + msg_fatal("Missing %s parameter value", param_name); + + if (*primary_value != '/') + msg_fatal("Invalid default %s parameter value: '%s': " + "specify an absolute pathname", + param_name, primary_value); + + base = mystrdup(primary_value); + if ((end = strrchr(base, '/')) != 0) { + /* Drop trailing slashes */ + if (end[1] == '\0') { + while (--end > base && *end == '/') + *end = '\0'; + end = strrchr(base, '/'); + } + /* Drop last path component */ + while (end > base && *end == '/') + *end-- = '\0'; + } + path = concatenate(base[1] ? base : "", "/", + assignment->name, (char *) 0); + myfree(base); + return (path); +} + +/* assign_new_parameter - assign new instance private name=value */ + +static void assign_new_parameter(INSTANCE *new, int edit_cmd, + const char *arg) +{ + char *saved_arg; + char *name; + char *value; + char *end; + char **target = 0; + + /* + * With "import", only config_directory is specified on the command line + * (either explicitly as config_directory=/path/name, or implicitly as + * instance name). The other private directory pathnames are taken from + * the existing instance's main.cf file. + * + * With "create", all private pathname parameters are specified on the + * command line, or generated from an instance name. + */ + saved_arg = mystrdup(arg); + if (split_nameval(saved_arg, &name, &value)) + msg_fatal("Malformed parameter setting '%s'", arg); + + if (strcmp(VAR_CONFIG_DIR, name) == 0) { + target = &new->config_dir; + } else if (edit_cmd != EDIT_CMD_IMPORT) { + if (strcmp(VAR_QUEUE_DIR, name) == 0) { + target = &new->queue_dir; + } else if (strcmp(VAR_DATA_DIR, name) == 0) { + target = &new->data_dir; + } + } + if (target == 0) + msg_fatal("Parameter '%s' not valid with action %s", + name, EDIT_CMD_STR(edit_cmd)); + + /* + * Extract and assign the parameter value. We do a limited number of + * checks here. Conflicts between instances are checked by the caller. + * More checks may be implemented in the helper script if inspired. + */ + if (*value != '/') + msg_fatal("Parameter setting '%s' is not an absolute path", name); + + /* Tolerate+trim trailing "/" from readline completion */ + for (end = value + strlen(value) - 1; end > value && *end == '/'; --end) + *end = 0; + + /* No checks here for "/." or other shoot-foot silliness. */ + if (end == value) + msg_fatal("Parameter setting '%s' is the root directory", name); + + if (*target) + myfree(*target); + *target = mystrdup(value); + + /* + * Cleanup. + */ + myfree(saved_arg); +} + +/* assign_new_parameters - initialize new instance private parameters */ + +static void assign_new_parameters(INSTANCE *new, int edit_cmd, + char **argv, NAME_ASSIGNMENT *assignment) +{ + const char *owner; + + /* + * Sanity check the explicit parameter settings. More stringent checks + * may take place in the helper script. + */ + while (*argv) + assign_new_parameter(new, edit_cmd, *argv++); + + /* + * Initialize any missing private directory pathnames, using the primary + * configuration directory parameter values as a template, and using the + * assigned instance name to fill in the blanks. + * + * When importing an existing instance, load private directory pathnames + * from its main.cf file. + */ + if (new->config_dir == 0) + new->config_dir = + make_private_path(VAR_CONFIG_DIR, var_config_dir, assignment); + /* Needed for better-quality error message. */ + if ((owner = IS_CLAIMED_BY(new->config_dir)) != 0) + msg_fatal("new %s=%s is already in use by instance %s=%s", + VAR_CONFIG_DIR, new->config_dir, owner, new->config_dir); + if (edit_cmd != EDIT_CMD_IMPORT) { + if (new->queue_dir == 0) + new->queue_dir = + make_private_path(VAR_QUEUE_DIR, var_queue_dir, assignment); + if (new->data_dir == 0) + new->data_dir = + make_private_path(VAR_DATA_DIR, var_data_dir, assignment); + } else { + load_instance(new); + } +} + +/* export_helper_environment - update environment settings for helper command */ + +static void export_helper_environment(INSTANCE *target, int export_flags) +{ + ARGV *import_env; + VSTRING *multi_dirs; + const SHARED_PATH *sp; + RING *entry; + + /* + * Environment import filter, to enforce consistent behavior whether this + * command is started by hand, or at system boot time. This is necessary + * because some shell scripts use environment settings to override + * main.cf settings. + */ + import_env = mail_parm_split(VAR_IMPORT_ENVIRON, var_import_environ); + clean_env(import_env->argv); + argv_free(import_env); + + /* + * Prepend $command_directory: to PATH. This supposedly ensures that + * naive programs will execute commands from the right Postfix version. + */ + prepend_command_path(); + + /* + * The following ensures that Postfix's own programs will target the + * primary instance. + */ + check_setenv(CONF_ENV_PATH, var_config_dir); + + /* + * Export the parameter settings that are shared between instances. + */ + for (sp = shared_dir_table; sp->param_name; ++sp) + check_setenv(sp->param_name, sp->param_value[0]); + + /* + * Export the target instance's private directory locations. + */ + check_setenv(VAR_CONFIG_DIR, target->config_dir); + check_setenv(VAR_QUEUE_DIR, target->queue_dir); + check_setenv(VAR_DATA_DIR, target->data_dir); + + /* + * With operations that add or delete a secondary instance, we export the + * modified multi_instance_directories parameter value for the primary + * Postfix instance. + */ + if (export_flags & EXP_FLAG_MULTI_DIRS) { + multi_dirs = vstring_alloc(100); + FOREACH_SECONDARY_INSTANCE(entry) { + if (VSTRING_LEN(multi_dirs) > 0) + VSTRING_ADDCH(multi_dirs, ' '); + vstring_strcat(multi_dirs, RING_TO_INSTANCE(entry)->config_dir); + } + check_setenv(VAR_MULTI_CONF_DIRS, STR(multi_dirs)); + vstring_free(multi_dirs); + } + + /* + * Export updates for the instance name and group. Empty value (or no + * export) means don't update, "-" means clear. + */ + if (export_flags & EXP_FLAG_MULTI_NAME) + check_setenv(VAR_MULTI_NAME, target->name && *target->name ? + target->name : "-"); + + if (export_flags & EXP_FLAG_MULTI_GROUP) + check_setenv(VAR_MULTI_GROUP, target->gname && *target->gname ? + target->gname : "-"); + + /* + * If we would implement enable/disable commands by exporting the updated + * parameter value, then we could skip commands that have no effect, just + * like we can skip "assign" commands that make no change. + */ +} + +/* install_new_instance - install and return newly created instance */ + +static INSTANCE *install_new_instance(int edit_cmd, char **argv, + INST_SELECTION *selection, + NAME_ASSIGNMENT *assignment, + int *export_flags) +{ + INSTANCE *new; + + new = alloc_instance((char *) 0); + check_name_assignments(assignment); + assign_new_parameters(new, edit_cmd, argv, assignment); + *export_flags |= + (do_name_assignments(new, assignment) | EXP_FLAG_MULTI_DIRS); + insert_instance(new, selection); + return (new); +} + +/* update_instance - update existing instance, return export flags */ + +static int update_instance(INSTANCE *target, NAME_ASSIGNMENT *assignment) +{ + int export_flags; + + check_name_assignments(assignment); + export_flags = do_name_assignments(target, assignment); + return (export_flags); +} + +/* select_existing_instance - return instance selected for management */ + +static INSTANCE *select_existing_instance(INST_SELECTION *selection, + int unlink_flag, + int *export_flags) +{ + INSTANCE *selected = 0; + RING *entry; + INSTANCE *ip; + +#define DONT_UNLINK 0 +#define DO_UNLINK 1 + + if (selection->type != INST_SEL_NAME) + msg_fatal("Select an instance via '-i name'"); + + /* Find the selected instance and its predecessor */ + FOREACH_INSTANCE(entry) { + if (match_instance_selection(ip = RING_TO_INSTANCE(entry), selection)) { + selected = ip; + break; + } + } + + if (selected == 0) + msg_fatal("No instance named %s", selection->name); + + if (unlink_flag) { + /* Splice the target instance out of the list */ + if (ring_pred(entry) == instance_hd) + msg_fatal("Cannot remove the primary instance"); + if (selected->enabled) + msg_fatal("Cannot remove enabled instances"); + ring_detach(entry); + if (export_flags == 0) + msg_panic("select_existing_instance: no export flags"); + *export_flags |= EXP_FLAG_MULTI_DIRS; + } + return (selected); +} + +/* manage - create/destroy/... manage instances */ + +static NORETURN manage(int edit_cmd, int argc, char **argv, + INST_SELECTION *selection, + NAME_ASSIGNMENT *assignment) +{ + char *cmd; + INSTANCE *target; + int export_flags; + + /* + * Edit mode is not subject to iterator controls. + */ +#define NO_EXPORT_FLAGS ((int *) 0) + export_flags = 0; + + switch (edit_cmd) { + case EDIT_CMD_INIT: + target = create_primary_instance(); + break; + + case EDIT_CMD_CREATE: + case EDIT_CMD_IMPORT: + load_all_instances(); + target = install_new_instance(edit_cmd, argv, selection, + assignment, &export_flags); + break; + + case EDIT_CMD_ASSIGN: + load_all_instances(); + target = + select_existing_instance(selection, DONT_UNLINK, NO_EXPORT_FLAGS); + export_flags |= update_instance(target, assignment); + if (export_flags == 0) + exit(0); + break; + + case EDIT_CMD_DESTROY: + case EDIT_CMD_DEPORT: + load_all_instances(); + target = select_existing_instance(selection, DO_UNLINK, &export_flags); + break; + + default: + load_all_instances(); + target = + select_existing_instance(selection, DONT_UNLINK, NO_EXPORT_FLAGS); + break; + } + + /* + * Set up the helper script's process environment, and execute the helper + * script. + */ +#define HELPER "postmulti-script" + + export_helper_environment(target, export_flags); + cmd = concatenate(var_daemon_dir, "/" HELPER, (char *) 0); + execl(cmd, cmd, "-e", EDIT_CMD_STR(edit_cmd), (char *) 0); + msg_fatal("%s: %m", cmd); +} + +/* run_user_command - execute external command with requested MAIL_CONFIG env */ + +static int run_user_command(INSTANCE *ip, int iter_cmd, int iter_flags, + char **argv) +{ + WAIT_STATUS_T status; + int pid; + int wpid; + + /* + * Set up a process environment. The postfix(1) command needs MAIL_CONFIG + * (or the equivalent command-line option); it overrides everything else. + * + * postmulti(1) typically runs various Postfix utilities (postsuper, ...) in + * the context of one or more instances. It can also run various scripts + * on the users PATH. So we can't clobber the user's PATH, but do want to + * make sure that the utilities in $command_directory are always found in + * the right place (or at all). + */ + switch (pid = fork()) { + case -1: + msg_warn("fork %s: %m", argv[0]); + return -1; + case 0: + check_setenv(CONF_ENV_PATH, ip->config_dir); + if (iter_cmd != ITER_CMD_POSTFIX) { + check_setenv(VAR_DAEMON_DIR, var_daemon_dir); + check_setenv(VAR_COMMAND_DIR, var_command_dir); + check_setenv(VAR_CONFIG_DIR, ip->config_dir); + check_setenv(VAR_QUEUE_DIR, ip->queue_dir); + check_setenv(VAR_DATA_DIR, ip->data_dir); + check_setenv(VAR_MULTI_NAME, ip->name ? ip->name : ""); + check_setenv(VAR_MULTI_GROUP, ip->gname ? ip->gname : ""); + check_setenv(VAR_MULTI_ENABLE, ip->enabled ? + CONFIG_BOOL_YES : CONFIG_BOOL_NO); + prepend_command_path(); + } + + /* + * Replace: postfix -- start ... With: postfix -- check ... + */ + if (iter_cmd == ITER_CMD_POSTFIX + && (iter_flags & ITER_FLAG_CHECK_DISABLED) && !ip->enabled) + argv[2] = "check"; + + execvp(argv[0], argv); + msg_fatal("execvp %s: %m", argv[0]); + default: + do { + wpid = waitpid(pid, &status, 0); + } while (wpid == -1 && errno == EINTR); + return (wpid == -1 ? -1 : + WIFEXITED(status) ? WEXITSTATUS(status) : 1); + } +} + +/* word_in_list - look up command in start, stop, or control list */ + +static int word_in_list(char *cmdlist, const char *cmd) +{ + char *saved; + char *cp; + char *elem; + + cp = saved = mystrdup(cmdlist); + while ((elem = mystrtok(&cp, CHARS_COMMA_SP)) != 0 && strcmp(elem, cmd) != 0) + /* void */ ; + myfree(saved); + return (elem != 0); +} + +/* iterate_postfix_command - execute postfix(1) command */ + +static int iterate_postfix_command(int iter_cmd, int argc, char **argv, + INST_SELECTION *selection) +{ + int exit_status; + char *cmd; + ARGV *my_argv; + int iter_flags; + + /* + * Override the iterator controls. + */ + if (word_in_list(var_multi_start_cmds, argv[0])) { + iter_flags = ITER_FLAG_CHECK_DISABLED; + } else if (word_in_list(var_multi_stop_cmds, argv[0])) { + iter_flags = ITER_FLAG_SKIP_DISABLED | ITER_FLAG_REVERSE; + } else if (word_in_list(var_multi_cntrl_cmds, argv[0])) { + iter_flags = ITER_FLAG_SKIP_DISABLED; + } else { + iter_flags = 0; + } + + /* + * Override the command line in a straightforward manner: prepend + * "postfix --" to the command arguments. Other overrides (environment, + * start -> check) are implemented below the iterator. + */ +#define POSTFIX_CMD "postfix" + + my_argv = argv_alloc(argc + 2); + cmd = concatenate(var_command_dir, "/" POSTFIX_CMD, (char *) 0); + argv_add(my_argv, cmd, "--", (char *) 0); + myfree(cmd); + while (*argv) + argv_add(my_argv, *argv++, (char *) 0); + + /* + * Execute the command for all applicable Postfix instances. + */ + exit_status = + iterate_command(iter_cmd, iter_flags, my_argv->argv, selection); + + argv_free(my_argv); + return (exit_status); +} + +/* list_instances - list all selected instances */ + +static void list_instances(int iter_flags, INST_SELECTION *selection) +{ + RING *entry; + INSTANCE *ip; + + /* + * Iterate over the selected instances. + */ + FOREACH_ITERATOR_INSTANCE(iter_flags, entry) { + ip = RING_TO_INSTANCE(entry); + if (match_instance_selection(ip, selection)) + vstream_printf("%-15s %-15s %-9s %s\n", + ip->name ? ip->name : "-", + ip->gname ? ip->gname : "-", + ip->enabled ? "y" : "n", + ip->config_dir); + } + if (vstream_fflush(VSTREAM_OUT)) + msg_fatal("error writing output: %m"); +} + +/* iterate_command - execute command for selected instances */ + +static int iterate_command(int iter_cmd, int iter_flags, char **argv, + INST_SELECTION *selection) +{ + int exit_status = 0; + int matched = 0; + RING *entry; + INSTANCE *ip; + + /* + * Iterate over the selected instances. + */ + FOREACH_ITERATOR_INSTANCE(iter_flags, entry) { + ip = RING_TO_INSTANCE(entry); + if ((iter_flags & ITER_FLAG_SKIP_DISABLED) && !ip->enabled) + continue; + if (!match_instance_selection(ip, selection)) + continue; + matched = 1; + + /* Run the requested command */ + if (run_user_command(ip, iter_cmd, iter_flags, argv) != 0) + exit_status = 1; + } + if (matched == 0) + msg_fatal("No matching instances"); + + return (exit_status); +} + +/* iterate - Iterate over all or selected instances */ + +static NORETURN iterate(int iter_cmd, int iter_flags, int argc, char **argv, + INST_SELECTION *selection) +{ + int exit_status; + + /* + * In iterator mode, no selection means wild-card selection. + */ + if (selection->type == INST_SEL_NONE) + selection->type = INST_SEL_ALL; + + /* + * Load the in-memory instance table from main.cf files. + */ + load_all_instances(); + + /* + * Iterate over the selected instances. + */ + switch (iter_cmd) { + case ITER_CMD_POSTFIX: + exit_status = iterate_postfix_command(iter_cmd, argc, argv, selection); + break; + case ITER_CMD_LIST: + list_instances(iter_flags, selection); + exit_status = 0; + break; + case ITER_CMD_GENERIC: + exit_status = iterate_command(iter_cmd, iter_flags, argv, selection); + break; + default: + msg_panic("iterate: unknown mode: %d", iter_cmd); + } + exit(exit_status); +} + +static NORETURN usage(const char *progname) +{ + msg_fatal("Usage:" + "%s -l [-v] [-a] [-g group] [-i instance] | " + "%s -p [-v] [-a] [-g group] [-i instance] command... | " + "%s -x [-v] [-a] [-i name] [-g group] command... | " + "%s -e action [-v] [-a] [-i name] [-g group] [-I name] " + "[-G group] [param=value ...]", + progname, progname, progname, progname); +} + +MAIL_VERSION_STAMP_DECLARE; + +/* main - iterate commands over multiple instance or manage instances */ + +int main(int argc, char **argv) +{ + int fd; + struct stat st; + char *slash; + char *config_dir; + int ch; + static const CONFIG_STR_TABLE str_table[] = { + VAR_MULTI_START_CMDS, DEF_MULTI_START_CMDS, &var_multi_start_cmds, 0, 0, + VAR_MULTI_STOP_CMDS, DEF_MULTI_STOP_CMDS, &var_multi_stop_cmds, 0, 0, + VAR_MULTI_CNTRL_CMDS, DEF_MULTI_CNTRL_CMDS, &var_multi_cntrl_cmds, 0, 0, + 0, + }; + int instance_select_count = 0; + int command_mode_count = 0; + INST_SELECTION selection; + NAME_ASSIGNMENT assignment; + int iter_flags = ITER_FLAG_DEFAULT; + int cmd_mode = 0; + int code; + + selection.type = INST_SEL_NONE; + assignment.name = assignment.gname = 0; + + /* + * Fingerprint executables and core dumps. + */ + MAIL_VERSION_STAMP_ALLOCATE; + + /* + * Be consistent with file permissions. + */ + umask(022); + + /* + * To minimize confusion, make sure that the standard file descriptors + * are open before opening anything else. XXX Work around for 44BSD where + * fstat can return EBADF on an open file descriptor. + */ + for (fd = 0; fd < 3; fd++) + if (fstat(fd, &st) == -1 + && (close(fd), open("/dev/null", O_RDWR, 0)) != fd) + msg_fatal("open /dev/null: %m"); + + /* + * Set up diagnostics. XXX What if stdin is the system console during + * boot time? It seems a bad idea to log startup errors to the console. + * This is UNIX, a system that can run without hand holding. + */ + if ((slash = strrchr(argv[0], '/')) != 0 && slash[1]) + argv[0] = slash + 1; + if (isatty(STDERR_FILENO)) + msg_vstream_init(argv[0], VSTREAM_ERR); + maillog_client_init(argv[0], MAILLOG_CLIENT_FLAG_LOGWRITER_FALLBACK); + + /* + * Check the Postfix library version as soon as we enable logging. + */ + MAIL_VERSION_CHECK; + + /* + * Process main.cf parameters. This is done before the GETOPT() loop to + * improve logging. This assumes that no command-line option can affect + * parameter processing. + */ + mail_conf_read(); + get_mail_conf_str_table(str_table); + maillog_client_init(argv[0], MAILLOG_CLIENT_FLAG_LOGWRITER_FALLBACK); + + if ((config_dir = getenv(CONF_ENV_PATH)) != 0 + && strcmp(config_dir, DEF_CONFIG_DIR) != 0) + msg_fatal("Non-default configuration directory: %s=%s", + CONF_ENV_PATH, config_dir); + + /* + * Parse switches. Move the above mail_conf_read() block after this loop, + * if any command-line option can affect parameter processing. + */ + while ((ch = GETOPT(argc, argv, "ae:g:i:G:I:lpRvx")) > 0) { + switch (ch) { + default: + usage(argv[0]); + /* NOTREACHED */ + case 'a': + if (selection.type != INST_SEL_ALL) + instance_select_count++; + selection.type = INST_SEL_ALL; + break; + case 'e': + if ((code = EDIT_CMD_CODE(optarg)) < 0) + msg_fatal("Invalid '-e' edit action '%s'. Specify '%s', " + "'%s', '%s', '%s', '%s', '%s', '%s' or '%s'", + optarg, + EDIT_CMD_STR(EDIT_CMD_CREATE), + EDIT_CMD_STR(EDIT_CMD_DESTROY), + EDIT_CMD_STR(EDIT_CMD_IMPORT), + EDIT_CMD_STR(EDIT_CMD_DEPORT), + EDIT_CMD_STR(EDIT_CMD_ENABLE), + EDIT_CMD_STR(EDIT_CMD_DISABLE), + EDIT_CMD_STR(EDIT_CMD_ASSIGN), + EDIT_CMD_STR(EDIT_CMD_INIT)); + if (cmd_mode != code) + command_mode_count++; + cmd_mode = code; + break; + case 'g': + instance_select_count++; + selection.type = INST_SEL_GROUP; + selection.name = optarg; + break; + case 'i': + instance_select_count++; + selection.type = INST_SEL_NAME; + selection.name = optarg; + break; + case 'G': + if (assignment.gname != 0) + msg_fatal("Specify at most one '-G' option"); + assignment.gname = strcmp(optarg, "-") == 0 ? "" : optarg; + break; + case 'I': + if (assignment.name != 0) + msg_fatal("Specify at most one '-I' option"); + assignment.name = strcmp(optarg, "-") == 0 ? "" : optarg; + break; + case 'l': + if (cmd_mode != ITER_CMD_LIST) + command_mode_count++; + cmd_mode = ITER_CMD_LIST; + break; + case 'p': + if (cmd_mode != ITER_CMD_POSTFIX) + command_mode_count++; + cmd_mode = ITER_CMD_POSTFIX; + break; + case 'R': + iter_flags ^= ITER_FLAG_REVERSE; + break; + case 'v': + msg_verbose++; + check_setenv(CONF_ENV_VERB, ""); + break; + case 'x': + if (cmd_mode != ITER_CMD_GENERIC) + command_mode_count++; + cmd_mode = ITER_CMD_GENERIC; + break; + } + } + + /* + * Report missing arguments, or wrong arguments in the wrong context. + */ + if (instance_select_count > 1) + msg_fatal("Specity no more than one of '-a', '-g', '-i'"); + + if (command_mode_count != 1) + msg_fatal("Specify exactly one of '-e', '-l', '-p', '-x'"); + + if (cmd_mode == ITER_CMD_LIST && argc > optind) + msg_fatal("Command not allowed with '-l'"); + + if (cmd_mode == ITER_CMD_POSTFIX || cmd_mode == ITER_CMD_GENERIC) + if (argc == optind) + msg_fatal("Command required with '-p' or '-x' option"); + + if (cmd_mode == ITER_CMD_POSTFIX || (cmd_mode & EDIT_CMD_MASK_ALL)) + if (iter_flags != ITER_FLAG_DEFAULT) + msg_fatal("The '-p' and '-e' options preclude the use of '-R'"); + + if ((cmd_mode & EDIT_CMD_MASK_ASSIGN) == 0 + && (assignment.name || assignment.gname)) { + if ((cmd_mode & EDIT_CMD_MASK_ALL) == 0) + msg_fatal("Cannot assign instance name or group without '-e %s'", + EDIT_CMD_STR(EDIT_CMD_ASSIGN)); + else + msg_fatal("Cannot assign instance name or group with '-e %s'", + EDIT_CMD_STR(cmd_mode)); + } + if (cmd_mode & EDIT_CMD_MASK_ALL) { + if (cmd_mode == EDIT_CMD_ASSIGN + && (assignment.name == 0 && assignment.gname == 0)) + msg_fatal("Specify new instance name or group with '-e %s'", + EDIT_CMD_STR(cmd_mode)); + + if ((cmd_mode & ~EDIT_CMD_MASK_ADD) != 0 && argc > optind) + msg_fatal("Parameter overrides not valid with '-e %s'", + EDIT_CMD_STR(cmd_mode)); + } + + /* + * Sanity checks. + */ + check_shared_dir_status(); + + /* + * Iterate over selected instances, or manipulate one instance. + */ + if (cmd_mode & ITER_CMD_MASK_ALL) + iterate(cmd_mode, iter_flags, argc - optind, argv + optind, &selection); + else + manage(cmd_mode, argc - optind, argv + optind, &selection, &assignment); +} |