001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2006 Continuent Inc.
004: * Contact: sequoia@continuent.org
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * Initial developer(s): Mykola Paliyenko.
019: * Contributor(s): Emmanuel Cecchet, Stephane Giron
020: */package org.continuent.sequoia.controller.backup.backupers;
021:
022: import java.io.BufferedReader;
023: import java.io.BufferedWriter;
024: import java.io.File;
025: import java.io.FileInputStream;
026: import java.io.FileOutputStream;
027: import java.io.FileWriter;
028: import java.io.IOException;
029: import java.io.InputStreamReader;
030: import java.sql.Connection;
031: import java.sql.ResultSet;
032: import java.sql.SQLException;
033: import java.util.ArrayList;
034: import java.util.Date;
035: import java.util.Iterator;
036: import java.util.regex.Matcher;
037: import java.util.regex.Pattern;
038:
039: import org.continuent.sequoia.common.exceptions.BackupException;
040: import org.continuent.sequoia.common.exceptions.UnreachableBackendException;
041: import org.continuent.sequoia.common.log.Trace;
042: import org.continuent.sequoia.controller.backend.DatabaseBackend;
043: import org.continuent.sequoia.controller.connection.PooledConnection;
044: import org.continuent.sequoia.controller.connection.SimpleConnectionManager;
045:
046: /**
047: * MySQL backuper inspired from the PostgreSQL backuper.
048: * <p>
049: * Options for this backuper are:
050: *
051: * <pre>
052: * - bindir: path to mysqldump binary
053: * - dumpServer: address to bind the dump server
054: * </pre>
055: *
056: * @author <a href="mailto:mpaliyenko@gmail.com">Mykola Paliyenko</a>
057: * @author <a href="mailto:emmanuel.cecchet@continuent.com">Emmanuel Cecchet</a>
058: * @author <a href="mailto:stephane.giron@continuent.com">Stephane Giron</a>
059: */
060: public class MySQLBackuper extends AbstractBackuper {
061: private static final String DEFAULT_MYSQL_PORT = "3306";
062:
063: private static final String DEFAULT_MYSQL_HOST = "localhost";
064:
065: static Trace logger = Trace
066: .getLogger(MySQLBackuper.class.getName());
067: /** end user logger */
068: static Trace endUserLogger = Trace
069: .getLogger("org.continuent.sequoia.enduser");
070:
071: /** CommandExec instance for running native commands. * */
072: protected NativeCommandExec nativeCmdExec = new NativeCommandExec();
073:
074: /**
075: * Creates a new <code>MySQLBackuper</code> object
076: */
077: public MySQLBackuper() {
078: }
079:
080: /**
081: * @see org.continuent.sequoia.controller.backup.Backuper#getDumpFormat()
082: */
083: public String getDumpFormat() {
084: return "MySQL raw dump";
085: }
086:
087: /**
088: * @see org.continuent.sequoia.controller.backup.Backuper#backup(org.continuent.sequoia.controller.backend.DatabaseBackend,
089: * java.lang.String, java.lang.String, java.lang.String,
090: * java.lang.String, java.util.ArrayList)
091: */
092: public Date backup(DatabaseBackend backend, String login,
093: String password, String dumpName, String path,
094: ArrayList tables) throws BackupException {
095: String url = backend.getURL();
096: if (!url.startsWith("jdbc:mysql:")) {
097: throw new BackupException("Unsupported db url " + url);
098: }
099: MySQLUrlInfo info = new MySQLUrlInfo(url);
100:
101: try {
102: File pathDir = new File(path);
103: if (!pathDir.exists()) {
104: pathDir.mkdirs();
105: pathDir.mkdir();
106: }
107: String dumpPath = getDumpPhysicalPath(path, dumpName);
108: if (logger.isDebugEnabled()) {
109: logger.debug("Dumping " + backend.getURL() + " in "
110: + dumpPath);
111: }
112:
113: String executablePath = (optionsMap.containsKey("bindir") ? (String) optionsMap
114: .get("bindir")
115: + File.separator
116: : "")
117: + "mysqldump";
118:
119: int exitValue = safelyExecuteNativeCommand(executablePath
120: + getBackupStoredProceduresOption(executablePath)
121: + " -h " + info.getHost() + " --port="
122: + info.getPort() + " -u" + login + " --password="
123: + password + " " + info.getDbName(), dumpPath, true);
124:
125: if (exitValue != 0) {
126: printErrors();
127: throw new BackupException(
128: "mysqldump execution did not complete successfully!");
129: }
130: performPostBackup(backend, login, password, dumpPath);
131: } catch (Exception e) {
132: String msg = "Error while performing backup";
133: logger.error(msg, e);
134: throw new BackupException(msg, e);
135: }
136:
137: return new Date();
138: }
139:
140: /**
141: * Perform all post backup operations that are required :
142: * <ul>
143: * <li>save auto-generated keys values</li>
144: * </ul>
145: *
146: * @param backend backend that is used for the backup
147: * @param login login that is used
148: * @param password password that is used
149: * @param dumpPath path and file where the backup file is generated
150: * @throws SQLException if an exception occurs when querying the database
151: * @throws UnreachableBackendException if a connection cannot be taken on this
152: * backend
153: * @throws IOException if a problem occurs when writing into the backup file
154: */
155: private void performPostBackup(DatabaseBackend backend,
156: String login, String password, String dumpPath)
157: throws SQLException, UnreachableBackendException,
158: IOException {
159: BufferedWriter out = null;
160:
161: // If there are any auto-generated keys on tables, the backup will not
162: // contain these. We need to save it at the end of the dump to be able to
163: // restart the "auto_generated" column at the right value.
164: SimpleConnectionManager simpleConnectionManager = new SimpleConnectionManager(
165: backend.getURL(), backend.getName(), login, password,
166: backend.getDriverPath(), backend.getDriverClassName());
167: try {
168: simpleConnectionManager.initializeConnections();
169: PooledConnection pooledConn = simpleConnectionManager
170: .getConnection();
171: Connection conn = pooledConn.getConnection();
172:
173: ResultSet rs = conn.createStatement().executeQuery(
174: "show table status");
175: while (rs.next()) {
176: long lastAutoIncrement = rs.getLong("Auto_increment");
177: if (lastAutoIncrement > 1) {
178: if (out == null)
179: out = new BufferedWriter(new FileWriter(
180: dumpPath, true));
181:
182: out.write("ALTER TABLE `"
183: + rs.getString("Name")
184: .replaceAll("`", "``")
185: + "` AUTO_INCREMENT=" + lastAutoIncrement
186: + ";");
187:
188: out.newLine();
189: }
190:
191: }
192: if (out != null) {
193: out.flush();
194: out.close();
195: }
196: rs.close();
197: conn.close();
198: } finally {
199: simpleConnectionManager.finalizeConnections();
200: }
201: }
202:
203: /**
204: * Returns the option to supply to mysql so that it backups stored-procedures
205: * and functions in the dump. This can be either "" (mysql versions prior to
206: * 5.0.13 do not know about stored procedures) or " --routines" (mysql 5.0.13
207: * and above).
208: *
209: * @param executablePath the path to the mysql utility used for making dumps
210: * @return the option to supply to mysql so that it backups strored-procedures
211: * and functions in the dump
212: * @throws IOException in case of error when communicating with the
213: * sub-process.
214: * @throws IllegalArgumentException if no version information can be found in
215: * 'executablePath'
216: */
217: private static String getBackupStoredProceduresOption(
218: String executablePath) throws IOException,
219: IllegalArgumentException {
220: int majorVersion = Integer
221: .parseInt(getMajorVersion(executablePath));
222: if (majorVersion < 5)
223: return "";
224: else if (majorVersion == 5) {
225: String minorVersionString = getMinorVersion(executablePath);
226: float minorVersion = Float.parseFloat(minorVersionString);
227: if (minorVersion < 1) {
228: // --routines supported from v5.0.13 upwards
229: // Need to check for extra value (i.e. 0.9 < 0.13)
230: String extraVersionString;
231: try {
232: extraVersionString = minorVersionString
233: .substring(minorVersionString.indexOf('.') + 1);
234: } catch (IndexOutOfBoundsException e) { // No minor version number
235: return "";
236: }
237: float extraVersion = Float
238: .parseFloat(extraVersionString);
239: if (extraVersion < 13)
240: return "";
241: }
242: }
243:
244: return " --routines";
245: }
246:
247: /**
248: * Returns the major version number for specified mysql native utility.
249: *
250: * @param executablePath the path to the mysql native utility
251: * @return the major version number for specified mysql native utility. *
252: * @throws IOException in case of error when communicating with the
253: * sub-process.
254: * @throws IllegalArgumentException if no version information can be found in
255: * 'executablePath'
256: */
257: private static String getMajorVersion(String executablePath)
258: throws IOException, IllegalArgumentException {
259: Process p = Runtime.getRuntime().exec(
260: executablePath + " --version");
261: BufferedReader pout = new BufferedReader(new InputStreamReader(
262: p.getInputStream()));
263:
264: String versionString = pout.readLine();
265: // sample version string:
266: // "/usr/bin/mysql Ver 14.12 Distrib 5.0.18, for pc-linux-gnu (i686) using
267: // readline 5.0"
268: Pattern regex = Pattern
269: .compile("mysql.*Ver ([0-9.]*) Distrib ([0-9])\\.([0-9.]*)");
270: Matcher m = regex.matcher(versionString);
271: if (!m.find())
272: throw new IllegalArgumentException(
273: "Can not find version information for "
274: + executablePath);
275:
276: return m.group(2);
277: }
278:
279: /**
280: * Returns the minor version number for specified mysql native utility.
281: *
282: * @param executablePath the path to the mysql native utility
283: * @return the minor version number for specified mysql native utility. *
284: * @throws IOException in case of error when communicating with the
285: * sub-process.
286: * @throws IllegalArgumentException if no version information can be found in
287: * 'executablePath'
288: */
289: private static String getMinorVersion(String executablePath)
290: throws IOException, IllegalArgumentException {
291: Process p = Runtime.getRuntime().exec(
292: executablePath + " --version");
293: BufferedReader pout = new BufferedReader(new InputStreamReader(
294: p.getInputStream()));
295:
296: String versionString = pout.readLine();
297: // sample version string:
298: // "/usr/bin/mysql Ver 14.12 Distrib 5.0.18, for pc-linux-gnu (i686) using
299: // readline 5.0"
300: Pattern regex = Pattern
301: .compile("mysql.*Ver ([0-9.]*) Distrib ([0-9])\\.([0-9.]*)");
302: Matcher m = regex.matcher(versionString);
303: if (!m.find())
304: throw new IllegalArgumentException(
305: "Can not find version information for "
306: + executablePath);
307:
308: return m.group(3);
309: }
310:
311: /**
312: * @see org.continuent.sequoia.controller.backup.Backuper#restore(org.continuent.sequoia.controller.backend.DatabaseBackend,
313: * java.lang.String, java.lang.String, java.lang.String,
314: * java.lang.String, java.util.ArrayList)
315: */
316: public void restore(DatabaseBackend backend, String login,
317: String password, String dumpName, String path,
318: ArrayList tables) throws BackupException {
319: String url = backend.getURL();
320: if (!url.startsWith("jdbc:mysql:")) {
321: throw new BackupException("Unsupported db url " + url);
322: }
323: MySQLUrlInfo info = new MySQLUrlInfo(url);
324: try {
325: File pathDir = new File(path);
326: if (!pathDir.exists()) {
327: pathDir.mkdirs();
328: pathDir.mkdir();
329: }
330: String dumpPath = getDumpPhysicalPath(path, dumpName);
331: if (logger.isDebugEnabled()) {
332: logger.debug("Restoring " + backend.getURL() + " from "
333: + dumpPath);
334: }
335:
336: String executablePath = (optionsMap.containsKey("bindir") ? (String) optionsMap
337: .get("bindir")
338: + File.separator
339: : "");
340:
341: // Drop the database if it already exists
342: if (logger.isDebugEnabled())
343: logger.debug("Dropping database '" + info.getDbName()
344: + "'");
345:
346: String mysqladminExecutablePath = executablePath
347: + "mysqladmin";
348: if (executeNativeCommand(mysqladminExecutablePath + " -h "
349: + info.getHost() + " --port=" + info.getPort()
350: + " -f -u" + login + " --password=" + password
351: + " drop " + info.getDbName()) != 0) {
352: // Errors can happen there, e.g. if the database does not exist yet.
353: // Just log them, and carry-on...
354: printErrors();
355: }
356:
357: // Create database
358: if (logger.isDebugEnabled())
359: logger.debug("Creating database '" + info.getDbName()
360: + "'");
361:
362: if (executeNativeCommand(mysqladminExecutablePath + " -h "
363: + info.getHost() + " --port=" + info.getPort()
364: + " -f -u" + login + " --password=" + password
365: + " create " + info.getDbName()) != 0) {
366: // Errors can happen there, e.g. if the database does not exist yet.
367: // Just log them, and carry-on...
368: printErrors();
369: throw new BackupException("Failed to create database '"
370: + info.getDbName() + "'");
371: }
372:
373: // Load dump
374: String mysqlExecutablePath = executablePath + "mysql";
375: int exitValue = safelyExecuteNativeCommand(
376: mysqlExecutablePath + " -h " + info.getHost()
377: + " --port=" + info.getPort() + " -u"
378: + login + " --password=" + password + " "
379: + info.getDbName(), dumpPath, false);
380:
381: if (exitValue != 0) {
382: printErrors();
383: throw new BackupException(
384: "mysql execution did not complete successfully!");
385: }
386: } catch (Exception e) {
387: String msg = "Error while performing restore";
388: logger.error(msg, e);
389: throw new BackupException(msg, e);
390: }
391: }
392:
393: /**
394: * @see org.continuent.sequoia.controller.backup.Backuper#deleteDump(java.lang.String,
395: * java.lang.String)
396: */
397: public void deleteDump(String path, String dumpName)
398: throws BackupException {
399: File toRemove = new File(getDumpPhysicalPath(path, dumpName));
400: if (logger.isDebugEnabled())
401: logger.debug("Deleting compressed dump " + toRemove);
402: toRemove.delete();
403: }
404:
405: /**
406: * Get the dump physical path from its logical name
407: *
408: * @param path the path where the dump is stored
409: * @param dumpName dump logical name
410: * @return path to zip file
411: */
412: private String getDumpPhysicalPath(String path, String dumpName) {
413: return path + File.separator + dumpName;
414: }
415:
416: /**
417: * Allow to parse MySQL URL.
418: */
419: protected class MySQLUrlInfo {
420: private boolean isLocal;
421:
422: private String host;
423:
424: private String port;
425:
426: private String dbName;
427:
428: // Used to parse url
429: private Pattern pattern = Pattern
430: .compile("jdbc:mysql:((//([a-zA-Z0-9_\\-.]+|\\[[a-fA-F0-9:]+])((:(\\d+))|))/|)([a-zA-Z][a-zA-Z0-9_\\-]*)(\\?.*)?");
431:
432: Matcher matcher;
433:
434: /**
435: * Creates a new <code>MySQLUrlInfo</code> object, used to parse the
436: * postgresql jdbc options. If host and/or port aren't specified, will
437: * default to localhost:3306. Note that database name must be specified.
438: *
439: * @param url the MySQL JDBC url to parse
440: */
441: public MySQLUrlInfo(String url) {
442: matcher = pattern.matcher(url);
443:
444: if (matcher.matches()) {
445: if (matcher.group(3) != null)
446: host = matcher.group(3);
447: else
448: host = DEFAULT_MYSQL_HOST;
449:
450: if (matcher.group(6) != null)
451: port = matcher.group(6);
452: else
453: port = DEFAULT_MYSQL_PORT;
454:
455: dbName = matcher.group(7);
456: }
457: }
458:
459: /**
460: * Gets the HostParameters of this postgresql jdbc url as a String that can
461: * be used to pass into cmd line/shell calls.
462: *
463: * @return a string that can be used to pass into a cmd line/shell call.
464: */
465: public String getHostParametersString() {
466: if (isLocal) {
467: return "";
468: } else {
469: return "-h " + host + " --port=" + port;
470: }
471: }
472:
473: /**
474: * Gets the database name part of this postgresql jdbc url.
475: *
476: * @return the database name part of this postgresql jdbc url.
477: */
478: public String getDbName() {
479: return dbName;
480: }
481:
482: /**
483: * Gets the host part of this postgresql jdbc url.
484: *
485: * @return the host part of this postgresql jdbc url.
486: */
487: public String getHost() {
488: return host;
489: }
490:
491: /**
492: * Gets the port part of this postgresql jdbc url.
493: *
494: * @return the port part of this postgresql jdbc url.
495: */
496: public String getPort() {
497: return port;
498: }
499:
500: /**
501: * Checks whether this postgresql jdbc url refers to a local db or not, i.e.
502: * has no host specified, e.g. jdbc:postgresql:myDb.
503: *
504: * @return true if this postgresql jdbc url has no host specified, i.e.
505: * refers to a local db.
506: */
507: public boolean isLocal() {
508: return isLocal;
509: }
510:
511: }
512:
513: /**
514: * Executes a native operating system command. Output of these commands is
515: * captured and logged.
516: *
517: * @param command String of command to execute
518: * @return 0 if successful, any number otherwise
519: * @throws IOException
520: * @throws InterruptedException
521: */
522: protected int executeNativeCommand(String command)
523: throws IOException, InterruptedException {
524: return nativeCmdExec.executeNativeCommand(command, null, null,
525: 0, getIgnoreStdErrOutput());
526: }
527:
528: /**
529: * Executes a native operating system dump or restore command. Output of these
530: * commands is carefully captured and logged.
531: *
532: * @param command String of command to execute
533: * @param dumpPath path to the dump file (either source or dest, depending on
534: * specified 'backup' parameter.
535: * @param backup specifies whether we are doing a backup (true) or a restore
536: * (false). This sets the semantics of the dumpPath parameter to
537: * resp. dest or source file name.
538: * @return 0 if successful, 1 otherwise
539: */
540: protected int safelyExecuteNativeCommand(String command,
541: String dumpPath, boolean backup) throws IOException {
542: if (backup)
543: return nativeCmdExec.safelyExecNativeCommand(command, null,
544: new FileOutputStream(dumpPath), 0,
545: getIgnoreStdErrOutput()) ? 0 : 1;
546: else {
547: FileInputStream dumpStream = new FileInputStream(dumpPath);
548: return nativeCmdExec.safelyExecNativeCommand(command,
549: new NativeCommandInputSource(dumpStream), null, 0,
550: getIgnoreStdErrOutput()) ? 0 : 1;
551: }
552: }
553:
554: protected void printErrors() {
555: ArrayList errors = nativeCmdExec.getStderr();
556: Iterator it = errors.iterator();
557: while (it.hasNext()) {
558: String msg = (String) it.next();
559: logger.info(msg);
560: endUserLogger.error(msg);
561: }
562: }
563: }
|