MythTV  0.27pre
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Groups Pages
mythconverg_backup.pl
Go to the documentation of this file.
1 #!/usr/bin/perl -w
2 #
3 # mythconverg_backup.pl
4 #
5 # Creates a backup of the MythTV database.
6 #
7 # For details, see:
8 # mythconverg_backup.pl --help
9 
10 # Includes
11  use Getopt::Long;
12  use File::Temp qw/ tempfile /;
13 
14 # Script info
15  $NAME = 'MythTV Database Backup Script';
16  $VERSION = '1.0.11';
17 
18 # Some variables we'll use here
19  our ($username, $homedir, $mythconfdir, $database_information_file);
20  our ($mysqldump, $compress, $rotate, $rotateglob, $backup_xmltvids);
21  our ($usage, $debug, $show_version, $show_version_script, $dbh);
23 # This script does not accept a database password on the command-line.
24 # Any packager who enables the functionality should modify the --help output.
25 # our ($db_password);
26  our ($db_hostname, $db_port, $db_username, $db_name, $db_schema_version);
27  our ($backup_directory, $backup_filename);
29 
30  our %mysql_conf = ('db_host' => '',
31  'db_port' => -1,
32  'db_user' => '',
33  'db_pass' => '',
34  'db_name' => '',
35  'db_schemaver' => ''
36  );
37  our %backup_conf = ('directory' => '',
38  'filename' => ''
39  );
40 
41 # Variables used to untaint data
43  our $old_env_path = $ENV{"PATH"};
44  our @d_allowed_paths = ("/bin",
45  "/usr/bin",
46  "/usr/local/bin",
47  "/sbin",
48  "/usr/sbin",
49  "/usr/local/sbin"
50  );
52 
53 # Debug levels
57 
58 # Defaults
59  $d_db_name = 'mythconverg';
60  $d_mysqldump = 'mysqldump';
61  $d_compress = 'gzip';
62  $d_rotate = 5;
63  $d_rotateglob = $d_db_name.'-????-??????????????.sql*';
64 
65 # Provide default values for GetOptions
70  $debug = 0;
71 
72 # Load the cli options
73  GetOptions('hostname|DBHostName=s' => \$db_hostname,
74  'port|DBPort=i' => \$db_port,
75  'username|DBUserName=s' => \$db_username,
76 # This script does not accept a database password on the command-line.
77 # 'password|DBPassword=s' => \$db_password,
78  'name|DBName=s' => \$db_name,
79  'schemaver|DBSchemaVer=s' => \$db_schema_version,
80  'directory|DBBackupDirectory=s' => \$backup_directory,
81  'filename|DBBackupFilename=s' => \$backup_filename,
82  'mysqldump=s' => \$mysqldump,
83  'compress=s' => \$compress,
84  'rotate=i' => \$rotate,
85  'rotateglob|glob=s' => \$rotateglob,
86  'backup_xmltvids|backup-xmltvids|'.
87  'xmltvids' => \$backup_xmltvids,
88  'usage|help|h+' => \$usage,
89  'version' => \$show_version,
90  'script_version|script-version|v' => \$show_version_script,
91  'verbose|debug|d+' => \$debug
92  );
93 
94 # Print version information
95  sub print_version_information
96  {
97  my $script_name = substr $0, rindex($0, '/') + 1;
98  print "$NAME\n$script_name\nversion: $VERSION\n";
99  }
100 
101  if ($show_version_script)
102  {
103  print "$NAME,$VERSION,,\n";
104  exit;
105  }
106  elsif ($show_version)
107  {
108  print_version_information;
109  exit;
110  }
111 
112 
113 # Print usage
114  if ($usage)
115  {
116  print_version_information;
117  print <<EOF;
118 
119 Usage:
120  $0 [options|database_information_file]
121 
122 Creates a backup of the MythTV database.
123 
124 QUICK START:
125 
126 Create a file ~/.mythtv/backuprc with a single line,
127 "DBBackupDirectory=/home/mythtv" (no quotes), and run this script to create a
128 database backup. Use the --verbose argument to see what is happening.
129 
130 # echo "DBBackupDirectory=/home/mythtv" > ~/.mythtv/backuprc
131 # $0 --verbose
132 
133 Make sure you keep the backuprc file for next time. Once you have successfully
134 created a backup, the script may be run without the --verbose argument.
135 
136 To backup xmltvids:
137 
138 Ensure you have a ~/.mythtv/backuprc file, as described above, and execute this
139 script with the --backup_xmltvids argument.
140 
141 # $0 --backup_xmltvids
142 
143 EOF
144 
145  if ($usage > 1)
146  {
147  print <<EOF;
148 DETAILED DESCRIPTION:
149 
150 This script can be called by MythTV for creating automatic database backups.
151 In this mode, it is always exected with a single command-line argument
152 specifying the name of a "database information file" (see DATABASE INFORMATION
153 FILE, below), which contains sufficient information about the database and the
154 backup to allow the script to create a backup without needing any additional
155 configuration files. In this mode, all other MythTV configuration files
156 (including config.xml, mysql.txt) are ignored, but the backup resource file
157 (see RESOURCE FILE, below) and the MySQL option files (i.e. /etc/my.cnf or
158 ~/.my.cnf) will be honored.
159 
160 The script can also be called interactively (i.e. "manually") by the user to
161 create a database backup on demand. Required information may be passed into
162 the script using command-line arguments or with a database information file.
163 If a database information file is specified, all command-line arguments will be
164 ignored. If no database information file is specified, the script will attempt
165 to determine the appropriate configuration by using the MythTV configuration
166 file(s) (preferring config.xml, but falling back to mysql.txt if no config.xml
167 exists). Once the MythTV configuration file has been parsed, the backup
168 resource file (see RESOURCE FILE, below) will be parsed, then command-line
169 arguments will be applied (thus overriding any values determined from the
170 configuration files).
171 
172 The only information required by the script is the directory in which the
173 backup should be created. Therefore, when using a database information file,
174 the DBBackupDirectory should be specified, or if running manually, the
175 --directory command-line argument should be specified. The DBBackupDirectory
176 may be specified in a backup resource file (see RESOURCE FILE, below). Doing
177 so is especially useful for manual backups. If the specified directory is not
178 writable, the script will terminate. Likewise, if a file whose name matches
179 the name to be used for the backup file already exists, the script will
180 terminate.
181 
182 If the database name is not specified, the script will attempt to use the
183 MythTV default database name, $d_db_name. Note that the same is not true for
184 the database username and database password. These must be explicitly
185 specified. The password must be specified in a database information file, a
186 backup resource file, or a MySQL options file. The username may be specified
187 the same way or may be specified using a command-line argument if not using a
188 database information file.
189 
190 While this script may be called while MythTV is running, there is a possibility
191 of creating a backup with data integrity errors (i.e. if MythTV updates data in
192 multiple tables between the time the script backs up the first and subsequent
193 tables). Also, depending on your system configuration, performing a backup
194 (which may result in locking a table while it is being backed up) while
195 recording may cause corruption of the recording or inability to properly write
196 recording data (such as the recording seek table) to the database.
197 Therefore, if configuring this script to run in a cron job, try to ensure it
198 runs at a time when recordings are least likely to occur. Alternatively, by
199 choosing to run the script in a system start/shutdown script (i.e. an init
200 script), you may call the script before starting mythbackend or after stopping
201 mythbackend. Note, however, that checking whether to perform the backup is the
202 responsibility of the init script (not this script)--i.e. in a system with
203 multiple frontends/backends, the init script should ensure the backup is
204 created only on the master backend.
205 
206 DATABASE INFORMATION FILE
207 
208 The database information file contains information about the database and the
209 backup. The information within the file is specified as name=value pairs using
210 the same names as used by the MythTV config.xml and mysql.txt configuration
211 files. The following variables are recognized:
212 
213  DBHostName - The hostname (or IP address) which should be used to find the
214  MySQL server.
215  DBPort - The TCP/IP port number to use for the connection. This may have a
216  value of 0, i.e. if the hostname is localhost or if the server is
217  using the default MySQL port or the port specified in a MySQL
218  options file.
219  DBUserName - The database username to use when connecting to the server.
220  DBPassword - The password to use when connecting to the server.
221  DBName - The name of the database that contains the MythTV data.
222  DBSchemaVer - The MythTV schema version of the database. This value will be
223  used to create the backup filename, but only if the filename
224  has not been specified using DBBackupFilename or the --filename
225  argument.
226  DBBackupDirectory - The directory in which the backup file should be
227  created. This directory may have been specially
228  configured by the user as the "DB Backups" storage
229  group. It is recommended that this directory be
230  used--especially in "common-use" scripts such as those
231  provided by distributions.
232  DBBackupFilename - The name of the file in which the backup should be
233  created. Additional extensions may be added by this
234  script as required (i.e. adding an appropriate suffix,
235  such as ".gz", to the file if it is compressed). If the
236  filename recommended by mythbackend is used, it will be
237  displayed in the GUI messages provided for the user. If
238  the recommended filename is not used, the user will not be
239  told where to find the backup file. If no value is
240  provided, a filename using the default filename format
241  will be chosen.
242  mysqldump - The path (including filename) of the mysqldump executable.
243  compress - The command (including path, if necessary) to use to
244  compress the backup. Using gzip is significantly less
245  resource intensive on an SQL backup file than using bzip2,
246  at the cost of a slightly (about 33%) larger compressed
247  filesize, a difference which should be irrelevant at the
248  filesizes involved (especially when compared to the size
249  of recording files). If you decide to use another
250  compression algorithm, please ensure you test it
251  appropriately to verify it does not negatively affect
252  operation of your system. If no value is specified for
253  compress or if the value '$d_compress' is specified, the
254  script will first attempt to use the IO::Compress::Gzip
255  module to compress the backup file, but, if not available,
256  will run the command specified. Therefore, if
257  IO::Compress::Gzip is installed and functional, specifying
258  a value for compress is unnecessary. If neither approach
259  works, the backup file will be left uncompressed.
260  rotate - The number of backups to keep when rotating. To disable
261  rotation, specify -1. Backup rotation is performed by
262  identifying all files in DBBackupDirectory whose names
263  match the glob specified by rotateglob. It is critical
264  that the chosen backup filenames can be sorted properly
265  using an alphabetical sort. If using the default filename
266  format--which contains the DBSchemaVer--and you downgrade
267  MythTV and restore a backup from an older DBSchemaVer,
268  make sure you move the backups from the newer DBSchemaVer
269  out of the DBBackupDirectory or they may cause your new
270  backups to be deleted.
271  rotateglob - The sh-like glob used to identify files within
272  DBBackupDirectory to be considered for rotation. Be
273  very careful with the value--especially if using a
274  DBBackupDirectory that contains any files other than
275  backups.
276 
277 RESOURCE FILE
278 
279 The backup resource file specifies values using the same format as described
280 for the database information file, above, but is intended as a "permanent,"
281 user-created configuration file. The database information file is intended as
282 a "single-use" configuration file, often created automatically (i.e. by a
283 program, such as mythbackend, or a script). The backup resource file should be
284 placed at "~/.mythtv/backuprc" and given appropriate permissions. To be usable
285 by the script, it must be readable. However, it should be protected as
286 required--i.e. if the DBPassword is specified, it should be made readable only
287 by the owner.
288 
289 When specifying a database information file, the resource file is parsed before
290 the database information file to prevent the resource file from overriding the
291 information in the database information file. When no database information
292 file is specified, the resource file is parsed after the MythTV configuration
293 files, but before the command-line arguments to allow the resource file to
294 override values in the configuration files and to allow command-line arguments
295 to override resource file defaults.
296 
297 options:
298 
299 --hostname [database hostname]
300 
301  The hostname (or IP address) which should be used to find the MySQL server.
302  See DBHostName, above.
303 
304 --port [database port]
305 
306  The TCP/IP port number to use for connection to the MySQL server. See
307  DBPort, above.
308 
309 --username [database username]
310 
311  The MySQL username to use for connection to the MySQL server. See
312  DBUserName, above.
313 
314 --name [database name]
315 
316  The name of the database containing the MythTV data. See DBName, above.
317 
318  Default: $d_db_name
319 
320 --schemaver [MythTV database schema version]
321 
322  The MythTV schema version. See DBSchemaVer, above.
323 
324 --directory [directory]
325 
326  The directory in which the backup file should be stored. See
327  DBBackupDirectory, above.
328 
329 --filename [database backup filename]
330 
331  The name to use for the database backup file. If not provided, a filename
332  using a default format will be chosen. See DBBackupFilename, above.
333 
334 --mysqldump [path]
335 
336  The path (including filename) of the mysqldump executable. See mysqldump
337  in the DATABASE INFORMATION FILE description, above.
338 
339  Default: $d_mysqldump
340 
341 --compress [path]
342 
343  The command (including path, if necessary) to use to compress the backup.
344  See compress in the DATABASE INFORMATION FILE description, above.
345 
346  Default: $d_compress
347 
348 --rotate [number]
349  The number of backups to keep when rotating. To disable rotation, specify
350  -1. See rotate in the DATABASE INFORMATION FILE description, above.
351 
352  Default: $d_rotate
353 
354 --rotateglob [glob]
355  The sh-like glob used to identify files within DBBackupDirectory to be
356  considered for rotation. See rotateglob in the DATABASE INFORMATION FILE
357  description, above.
358 
359  Default: $d_rotateglob
360 
361 --backup_xmltvids
362  Rather than creating a backup of the entire database, create a backup of
363  xmltvids. This is useful when doing a full channel scan. The resulting
364  backup is a series of SQL UPDATE statements that can be executed to set
365  the xmltvid for channels whose callsign is the same before and after
366  the scan. Note that the backup file will contain comments with additional
367  channel information, which you can use to identify channels in case the
368  callsign changes.
369 
370 --help
371 
372  Show this help text.
373 
374 --version
375 
376  Show version information.
377 
378 --verbose
379 
380  Show what is happening.
381 
382 --script_version | -v
383 
384  Show script version information. This is primarily useful for scripts
385  or programs needing to parse the version information.
386 
387 EOF
388  }
389  else
390  {
391  print "For detailed help:\n\n# $0 --help --help\n\n";
392  }
393  exit;
394  }
395 
396  sub verbose
397  {
398  my $level = shift;
399  my $error = 0;
400  if ($level == $verbose_level_error)
401  {
402  $error = 1;
403  }
404  else
405  {
406  return unless ($debug >= $level);
407  }
408  print { $error ? STDERR : STDOUT } join("\n", @_), "\n";
409  }
410 
412  {
414  '',
415  'Database Information:',
416  " DBHostName: $mysql_conf{'db_host'}",
417  " DBPort: $mysql_conf{'db_port'}",
418  " DBUserName: $mysql_conf{'db_user'}",
419  ' DBPassword: ' .
420  ( $mysql_conf{'db_pass'} ? 'XXX' : '' ),
421  # "$mysql_conf{'db_pass'}",
422  " DBName: $mysql_conf{'db_name'}",
423  " DBSchemaVer: $mysql_conf{'db_schemaver'}",
424  " DBBackupDirectory: $backup_conf{'directory'}",
425  " DBBackupFilename: $backup_conf{'filename'}");
427  '',
428  'Executables:',
429  " mysqldump: $mysqldump",
430  " compress: $compress");
431  }
432 
434  {
436  '', 'Configuring environment:');
437 
438  # Get the user's login and home directory, so we can look for config files
439  ($username, $homedir) = (getpwuid $>)[0,7];
440  $username = $ENV{'USER'} if ($ENV{'USER'});
441  $homedir = $ENV{'HOME'} if ($ENV{'HOME'});
442  if ($username && !$homedir)
443  {
444  $homedir = "/home/$username";
445  if (!-e $homedir && -e "/Users/$username")
446  {
447  $homedir = "/Users/$username";
448  }
449  }
451  " - username: $username",
452  " - HOME: $homedir");
453 
454  # Find the config directory
455  $mythconfdir = $ENV{'MYTHCONFDIR'}
456  ? $ENV{'MYTHCONFDIR'}
457  : "$homedir/.mythtv"
458  ;
459 
461  " - MYTHCONFDIR: $mythconfdir");
462  }
463 
464 # Though much of the configuration file parsing could be done by the MythTV
465 # Perl bindings, using them to retrieve database information is not appropriate
466 # for a backup script. The Perl bindings require the backend to be running and
467 # use UPnP for autodiscovery. Also, parsing the files "locally" allows
468 # supporting even the old MythTV database configuration file, mysql.txt.
469  sub parse_database_information
470  {
471  my $file = shift;
473  " - checking: $file");
474  return 0 unless ($file && -e $file);
476  " parsing: $file");
477  open(CONF, $file) or die("\nERROR: Unable to read $file: $!".
478  ', stopped');
479  while (my $line = <CONF>)
480  {
481  # Cleanup
482  next if ($line =~ m/^\s*#/);
483  $line =~ s/^str //;
484  chomp($line);
485  $line =~ s/^\s+//;
486  $line =~ s/\s+$//;
487  # Split off the var=val pairs
488  my ($var, $val) = split(/ *[\=\: ] */, $line, 2);
489  # Also look for <var>val</var> from config.xml
490  if ($line =~ m/<(\w+)>(.+)<\/(\w+)>$/ && $1 eq $3)
491  {
492  $var = $1; $val = $2;
493  }
494  next unless ($var && $var =~ m/\w/);
495  if (($var eq 'Host') || ($var eq 'DBHostName'))
496  {
497  $mysql_conf{'db_host'} = $val;
498  }
499  elsif (($var eq 'Port') || ($var eq 'DBPort'))
500  {
501  $mysql_conf{'db_port'} = $val;
502  }
503  elsif (($var eq 'UserName') || ($var eq 'DBUserName'))
504  {
505  $mysql_conf{'db_user'} = $val;
506  }
507  elsif (($var eq 'Password') || ($var eq 'DBPassword'))
508  {
509  $mysql_conf{'db_pass'} = $val;
510  $mysql_conf{'db_pass'} =~ s/&amp;/&/sg;
511  $mysql_conf{'db_pass'} =~ s/&gt;/>/sg;
512  $mysql_conf{'db_pass'} =~ s/&lt;/</sg;
513  }
514  elsif (($var eq 'DatabaseName') || ($var eq 'DBName'))
515  {
516  $mysql_conf{'db_name'} = $val;
517  }
518  elsif ($var eq 'DBSchemaVer')
519  {
520  $mysql_conf{'db_schemaver'} = $val;
521  }
522  elsif ($var eq 'DBBackupDirectory')
523  {
524  $backup_conf{'directory'} = $val;
525  }
526  elsif ($var eq 'DBBackupFilename')
527  {
528  $backup_conf{'filename'} = $val;
529  }
530  elsif ($var eq 'mysqldump')
531  {
532  $mysqldump = $val;
533  }
534  elsif ($var eq 'compress')
535  {
536  $compress = $val;
537  }
538  elsif ($var eq 'rotate')
539  {
540  $rotate = $val;
541  }
542  elsif ($var eq 'rotateglob')
543  {
544  $rotateglob = $val;
545  }
546  }
547  close CONF;
548  return 1;
549  }
550 
551  sub read_mysql_txt
552  {
553  # Read the "legacy" mysql.txt file in use by MythTV. It could be in a
554  # couple places, so try the usual suspects in the same order that mythtv
555  # does in libs/libmyth/mythcontext.cpp
556  my $found = 0;
557  my $result = 0;
558  my @mysql = ('/usr/local/share/mythtv/mysql.txt',
559  '/usr/share/mythtv/mysql.txt',
560  '/usr/local/etc/mythtv/mysql.txt',
561  '/etc/mythtv/mysql.txt',
562  $homedir ? "$homedir/.mythtv/mysql.txt" : '',
563  'mysql.txt',
564  $mythconfdir ? "$mythconfdir/mysql.txt" : '',
565  );
566  foreach my $file (@mysql)
567  {
568  $found = parse_database_information($file);
569  $result = $result + $found;
570  }
571  return $result;
572  }
573 
574  sub read_resource_file
575  {
576  parse_database_information("$mythconfdir/backuprc");
577  }
578 
579  sub apply_arguments
580  {
582  '', 'Applying command-line arguments.');
583  if ($db_hostname)
584  {
585  $mysql_conf{'db_host'} = $db_hostname;
586  }
587  if ($db_port)
588  {
589  $mysql_conf{'db_port'} = $db_port;
590  }
591  if ($db_username)
592  {
593  $mysql_conf{'db_user'} = $db_username;
594  }
595  # This script does not accept a database password on the command-line.
596 # if ($db_password)
597 # {
598 # $mysql_conf{'db_pass'} = $db_password;
599 # }
600  if ($db_name)
601  {
602  $mysql_conf{'db_name'} = $db_name;
603  }
604  if ($db_schema_version)
605  {
606  $mysql_conf{'db_schemaver'} = $db_schema_version;
607  }
608  if ($backup_directory)
609  {
610  $backup_conf{'directory'} = $backup_directory;
611  }
612  if ($backup_filename)
613  {
614  $backup_conf{'filename'} = $backup_filename;
615  }
616  }
617 
618  sub read_config
619  {
620  my $result = 0;
621  # If specified, use only the database information file
623  {
625  '', 'Database Information File specified. Ignoring all'.
626  ' command-line arguments');
628  '', 'Database Information File: '.
630  unless (-T "$database_information_file")
631  {
633  '', 'The argument you supplied for the database'.
634  ' information file is invalid.',
635  'If you were trying to specify a backup filename,'.
636  ' please use the --directory ',
637  'and --filename arguments.');
638  die("\nERROR: Invalid database information file, stopped");
639  }
640  # When using a database information file, parse the resource file first
641  # so it cannot override database information file settings
642  read_resource_file;
643  $result = parse_database_information($database_information_file);
644  return $result;
645  }
646 
647  # No database information file, so try the MythTV configuration files.
649  '', 'Parsing configuration files:');
650  # Prefer the config.xml file
651  my $file = $mythconfdir ? "$mythconfdir/config.xml" : '';
652  $result = parse_database_information($file);
653  if (!$result)
654  {
655  # Use the "legacy" mysql.txt file as a fallback
656  $result = read_mysql_txt;
657  }
658  # Read the resource file next to override the config file information, but
659  # to allow command-line arguments to override resource file "defaults"
660  read_resource_file;
661  # Apply the command-line arguments to override the information provided
662  # by the config file(s).
663  apply_arguments;
664  return $result;
665  }
666 
667  sub check_database_libs
668  {
669  # Try to load the DBI library if available (but don't require it)
670  BEGIN
671  {
672  our $has_dbi = 1;
673  eval 'use DBI;';
674  if ($@)
675  {
676  $has_dbi = 0;
677  }
678  }
680  '', 'DBI is not installed.') if (!$has_dbi);
681  # Try to load the DBD::mysql library if available (but don't
682  # require it)
683  BEGIN
684  {
685  our $has_dbd = 1;
686  eval 'use DBD::mysql;';
687  if ($@)
688  {
689  $has_dbd = 0;
690  }
691  }
693  '', 'DBD::mysql is not installed.') if (!$has_dbd);
694  return ($has_dbi + $has_dbd);
695  }
696 
697  sub check_database
698  {
699  if (!defined($dbh))
700  {
701  my $have_database_libs = check_database_libs;
702  return 0 if ($have_database_libs < 2);
703  $dbh = DBI->connect("dbi:mysql:".
704  "database=$mysql_conf{'db_name'}:".
705  "host=$mysql_conf{'db_host'}",
706  "$mysql_conf{'db_user'}",
707  "$mysql_conf{'db_pass'}",
708  { PrintError => 0 });
709  }
710  return 1;
711  }
712 
713  sub create_backup_filename
714  {
715  # Create a default backup filename
716  $backup_conf{'filename'} = $mysql_conf{'db_name'};
717  if (!$backup_conf{'filename'})
718  {
719  $backup_conf{'filename'} = $d_db_name;
720  }
721  if ((!$mysql_conf{'db_schemaver'}) &&
722  ($mysql_conf{'db_host'}) && ($mysql_conf{'db_name'}) &&
723  ($mysql_conf{'db_user'}) && ($mysql_conf{'db_pass'}))
724  {
725  # If DBI is available, query the DB for the schema version
726  if (check_database)
727  {
729  '', 'No DBSchemaVer specified, querying database.');
730  my $query = 'SELECT data FROM settings WHERE value = ?';
731  if (defined($dbh))
732  {
733  my $sth = $dbh->prepare($query);
734  if ($sth->execute('DBSchemaVer'))
735  {
736  while (my @data = $sth->fetchrow_array)
737  {
738  $mysql_conf{'db_schemaver'} = $data[0];
740  "Found DBSchemaVer:".
741  " $mysql_conf{'db_schemaver'}.");
742  }
743  }
744  else
745  {
747  "Unable to retrieve DBSchemaVer from".
748  " database. Filename will not contain ",
749  "DBSchemaVer.");
750  }
751  }
752  }
753  else
754  {
756  '', 'No DBSchemaVer specified.',
757  'DBI and/or DBD:mysql is not available. Unable'.
758  ' to query database to determine ',
759  'DBSchemaVer. DBSchemaVer will not be included'.
760  ' in backup filename.',
761  'Please ensure DBI and DBD::mysql are'.
762  ' installed.');
763  }
764  }
765  if ($mysql_conf{'db_schemaver'})
766  {
767  $backup_conf{'filename'} .= '-'.$mysql_conf{'db_schemaver'};
768  }
769  # Format the time using localtime data so we don't have to bring in
770  # another dependency.
771  my @timeData = localtime(time);
772  $backup_conf{'filename'} .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
773  ($timeData[5] + 1900),
774  ($timeData[4] + 1),
775  $timeData[3], $timeData[2],
776  $timeData[1], $timeData[0]);
777  }
778 
779  sub check_backup_directory
780  {
781  my $result = 0;
782  if ($backup_conf{'directory'})
783  {
784  $result = 1;
785  }
786  elsif (check_database)
787  # If DBI is available, query the DB for the backup directory
788  {
790  '', 'No DBBackupDirectory specified, querying database.');
791  my $query = 'SELECT dirname, hostname FROM storagegroup '.
792  ' WHERE groupname = ?';
793  if (defined($dbh))
794  {
795  my $directory;
796  my $sth = $dbh->prepare($query);
797  if ($sth->execute('DB Backups'))
798  {
799  # We don't know the hostname associated with this host, and
800  # since it's not worth parsing the mysql.txt/config.xml
801  # LocalHostName (unique identifier), with fallback to the
802  # system hostname, and handling issues along the way, just look
803  # for any available DB Backups directory and, if none are
804  # usable, look for a Default group directory
805  while (my @data = $sth->fetchrow_array)
806  {
807  $directory = $data[0];
808  if (-d $directory && -w $directory)
809  {
810  $backup_conf{'directory'} = $directory;
812  "Found DB Backups directory:".
813  " $backup_conf{'directory'}.");
814  $result = 1;
815  $sth->finish;
816  last;
817  }
818  }
819  }
820  if ($result == 0 && $sth->execute('Default'))
821  {
822  while (my @data = $sth->fetchrow_array)
823  {
824  $directory = $data[0];
825  if (-d $directory && -w $directory)
826  {
827  $backup_conf{'directory'} = $directory;
829  "Found Default directory:".
830  " $backup_conf{'directory'}.");
831  $result = 1;
832  $sth->finish;
833  last;
834  }
835  }
836  }
837  }
838  if ($result == 0)
839  {
841  "Unable to retrieve DBBackupDirectory from".
842  " database.");
843  }
844  }
845  return $result;
846  }
847 
848  sub check_config
849  {
851  '', 'Checking configuration.');
852  # Check directory/filename
853  if (!check_backup_directory)
854  {
856  die("\nERROR: DBBackupDirectory not specified, stopped");
857  }
858  if ((!-d $backup_conf{'directory'}) ||
859  (!-w $backup_conf{'directory'}))
860  {
863  '', 'ERROR: DBBackupDirectory is not a directory or is '.
864  'not writable. Please specify',
865  ' a directory in your database information file'.
866  ' using DBBackupDirectory.',
867  ' If not using a database information file,'.
868  ' please specify the ',
869  ' --directory command-line option.');
870  die("\nInvalid backup directory, stopped");
871  }
872  if (!$backup_conf{'filename'})
873  {
874  if ($backup_xmltvids)
875  {
876  my $file = 'mythtv_xmltvid_backup';
877  # Format the time using localtime data so we don't have to bring in
878  # another dependency.
879  my @timeData = localtime(time);
880  $file .= sprintf('-%04d%02d%02d%02d%02d%02d.sql',
881  ($timeData[5] + 1900),
882  ($timeData[4] + 1),
883  $timeData[3], $timeData[2],
884  $timeData[1], $timeData[0]);
885  $backup_conf{'filename'} = $file;
886  }
887  else
888  {
889  create_backup_filename;
890  }
891  }
892  if ( -e "$backup_conf{'directory'}/$backup_conf{'filename'}")
893  {
895  '', 'ERROR: The specified file already exists.');
896  die("\nInvalid backup filename, stopped");
897  }
898  if (!$mysql_conf{'db_name'})
899  {
901  '', "WARNING: DBName not specified. Using $d_db_name");
902  $mysql_conf{'db_name'} = $d_db_name;
903  }
904  # Though the script will attempt a backup even if no other database
905  # information is provided (i.e. using "defaults" from the MySQL options
906  # file, warning the user that some "normally-necessary" information is not
907  # provided may be nice.
908  return if (!$debug);
909  if (!$mysql_conf{'db_host'})
910  {
912  '', 'WARNING: DBHostName not specified.',
913  ' Assuming it is specified in the MySQL'.
914  ' options file.');
915  }
916  if (!$mysql_conf{'db_user'})
917  {
919  '', 'WARNING: DBUserName not specified.',
920  ' Assuming it is specified in the MySQL'.
921  ' options file.');
922  }
923  if (!$mysql_conf{'db_pass'})
924  {
926  '', 'WARNING: DBPassword not specified.',
927  ' Assuming it is specified in the MySQL'.
928  ' options file.');
929  }
930  }
931 
932  sub create_defaults_extra_file
933  {
934  return '' if (!$mysql_conf{'db_pass'});
936  '', "Attempting to use supplied password for $mysqldump.",
937  'Any [client] or [mysqldump] password specified in the MySQL'.
938  ' options file will',
939  'take precedence.');
940  # Let tempfile handle unlinking on exit so we don't have to verify that the
941  # file with $filename is the file we created
942  my ($fh, $filename) = tempfile(UNLINK => 1);
943  # Quote the password if it contains # or whitespace or quotes.
944  # Quoting of values in MySQL options files is only supported on MySQL
945  # 4.0.16 and above, so only quote if required.
946  my $quote = '';
947  my $safe_password = $mysql_conf{'db_pass'};
948  if ($safe_password =~ /[#'"\s]/)
949  {
950  $quote = "'";
951  $safe_password =~ s/'/\\'/g;
952  }
953  print $fh "[client]\npassword=${quote}${safe_password}${quote}\n".
954  "[mysqldump]\npassword=${quote}${safe_password}${quote}\n";
955  return $filename;
956  }
957 
958  sub do_xmltvid_backup
959  {
960  my $exit = 1;
961  if (check_database)
962  {
963  my ($chanid, $channum, $callsign, $name, $xmltvid);
964  my $query = " SELECT chanid, channum, callsign, name, xmltvid".
965  " FROM channel ".
966  "ORDER BY CAST(channum AS SIGNED),".
967  " CAST(SUBSTRING(channum".
968  " FROM (1 +".
969  " LOCATE('_', channum) +".
970  " LOCATE('-', channum) +".
971  " LOCATE('#', channum) +".
972  " LOCATE('.', channum)))".
973  " AS SIGNED)";
974  my $sth = $dbh->prepare($query);
975  verbose($verbose_level_debug,
976  '', 'Querying database for xmltvid information.');
977  my $file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
978  open BACKUP, '>', $file or die("\nERROR: Unable to open".
979  " $file: $!, stopped");
980  for ($section = 0; $section < 2; $section++)
981  {
982  if ($sth->execute)
983  {
984  while (my @data = $sth->fetchrow_array)
985  {
986  $chanid = $data[0];
987  $channum = $data[1];
988  $callsign = $data[2];
989  $name = $data[3];
990  $xmltvid = $data[4];
991  verbose($verbose_level_debug,
992  "Found channel: $chanid, $channum, $callsign,".
993  " $name, $xmltvid.") if ($section == 0);
994  if ($xmltvid && $callsign)
995  {
996  if ($section == 0)
997  {
998  print BACKUP "-- Start Channel Data\n".
999  "-- ID: '$chanid'\n".
1000  "-- Number: '$channum'\n".
1001  "-- Callsign: '$callsign'\n".
1002  "-- Name: '$name'\n".
1003  "-- XMLTVID: '$xmltvid'\n".
1004  "-- End Channel Data\n";
1005  print BACKUP "UPDATE channel".
1006  " SET xmltvid = '$xmltvid'".
1007  " WHERE callsign = '$callsign'".
1008  ";\n";
1009  }
1010  else
1011  {
1012  print BACKUP "UPDATE channel".
1013  " SET xmltvid = '$xmltvid'".
1014  " WHERE channum = '$channum'".
1015  " AND name = '$name';\n";
1016  }
1017  }
1018  }
1019  if ($section == 0)
1020  {
1021  verbose($verbose_level_debug,
1022  '', 'Successfully backed up xmltvid'.
1023  ' information.'.
1024  '', '', 'Creating alternate format backup.');
1025  print BACKUP "\n\n/* Alternate format */\n".
1026  "/*\n";
1027  }
1028  else
1029  {
1030  print BACKUP "*/\n";
1031  verbose($verbose_level_debug,
1032  'Successfully created alternate format'.
1033  ' backup.');
1034  }
1035  $exit = 0;
1036  }
1037  else
1038  {
1039  verbose($verbose_level_error,
1040  '', 'ERROR: Unable to retrieve xmltvid information'.
1041  ' from database.');
1042  die("\nError retrieving xmltvid information, stopped");
1043  }
1044  }
1045  close BACKUP;
1046  }
1047  else
1048  {
1049  verbose($verbose_level_error,
1050  '', 'ERROR: Unable to backup xmltvids without Perl'.
1051  ' database libraries.',
1052  ' Please ensure the Perl DBI and DBD::mysql'.
1053  ' modules are installed.');
1054  die("\nPerl database libraries missing, stopped");
1055  }
1056  return $exit;
1057  }
1058 
1059 # This subroutine performs limited checking of a command and untaints the
1060 # command (and the environment) if the command seems to use an absolute path
1061 # containing no . or .. references or if it's a simple command name referencing
1062 # an executable in a "normal" directory for binaries. It should only be called
1063 # after careful consideration of the effects of doing so and of whether it
1064 # makes sense to override taint-mode runtime checking of the value.
1065  sub untaint_command
1066  {
1067  my $command = shift;
1068  my $allow_untaint = 0;
1069  # Only allow directories from @d_allowed_paths that exist in the PATH
1070  if (!defined(@allowed_paths))
1071  {
1072  foreach my $path (split(/:/, $old_env_path))
1073  {
1074  if (grep(/^$path$/, @d_allowed_paths))
1075  {
1076  push(@allowed_paths, $path);
1077  }
1078  }
1079  verbose($verbose_level_debug + 2,
1080  '', 'Allowing paths:', @allowed_paths,
1081  'From PATH: '.$old_env_path);
1082  }
1083 
1084  verbose($verbose_level_debug + 2, '', 'Verifying command: '.$command);
1085  if ($command =~ /^\//)
1086  {
1087  verbose($verbose_level_debug + 2, ' - Command starts with /.');
1088  if (! ($command =~ /\/\.+\//))
1089  {
1090  verbose($verbose_level_debug + 2,
1091  ' - Command does not contain dir refs.');
1092  if (-e "$command" && -f "$command" && -x "$command")
1093  {
1094  # Seems to be a valid executable specified with a path starting
1095  # with / and having no current/previous directory references
1096  verbose($verbose_level_debug + 2,
1097  'Unmodified command meets untaint requirements.',
1098  $command);
1099  $allow_untaint = 1;
1100  }
1101  }
1102  }
1103  else
1104  {
1105  foreach my $path (@allowed_paths)
1106  {
1107  if (-e "$path/$command" && -f "$path/$command" &&
1108  -x "$path/$command")
1109  {
1110  # Seems to be a valid executable in a "normal" directory for
1111  # binaries
1112  $command = "$path/$command";
1113  verbose($verbose_level_debug + 2,
1114  'Command seems to be a simple command in a'.
1115  ' normal directory for binaries: '.$command);
1116  $allow_untaint = 1;
1117  }
1118  }
1119  }
1120  if ($allow_untaint)
1121  {
1122  if ($command =~ /^(.*)$/)
1123  {
1124  verbose($verbose_level_debug + 1,
1125  'Untainting command: '.$command);
1126  $command = $1;
1127  $ENV{'PATH'} = '';
1128  $is_env_tainted = 0;
1129  }
1130  }
1131  return $command;
1132  }
1133 
1134 # This subroutine performs limited checking of file or directory paths and
1135 # untaints the path if it seems to be an absolute path to a normal file or
1136 # directory and contains no . or .. references. It should only be called after
1137 # careful consideration of the effects of doing so and of whether it makes
1138 # sense to override taint-mode runtime checking of the value.
1139  sub untaint_path
1140  {
1141  my $path = shift;
1142  verbose($verbose_level_debug + 2, '', 'Verifying path: '.$path);
1143  if ($path =~ /^\//)
1144  {
1145  verbose($verbose_level_debug + 2, ' - Path starts with /.');
1146  if (! ($path =~ /\/\.+\//))
1147  {
1148  verbose($verbose_level_debug + 2,
1149  ' - Path contains no dir refs.');
1150  if (-e "$path" && (-f "$path" || -d "$path"))
1151  {
1152  # Seems to be a file or directory path starting with / and
1153  # having no current/previous directory references
1154  if ($path =~ /^(.*)$/)
1155  {
1156  verbose($verbose_level_debug + 1,
1157  'Untainting path: '.$path);
1158  $path = $1;
1159  }
1160  }
1161  }
1162  }
1163  return $path;
1164  }
1165 
1166 # This subroutine does absolutely no data checking. It blindly accepts a
1167 # possibly-tainted value and "untaints" it. It should only be called after
1168 # careful consideration of the effects of doing so and of whether it makes
1169 # sense to override taint-mode runtime checking of the value.
1170  sub untaint_data
1171  {
1172  my $value = shift;
1173  if ($value =~ /^(.*)$/)
1174  {
1175  verbose($verbose_level_debug + 1, 'Untainting data: '.$value);
1176  $value = $1;
1177  }
1178  return $value;
1179  }
1180 
1181  sub reset_environment
1182  {
1183  if (!$is_env_tainted)
1184  {
1185  $is_env_tainted = 1;
1186  $ENV{'PATH'} = $old_env_path;
1187  }
1188  }
1189 
1190  sub do_backup
1191  {
1192  my $defaults_extra_file = create_defaults_extra_file;
1193  my $host_arg = '';
1194  my $port_arg = '';
1195  my $user_arg = '';
1196  if ($defaults_extra_file)
1197  {
1198  $defaults_arg = " --defaults-extra-file='$defaults_extra_file'";
1199  }
1200  else
1201  {
1202  $defaults_arg = '';
1203  }
1204  # For users running in environments where taint mode is activated (i.e.
1205  # running mythtv-setup or mythbackend as root), executing a command line
1206  # built with tainted data will fail. Therefore, try to untaint data if it
1207  # meets certain basic requirements.
1208  my $safe_mysqldump = $mysqldump;
1209  $safe_mysqldump = untaint_command($safe_mysqldump);
1210  $safe_mysqldump =~ s/'/'\\''/sg;
1211  $mysql_conf{'db_name'} = untaint_data($mysql_conf{'db_name'});
1212  $mysql_conf{'db_host'} = untaint_data($mysql_conf{'db_host'});
1213  $mysql_conf{'db_port'} = untaint_data($mysql_conf{'db_port'});
1214  $mysql_conf{'db_user'} = untaint_data($mysql_conf{'db_user'});
1215  $backup_conf{'directory'} = untaint_path($backup_conf{'directory'});
1216  # Can't use untaint_path because the filename is not a full path and the
1217  # file doesn't yet exist, anyway
1218  $backup_conf{'filename'} =~ s/'/'\\''/g;
1219  $backup_conf{'filename'} = untaint_data($backup_conf{'filename'});
1220  my $output_file = "$backup_conf{'directory'}/$backup_conf{'filename'}";
1221  $output_file =~ s/'/'\\''/sg;
1222  # Create the args for host, port, and user, shell-escaping values, as
1223  # necessary.
1224  my $safe_db_name = $mysql_conf{'db_name'};
1225  $safe_db_name =~ s/'/'\\''/g;
1226  my $safe_string;
1227  if ($mysql_conf{'db_host'})
1228  {
1229  $safe_string = $mysql_conf{'db_host'};
1230  $safe_string =~ s/'/'\\''/g;
1231  $host_arg = " --host='$safe_string'";
1232  }
1233  if ($mysql_conf{'db_port'} > 0)
1234  {
1235  $safe_string = $mysql_conf{'db_port'};
1236  $safe_string =~ s/'/'\\''/g;
1237  $port_arg = " --port='$safe_string'";
1238  }
1239  if ($mysql_conf{'db_user'})
1240  {
1241  $safe_string = $mysql_conf{'db_user'};
1242  $safe_string =~ s/'/'\\''/g;
1243  $user_arg = " --user='$safe_string'";
1244  }
1245 
1246  # Use redirects to capture stderr (for debug) and send stdout (the backup)
1247  # to a file
1248  my $command = "'${safe_mysqldump}'${defaults_arg}${host_arg}".
1249  "${port_arg}${user_arg} --add-drop-table --add-locks ".
1250  "--allow-keywords --complete-insert --extended-insert ".
1251  "--lock-tables --no-create-db --quick --add-drop-table ".
1252  "'$safe_db_name' 2>&1 1>'$output_file'";
1253  verbose($verbose_level_debug,
1254  '', 'Executing command:', $command);
1255  my $result = `$command`;
1256  my $exit = $? >> 8;
1257  verbose($verbose_level_debug,
1258  '', "$mysqldump exited with status: $exit");
1259  verbose($verbose_level_debug,
1260  "$mysqldump output:", $result) if ($exit);
1261  reset_environment;
1262  return $exit;
1263  }
1264 
1265  sub compress_backup
1266  {
1267  if (!-e "$backup_conf{'directory'}/$backup_conf{'filename'}")
1268  {
1269  verbose($verbose_level_debug,
1270  '', 'Unable to find backup file to compress');
1271  return 1;
1272  }
1273  my $result = 0;
1274  verbose($verbose_level_debug,
1275  '', 'Attempting to compress backup file.');
1276  if ($d_compress eq $compress)
1277  {
1278  # Try to load the IO::Compress::Gzip library if available (but don't
1279  # require it)
1280  BEGIN
1281  {
1282  our $has_compress_gzip = 1;
1283  # Though this does nothing, it prevents an invalid "only used
1284  # once" warning that occurs for users without IO::Compress
1285  # installed.
1286  undef $GzipError;
1287  eval 'use IO::Compress::Gzip qw(gzip $GzipError);';
1288  if ($@)
1289  {
1290  $has_compress_gzip = 0;
1291  }
1292  }
1293  if (!$has_compress_gzip)
1294  {
1296  " - IO::Compress::Gzip is not installed.");
1297  }
1298  else
1299  {
1300  if (-e "$backup_conf{'directory'}/$backup_conf{'filename'}.gz")
1301  {
1303  '', 'A file whose name is the backup filename'.
1304  ' with the \'.gz\' extension already',
1305  'exists. Leaving backup uncompressed.');
1306  return 1;
1307  }
1309  " - Compressing backup file with IO::Compress::Gzip.");
1310  $result = gzip(
1311  "$backup_conf{'directory'}/$backup_conf{'filename'}" =>
1312  "$backup_conf{'directory'}/$backup_conf{'filename'}.gz");
1313  if ((defined($result)) &&
1314  (-e "$backup_conf{'directory'}/".
1315  "$backup_conf{'filename'}.gz"))
1316  {
1317  # For users running in environments where taint mode is
1318  # activated (i.e. running mythtv-setup or mythbackend as
1319  # root), unlinking a file whose path is built with tainted data
1320  # will fail. Therefore, try to untaint the path if it meets
1321  # certain basic requirements.
1322  my $uncompressed_file = $backup_conf{'directory'}."/".
1323  $backup_conf{'filename'};
1324  $uncompressed_file = untaint_path($uncompressed_file);
1325  $uncompressed_file =~ s/'/'\\''/sg;
1327  "Unlinking uncompressed file: $uncompressed_file");
1328  unlink "$uncompressed_file";
1329  $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
1331  '', 'Successfully compressed backup to file:',
1332  "$backup_conf{'directory'}/".
1333  "$backup_conf{'filename'}");
1334  return 0;
1335  }
1337  " Error: $GzipError");
1338  }
1339  }
1340  # Try to compress the file with the compress binary.
1342  " - Compressing backup file with $compress.");
1343  my $backup_path = "$backup_conf{'directory'}/$backup_conf{'filename'}";
1344  # For users running in environments where taint mode is activated (i.e.
1345  # running mythtv-setup or mythbackend as root), executing a command line
1346  # built with tainted data will fail. Therefore, try to untaint data if it
1347  # meets certain basic requirements.
1348  $compress = untaint_command($compress);
1349  $compress =~ s/'/'\\''/sg;
1350  $backup_path = untaint_path($backup_path);
1351  $backup_path =~ s/'/'\\''/sg;
1352  my $command = "'$compress' '$backup_path' 2>&1";
1354  '', 'Executing command:', $command);
1355  my $output = `$command`;
1356  my $exit = $? >> 8;
1358  '', "$compress exited with status: $exit");
1359  if ($exit)
1360  {
1362  "$compress output:", $output);
1363  }
1364  else
1365  {
1366  $backup_conf{'filename'} = "$backup_conf{'filename'}.gz";
1367  }
1368  reset_environment;
1369  return $exit;
1370  }
1371 
1372  sub rotate_backups
1373  {
1374  if (($rotate < 1) || (!defined($rotateglob)) || (!$rotateglob))
1375  {
1377  '', 'Backup file rotation disabled.');
1378  return 0;
1379  }
1381  '', 'Rotating backups.');
1383  '', 'Searching for files matching pattern:',
1384  "$backup_conf{'directory'}/$rotateglob");
1385  my @files = <$backup_conf{'directory'}/$rotateglob>;
1386  my @sorted_files = sort { lc($a) cmp lc($b) } @files;
1387  my $num_files = @sorted_files;
1389  " - Found $num_files matching files.");
1390  $num_files = $num_files - $rotate;
1391  $num_files = 0 if ($num_files < 0);
1392  verbose($verbose_level_debug,
1393  '', "Deleting $num_files and keeping (up to) $rotate backup".
1394  ' files.');
1395  my $index = 0;
1396  foreach my $file (@sorted_files)
1397  {
1398  if ($index++ < $num_files)
1399  {
1400  if ($file eq
1401  "$backup_conf{'directory'}/$backup_conf{'filename'}")
1402  {
1403  # This is the just-created backup. Warn the user that older
1404  # backups with newer schema versions may cause rotation to
1405  # fail.
1406  verbose($verbose_level_debug,
1407  '', 'WARNING: You seem to have reverted to an'.
1408  ' older database schema version.',
1409  'You should move all backups from newer schema'.
1410  ' versions to another directory or',
1411  'delete them to prevent your new backups from'.
1412  ' being deleted on rotation.', '');
1413  verbose($verbose_level_debug,
1414  " - Keeping backup file: $file");
1415 
1416  }
1417  else
1418  {
1419  verbose($verbose_level_debug,
1420  " - Deleting old backup file: $file");
1421  # For users running in environments where taint mode is
1422  # activated (i.e. running mythtv-setup or mythbackend as
1423  # root), unlinking a file whose path is built with tainted data
1424  # will fail. Therefore, try to untaint the path if it meets
1425  # certain basic requirements.
1426  $file = untaint_path($file);
1427  $file =~ s/'/'\\''/sg;
1428  unlink "$file";
1429  }
1430  }
1431  else
1432  {
1433  verbose($verbose_level_debug,
1434  " - Keeping backup file: $file");
1435  }
1436  }
1437  return 1;
1438  }
1439 
1440 ##############################################################################
1441 # Main functionality
1442 ##############################################################################
1443 
1444 # The first argument after option parsing, if it exists, should be a database
1445 # information file.
1447 
1451 
1453 
1455  if ($backup_xmltvids)
1456  {
1457  $status = do_xmltvid_backup;
1458  }
1459  else
1460  {
1461  $status = do_backup;
1462  if (!$status)
1463  {
1464  compress_backup;
1465  rotate_backups;
1466  }
1467  }
1468 
1469  $dbh->disconnect if (defined($dbh));
1470 
1471  exit $status;
1472