pg_basebackup: Add support for relocating tablespaces
authorPeter Eisentraut <peter_e@gmx.net>
Sat, 22 Feb 2014 18:38:06 +0000 (13:38 -0500)
committerPeter Eisentraut <peter_e@gmx.net>
Sat, 22 Feb 2014 18:38:06 +0000 (13:38 -0500)
Tablespaces can be relocated in plain backup mode by specifying one or
more -T olddir=newdir options.

Author: Steeve Lennmark <steevel@handeldsbanken.se>
Reviewed-by: Peter Eisentraut <peter_e@gmx.net>
doc/src/sgml/ref/pg_basebackup.sgml
src/bin/pg_basebackup/pg_basebackup.c

index c379df546c61a616e137777214408b2c32c24543..ea2233123ecae2b8616425ed52ad9ec21c66ea46 100644 (file)
@@ -202,6 +202,33 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-T <replaceable class="parameter">olddir</replaceable>=<replaceable class="parameter">newdir</replaceable></option></term>
+      <term><option>--tablespace-mapping=<replaceable class="parameter">olddir</replaceable>=<replaceable class="parameter">newdir</replaceable></option></term>
+      <listitem>
+       <para>
+        Relocate the tablespace in directory <replaceable>olddir</replaceable>
+        to <replaceable>newdir</replaceable> during the backup.  To be
+        effective, <replaceable>olddir</replaceable> must exactly match the
+        path specification of the tablespace as it is currently defined.  (But
+        it is not an error if there is no tablespace
+        in <replaceable>olddir</replaceable> contained in the backup.)
+        Both <replaceable>olddir</replaceable>
+        and <replaceable>newdir</replaceable> must be absolute paths.  If a
+        path happens to contain a <literal>=</literal> sign, escape it with a
+        backslash.  This option can be specified multiple times for multiple
+        tablespaces.  See examples below.
+       </para>
+
+       <para>
+        If a tablespace is relocated in this way, the symbolic links inside
+        the main data directory are updated to point to the new location.  So
+        the new data directory is ready to be used for a new server instance
+        with all tablespaces in the updated locations.
+        </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--xlogdir=<replaceable class="parameter">xlogdir</replaceable></option></term>
       <listitem>
@@ -528,9 +555,13 @@ PostgreSQL documentation
   </para>
 
   <para>
-   The way <productname>PostgreSQL</productname> manages tablespaces, the path
-   for all additional tablespaces must be identical whenever a backup is
-   restored. The main data directory, however, is relocatable to any location.
+   Tablespaces will in plain format by default be backed up to the same path
+   they have on the server, unless the
+   option <replaceable>--tablespace-mapping</replaceable> is used.  Without
+   this option, running a plain format base backup on the same host as the
+   server will not work if tablespaces are in use, because the backup would
+   have to be written to the same directory locations as the original
+   tablespaces.
   </para>
 
   <para>
@@ -570,6 +601,15 @@ PostgreSQL documentation
    (This command will fail if there are multiple tablespaces in the
    database.)
   </para>
+
+  <para>
+   To create a backup of a local database where the tablespace in
+   <filename>/opt/ts</filename> is relocated
+   to <filename>./backup/ts</filename>:
+<screen>
+<prompt>$</prompt> <userinput>pg_basebackup -D backup/data -T /opt/ts=$(pwd)/backup/ts</userinput>
+</screen>
+  </para>
  </refsect1>
 
  <refsect1>
index 3d155e8907c2932cc54f9b83d09005565a3b02e1..9d7a1e38add4dfb6a5c8d475babcb2d8c3176cf0 100644 (file)
 #include "streamutil.h"
 
 
+#define atooid(x)  ((Oid) strtoul((x), NULL, 10))
+
+typedef struct TablespaceListCell
+{
+   struct TablespaceListCell *next;
+   char old_dir[MAXPGPATH];
+   char new_dir[MAXPGPATH];
+} TablespaceListCell;
+
+typedef struct TablespaceList
+{
+   TablespaceListCell *head;
+   TablespaceListCell *tail;
+} TablespaceList;
+
 /* Global options */
 static char *basedir = NULL;
+static TablespaceList tablespace_dirs = {NULL, NULL};
 static char *xlog_dir = "";
 static char    format = 'p';       /* p(lain)/t(ar) */
 static char *label = "pg_basebackup base backup";
@@ -90,6 +106,10 @@ static void BaseBackup(void);
 static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
                     bool segment_finished);
 
+static const char *get_tablespace_mapping(const char *dir);
+static void update_tablespace_symlink(Oid oid, const char *old_dir);
+static void tablespace_list_append(const char *arg);
+
 
 static void disconnect_and_exit(int code)
 {
@@ -110,6 +130,77 @@ static void disconnect_and_exit(int code)
 }
 
 
+/*
+ * Split argument into old_dir and new_dir and append to tablespace mapping
+ * list.
+ */
+static void
+tablespace_list_append(const char *arg)
+{
+   TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell));
+   char        *dst;
+   char        *dst_ptr;
+   const char  *arg_ptr;
+
+   dst_ptr = dst = cell->old_dir;
+   for (arg_ptr = arg; *arg_ptr; arg_ptr++)
+   {
+       if (dst_ptr - dst >= MAXPGPATH)
+       {
+           fprintf(stderr, _("%s: directory name too long\n"), progname);
+           exit(1);
+       }
+
+       if (*arg_ptr == '\\' && *(arg_ptr + 1) == '=')
+           ;  /* skip backslash escaping = */
+       else if (*arg_ptr == '=' && (arg_ptr == arg || *(arg_ptr - 1) != '\\'))
+       {
+           if (*cell->new_dir)
+           {
+               fprintf(stderr, _("%s: multiple \"=\" signs in tablespace mapping\n"), progname);
+               exit(1);
+           }
+           else
+               dst = dst_ptr = cell->new_dir;
+       }
+       else
+           *dst_ptr++ = *arg_ptr;
+   }
+
+   if (!*cell->old_dir || !*cell->new_dir)
+   {
+       fprintf(stderr,
+               _("%s: invalid tablespace mapping format \"%s\", must be \"OLDDIR=NEWDIR\"\n"),
+               progname, arg);
+       exit(1);
+   }
+
+   /* This check isn't absolutely necessary.  But all tablespaces are created
+    * with absolute directories, so specifying a non-absolute path here would
+    * just never match, possibly confusing users.  It's also good to be
+    * consistent with the new_dir check. */
+   if (!is_absolute_path(cell->old_dir))
+   {
+       fprintf(stderr, _("%s: old directory not absolute in tablespace mapping: %s\n"),
+               progname, cell->old_dir);
+       exit(1);
+   }
+
+   if (!is_absolute_path(cell->new_dir))
+   {
+       fprintf(stderr, _("%s: new directory not absolute in tablespace mapping: %s\n"),
+               progname, cell->new_dir);
+       exit(1);
+   }
+
+   if (tablespace_dirs.tail)
+       tablespace_dirs.tail->next = cell;
+   else
+       tablespace_dirs.head = cell;
+   tablespace_dirs.tail = cell;
+}
+
+
 #ifdef HAVE_LIBZ
 static const char *
 get_gz_error(gzFile gzf)
@@ -137,6 +228,8 @@ usage(void)
    printf(_("  -F, --format=p|t       output format (plain (default), tar)\n"));
    printf(_("  -R, --write-recovery-conf\n"
             "                         write recovery.conf after backup\n"));
+   printf(_("  -T, --tablespace-mapping=OLDDIR=NEWDIR\n"
+            "                         relocate tablespace in OLDDIR to NEWDIR\n"));
    printf(_("  -x, --xlog             include required WAL files in backup (fetch mode)\n"));
    printf(_("  -X, --xlog-method=fetch|stream\n"
             "                         include required WAL files with specified method\n"));
@@ -899,6 +992,52 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
        PQfreemem(copybuf);
 }
 
+
+/*
+ * Retrieve tablespace path, either relocated or original depending on whether
+ * -T was passed or not.
+ */
+static const char *
+get_tablespace_mapping(const char *dir)
+{
+   TablespaceListCell *cell;
+
+   for (cell = tablespace_dirs.head; cell; cell = cell->next)
+       if (strcmp(dir, cell->old_dir) == 0)
+           return cell->new_dir;
+
+   return dir;
+}
+
+
+/*
+ * Update symlinks to reflect relocated tablespace.
+ */
+static void
+update_tablespace_symlink(Oid oid, const char *old_dir)
+{
+   const char *new_dir = get_tablespace_mapping(old_dir);
+
+   if (strcmp(old_dir, new_dir) != 0)
+   {
+       char *linkloc = psprintf("%s/pg_tblspc/%d", basedir, oid);
+
+       if (unlink(linkloc) != 0 && errno != ENOENT)
+       {
+           fprintf(stderr, _("%s: could not remove symbolic link \"%s\": %s"),
+                   progname, linkloc, strerror(errno));
+           disconnect_and_exit(1);
+       }
+       if (symlink(new_dir, linkloc) != 0)
+       {
+           fprintf(stderr, _("%s: could not create symbolic link \"%s\": %s"),
+                   progname, linkloc, strerror(errno));
+           disconnect_and_exit(1);
+       }
+   }
+}
+
+
 /*
  * Receive a tar format stream from the connection to the server, and unpack
  * the contents of it into a directory. Only files, directories and
@@ -906,8 +1045,7 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
  *
  * If the data is for the main data directory, it will be restored in the
  * specified directory. If it's for another tablespace, it will be restored
- * in the original directory, since relocation of tablespaces is not
- * supported.
+ * in the original or mapped directory.
  */
 static void
 ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
@@ -923,7 +1061,7 @@ ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum)
    if (basetablespace)
        strlcpy(current_path, basedir, sizeof(current_path));
    else
-       strlcpy(current_path, PQgetvalue(res, rownum, 1), sizeof(current_path));
+       strlcpy(current_path, get_tablespace_mapping(PQgetvalue(res, rownum, 1)), sizeof(current_path));
 
    /*
     * Get the COPY data
@@ -1503,7 +1641,10 @@ BaseBackup(void)
         * we do anything anyway.
         */
        if (format == 'p' && !PQgetisnull(res, i, 1))
-           verify_dir_is_empty_or_create(PQgetvalue(res, i, 1));
+       {
+           char *path = (char *) get_tablespace_mapping(PQgetvalue(res, i, 1));
+           verify_dir_is_empty_or_create(path);
+       }
    }
 
    /*
@@ -1545,6 +1686,17 @@ BaseBackup(void)
        progress_report(PQntuples(res), NULL, true);
        fprintf(stderr, "\n");  /* Need to move to next line */
    }
+
+   if (format == 'p' && tablespace_dirs.head != NULL)
+   {
+       for (i = 0; i < PQntuples(res); i++)
+       {
+           Oid tblspc_oid = atooid(PQgetvalue(res, i, 0));
+           if (tblspc_oid)
+               update_tablespace_symlink(tblspc_oid, PQgetvalue(res, i, 1));
+       }
+   }
+
    PQclear(res);
 
    /*
@@ -1696,6 +1848,7 @@ main(int argc, char **argv)
        {"format", required_argument, NULL, 'F'},
        {"checkpoint", required_argument, NULL, 'c'},
        {"write-recovery-conf", no_argument, NULL, 'R'},
+       {"tablespace-mapping", required_argument, NULL, 'T'},
        {"xlog", no_argument, NULL, 'x'},
        {"xlog-method", required_argument, NULL, 'X'},
        {"gzip", no_argument, NULL, 'z'},
@@ -1735,7 +1888,7 @@ main(int argc, char **argv)
        }
    }
 
-   while ((c = getopt_long(argc, argv, "D:F:RxX:l:zZ:d:c:h:p:U:s:wWvP",
+   while ((c = getopt_long(argc, argv, "D:F:RT:xX:l:zZ:d:c:h:p:U:s:wWvP",
                            long_options, &option_index)) != -1)
    {
        switch (c)
@@ -1759,6 +1912,9 @@ main(int argc, char **argv)
            case 'R':
                writerecoveryconf = true;
                break;
+           case 'T':
+               tablespace_list_append(optarg);
+               break;
            case 'x':
                if (includewal)
                {