001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2005 Emic Networks.
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): Emmanuel Cecchet.
019: * Contributor(s): ______________________.
020: */package org.continuent.sequoia.controller.backup;
021:
022: import java.io.BufferedInputStream;
023: import java.io.BufferedOutputStream;
024: import java.io.File;
025: import java.io.FileInputStream;
026: import java.io.FileOutputStream;
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.io.ObjectInputStream;
030: import java.io.ObjectOutputStream;
031: import java.io.OutputStream;
032: import java.net.InetAddress;
033: import java.net.InetSocketAddress;
034: import java.net.ServerSocket;
035: import java.net.Socket;
036: import java.util.HashMap;
037: import java.util.Iterator;
038: import java.util.Set;
039:
040: import org.continuent.sequoia.common.exceptions.BackupException;
041: import org.continuent.sequoia.common.log.Trace;
042: import org.continuent.sequoia.common.xml.DatabasesXmlTags;
043: import org.continuent.sequoia.common.xml.XmlComponent;
044:
045: /**
046: * This class defines a BackupManager that is responsible for registering
047: * backupers and retrieving them as needed for backup/restore operations.
048: *
049: * @author <a href="mailto:emmanuel.cecchet@emicnetworks.com">Emmanuel Cecchet</a>
050: * @version 1.0
051: */
052: public class BackupManager implements XmlComponent {
053: static Trace logger = Trace
054: .getLogger(BackupManager.class.getName());
055:
056: /**
057: * This is a HashMap of backuperName -> Backuper HashMap<String,Backuper>
058: */
059: private HashMap backupers;
060:
061: /**
062: * Creates a new <code>BackupManager</code> object
063: */
064: public BackupManager() {
065: backupers = new HashMap();
066: }
067:
068: /**
069: * Retrieve a backuper given its name. If the backuper has not been registered
070: * null is returned.
071: *
072: * @param name the backuper to look for
073: * @return the backuper or null if not found
074: */
075: public synchronized Backuper getBackuperByName(String name) {
076: return (Backuper) backupers.get(name);
077: }
078:
079: /**
080: * Get the names of the <code>Backupers</code> available from this
081: * <code>BackupManager</code>.
082: *
083: * @return an (possibly 0-sized) array of <code>String</code> representing
084: * the name of the <code>Backupers</code>
085: */
086: public synchronized String[] getBackuperNames() {
087: Set backuperNames = backupers.keySet();
088: return (String[]) backuperNames
089: .toArray(new String[backuperNames.size()]);
090: }
091:
092: /**
093: * Get the first backuper that supports the given dump format. If no backuper
094: * supporting that format can be found, null is returned.
095: *
096: * @param format the dump format that the backuper must handle
097: * @return a backuper or null if not found
098: */
099: public synchronized Backuper getBackuperByFormat(String format) {
100: if (format == null)
101: return null;
102: for (Iterator iter = backupers.values().iterator(); iter
103: .hasNext();) {
104: Backuper b = (Backuper) iter.next();
105: if (format.equals(b.getDumpFormat()))
106: return b;
107: }
108: return null;
109: }
110:
111: /**
112: * Register a new backuper under a logical name.
113: *
114: * @param name backuper logical name
115: * @param backuper the backuper instance
116: * @throws BackupException if a backuper is null or a backuper has already
117: * been registered with the given name.
118: */
119: public synchronized void registerBackuper(String name,
120: Backuper backuper) throws BackupException {
121: // Sanity checks
122: if (backupers.containsKey(name))
123: throw new BackupException(
124: "A backuper has already been registered with name "
125: + name);
126: if (backuper == null)
127: throw new BackupException(
128: "Trying to register a null backuper under name "
129: + name);
130: String dumpFormat = backuper.getDumpFormat();
131: if (dumpFormat == null)
132: throw new BackupException(
133: "Invalid null dump format for backuper " + name);
134:
135: // Check that an already loaded backuper does no already handle that format
136: for (Iterator iter = backupers.values().iterator(); iter
137: .hasNext();) {
138: Backuper b = (Backuper) iter.next();
139: if (b.getDumpFormat().equals(dumpFormat))
140: throw new BackupException("Backuper " + b.getClass()
141: + " already handles " + dumpFormat
142: + " dump format");
143: }
144:
145: if (logger.isInfoEnabled())
146: logger.info("Registering backuper " + name
147: + " to handle format " + dumpFormat);
148:
149: backupers.put(name, backuper);
150: }
151:
152: /**
153: * Unregister a Backuper given its logical name.
154: *
155: * @param name the name of the backuper to unregister
156: * @return true if the backuper was removed successfully, false if it was not
157: * registered
158: */
159: public synchronized boolean unregisterBackuper(String name) {
160: Object backuper = backupers.remove(name);
161:
162: if (logger.isInfoEnabled() && (backuper != null))
163: logger.info("Unregistering backuper " + name
164: + " that handled format "
165: + ((Backuper) backuper).getDumpFormat());
166:
167: return backuper != null;
168: }
169:
170: /**
171: * @see org.continuent.sequoia.common.xml.XmlComponent#getXml()
172: */
173: public synchronized String getXml() {
174: StringBuffer sb = new StringBuffer("<"
175: + DatabasesXmlTags.ELT_Backup + "> ");
176: for (Iterator iter = backupers.keySet().iterator(); iter
177: .hasNext();) {
178: String backuperName = (String) iter.next();
179: Backuper b = (Backuper) backupers.get(backuperName);
180: sb.append("<" + DatabasesXmlTags.ELT_Backuper + " "
181: + DatabasesXmlTags.ATT_backuperName + "=\""
182: + backuperName + "\" "
183: + DatabasesXmlTags.ATT_className + "=\""
184: + b.getClass().getName() + "\" "
185: + DatabasesXmlTags.ATT_options + "=\""
186: + b.getOptions() + "\" />");
187: }
188: sb.append("</" + DatabasesXmlTags.ELT_Backup + ">");
189: return sb.toString();
190: }
191:
192: /**
193: * Fetches a dumpFile from a remote dumpFileServer. The remote dump file to
194: * fetch is specified by its name and path. The connection to the remote
195: * dumpFileServer is initiated and authenticated using the specified
196: * DumpTransferInfo. The dump file is then fetched and stored locally at the
197: * same path as it was on the remote site.
198: *
199: * @param info the DumpTransferInfo specifying where to get the dump from.
200: * @param path the path where the dump is stored (both remote and local).
201: * @param dumpName the name of the remote dump to fetch.
202: * @throws IOException if a networking error occurs during the fetch process.
203: * @throws BackupException if an authentication error occurs, or if parameters
204: * are invalid.
205: */
206: public static void fetchDumpFile(DumpTransferInfo info,
207: String path, String dumpName) throws IOException,
208: BackupException {
209: //
210: // Phase 1: talk to dump server using it's very smart protocol
211: //
212: logger.info("Dump fetch starting from "
213: + info.getBackuperServerAddress());
214:
215: Socket soc = new Socket();
216:
217: soc.connect(info.getBackuperServerAddress());
218:
219: ObjectOutputStream oos = new ObjectOutputStream(soc
220: .getOutputStream());
221:
222: oos.writeLong(info.getSessionKey());
223: oos.writeObject(path);
224: oos.writeObject(dumpName);
225:
226: // end of very smart protocol: read server response.
227: InputStream is = new BufferedInputStream(soc.getInputStream());
228: int response = is.read();
229: if (response != 0xEC) // server replies "EC" to say it's happy to carry on.
230: throw new BackupException("bad response from dump server");
231:
232: //
233: // Phase 2: protocolar ablutions ok, go copy the stream into a local file
234: //
235: logger.info("Dump fetch authentication ok. Fetching " + path
236: + File.separator + dumpName);
237:
238: File thePath = new File(path);
239: if (!thePath.exists())
240: thePath.mkdirs();
241:
242: File theFile = new File(path + File.separator + dumpName);
243: theFile.createNewFile();
244:
245: OutputStream os = new BufferedOutputStream(
246: new FileOutputStream(theFile));
247: int c = is.read();
248: while (c != -1) {
249: os.write(c);
250: c = is.read();
251: }
252: os.flush();
253: os.close();
254:
255: logger.info("Dump fetch done.");
256: }
257:
258: /**
259: * Sets up a DumpFileServer for a remote client to use with fetchDumpFile.
260: *
261: * @param dumpServerIpAddress IP address of the dump server
262: * @return a DumpTransferInfo to be used by the client to connect and
263: * authenticate to this dumpFileServer.
264: * @throws IOException if the server socket can not be created.
265: */
266: public static DumpTransferInfo setupDumpFileServer(
267: String dumpServerIpAddress) throws IOException {
268: ServerSocket soc = new ServerSocket();
269: soc.bind(null);
270:
271: InetSocketAddress dumpServerAddress;
272: if (dumpServerIpAddress != null) {
273: logger.info("Using provided dump-server address: "
274: + dumpServerIpAddress);
275: dumpServerAddress = new InetSocketAddress(
276: dumpServerIpAddress, soc.getLocalPort());
277: } else {
278: logger
279: .info("Using InetAddress.getLocalHost() as dump-server address: "
280: + InetAddress.getLocalHost());
281: dumpServerAddress = new InetSocketAddress(InetAddress
282: .getLocalHost(), soc.getLocalPort());
283: }
284:
285: if (dumpServerAddress.getAddress() == null)
286: throw new IOException(
287: "Cannot resolve provided IP address for dump server ("
288: + dumpServerAddress + ")");
289: else if (dumpServerAddress.getAddress().isLoopbackAddress())
290: throw new IOException(
291: "NOT setting-up a dump server on a loopback address.\n"
292: + "Please update your network configuration "
293: + "or specify option dumpServer in vdb.xml <Backuper ... option=/>");
294:
295: long sessionKey = soc.hashCode();
296: new DumpTransferServerThread(soc, sessionKey).start();
297:
298: // FIXME: sending the address we are listening to is dirty because:
299: // - it prevents us from listening to any interface/address
300: // - we may listen on some wrong interface/address!
301: // The Right Way to do this would be to bootstrap this socket from the
302: // existing inter-controller communication/addresses, this way we would be
303: // 100% sure to succeed.
304: return new DumpTransferInfo(dumpServerAddress, sessionKey);
305: }
306:
307: /**
308: * Sets up a DumpFileServer for a remote client to use with fetchDumpFile.
309: *
310: * @return a DumpTransferInfo to be used by the client to connect and
311: * authenticate to this dumpFileServer.
312: * @throws IOException if the server socket can not be created.
313: */
314: public static DumpTransferInfo setupDumpFileServer()
315: throws IOException {
316: return setupDumpFileServer(null);
317: }
318:
319: static class DumpTransferServerThread extends Thread {
320: private ServerSocket serverSocket;
321: private long sessionKey;
322:
323: public void run() {
324: try {
325: logger.info("Dump server started @ "
326: + serverSocket.getLocalSocketAddress());
327: //
328: // Wait for client to connect
329: //
330: Socket soc = serverSocket.accept();
331:
332: logger.info("Client connected to dump server from "
333: + soc.getRemoteSocketAddress());
334:
335: ObjectInputStream ois = new ObjectInputStream(soc
336: .getInputStream());
337:
338: //
339: // Phase 1: server side very smart protocol to authenticate client
340: //
341: long key = ois.readLong();
342:
343: if (key != this .sessionKey) {
344: logger.error("Bad session key from client: "
345: + soc.getRemoteSocketAddress());
346: soc.close();
347: return; // read will fail on client side
348: }
349:
350: String path = (String) ois.readObject();
351: String dumpName = (String) ois.readObject();
352:
353: File theFile = new File(path + File.separator
354: + dumpName);
355:
356: if (!theFile.exists()) {
357: logger.error("Requested dump does not exist: "
358: + theFile.getPath());
359: soc.close();
360: return;
361: }
362:
363: InputStream is = new BufferedInputStream(
364: new FileInputStream(theFile));
365: OutputStream os = new BufferedOutputStream(soc
366: .getOutputStream());
367:
368: // end of very smart protocol: return "EC" to client to say it's ok to
369: // fetch the dump
370: os.write(0xEC);
371:
372: //
373: // Phase 2: burst the dump file over the wire.
374: //
375: int c = is.read();
376: while (c != -1) {
377: os.write(c);
378: c = is.read();
379: }
380: os.flush();
381: os.close();
382:
383: logger.info("Dump server terminated.");
384: } catch (Exception e) {
385: logger.error(e);
386: }
387: }
388:
389: DumpTransferServerThread(ServerSocket serverSocket,
390: long sessionKey) {
391: setName("DumpTransfer server thread");
392: this.serverSocket = serverSocket;
393: this.sessionKey = sessionKey;
394: }
395: }
396:
397: }
|