001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  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 */
018package org.apache.hadoop.hdfs.server.namenode.snapshot;
019
020import java.io.DataInput;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.concurrent.atomic.AtomicInteger;
028
029import org.apache.hadoop.hdfs.DFSUtil;
030import org.apache.hadoop.hdfs.protocol.SnapshotException;
031import org.apache.hadoop.hdfs.protocol.SnapshottableDirectoryStatus;
032import org.apache.hadoop.hdfs.server.namenode.FSDirectory;
033import org.apache.hadoop.hdfs.server.namenode.FSImageFormat;
034import org.apache.hadoop.hdfs.server.namenode.FSNamesystem;
035import org.apache.hadoop.hdfs.server.namenode.INode;
036import org.apache.hadoop.hdfs.server.namenode.INode.BlocksMapUpdateInfo;
037import org.apache.hadoop.hdfs.server.namenode.INodeDirectory;
038import org.apache.hadoop.hdfs.server.namenode.INodesInPath;
039import org.apache.hadoop.hdfs.server.namenode.snapshot.INodeDirectorySnapshottable.SnapshotDiffInfo;
040
041/**
042 * Manage snapshottable directories and their snapshots.
043 * 
044 * This class includes operations that create, access, modify snapshots and/or
045 * snapshot-related data. In general, the locking structure of snapshot
046 * operations is: <br>
047 * 
048 * 1. Lock the {@link FSNamesystem} lock in {@link FSNamesystem} before calling
049 * into {@link SnapshotManager} methods.<br>
050 * 2. Lock the {@link FSDirectory} lock for the {@link SnapshotManager} methods
051 * if necessary.
052 */
053public class SnapshotManager implements SnapshotStats {
054  private boolean allowNestedSnapshots = false;
055  private final FSDirectory fsdir;
056  private static final int SNAPSHOT_ID_BIT_WIDTH = 24;
057
058  private final AtomicInteger numSnapshots = new AtomicInteger();
059
060  private int snapshotCounter = 0;
061  
062  /** All snapshottable directories in the namesystem. */
063  private final Map<Long, INodeDirectorySnapshottable> snapshottables
064      = new HashMap<Long, INodeDirectorySnapshottable>();
065
066  public SnapshotManager(final FSDirectory fsdir) {
067    this.fsdir = fsdir;
068  }
069
070  /** Used in tests only */
071  void setAllowNestedSnapshots(boolean allowNestedSnapshots) {
072    this.allowNestedSnapshots = allowNestedSnapshots;
073  }
074
075  private void checkNestedSnapshottable(INodeDirectory dir, String path)
076      throws SnapshotException {
077    if (allowNestedSnapshots) {
078      return;
079    }
080
081    for(INodeDirectorySnapshottable s : snapshottables.values()) {
082      if (s.isAncestorDirectory(dir)) {
083        throw new SnapshotException(
084            "Nested snapshottable directories not allowed: path=" + path
085            + ", the subdirectory " + s.getFullPathName()
086            + " is already a snapshottable directory.");
087      }
088      if (dir.isAncestorDirectory(s)) {
089        throw new SnapshotException(
090            "Nested snapshottable directories not allowed: path=" + path
091            + ", the ancestor " + s.getFullPathName()
092            + " is already a snapshottable directory.");
093      }
094    }
095  }
096
097  /**
098   * Set the given directory as a snapshottable directory.
099   * If the path is already a snapshottable directory, update the quota.
100   */
101  public void setSnapshottable(final String path, boolean checkNestedSnapshottable)
102      throws IOException {
103    final INodesInPath iip = fsdir.getINodesInPath4Write(path);
104    final INodeDirectory d = INodeDirectory.valueOf(iip.getLastINode(), path);
105    if (checkNestedSnapshottable) {
106      checkNestedSnapshottable(d, path);
107    }
108
109
110    final INodeDirectorySnapshottable s;
111    if (d.isSnapshottable()) {
112      //The directory is already a snapshottable directory.
113      s = (INodeDirectorySnapshottable)d; 
114      s.setSnapshotQuota(INodeDirectorySnapshottable.SNAPSHOT_LIMIT);
115    } else {
116      s = d.replaceSelf4INodeDirectorySnapshottable(iip.getLatestSnapshotId(),
117          fsdir.getINodeMap());
118    }
119    addSnapshottable(s);
120  }
121  
122  /** Add the given snapshottable directory to {@link #snapshottables}. */
123  public void addSnapshottable(INodeDirectorySnapshottable dir) {
124    snapshottables.put(dir.getId(), dir);
125  }
126
127  /** Remove the given snapshottable directory from {@link #snapshottables}. */
128  private void removeSnapshottable(INodeDirectorySnapshottable s) {
129    snapshottables.remove(s.getId());
130  }
131  
132  /** Remove snapshottable directories from {@link #snapshottables} */
133  public void removeSnapshottable(List<INodeDirectorySnapshottable> toRemove) {
134    if (toRemove != null) {
135      for (INodeDirectorySnapshottable s : toRemove) {
136        removeSnapshottable(s);
137      }
138    }
139  }
140
141  /**
142   * Set the given snapshottable directory to non-snapshottable.
143   * 
144   * @throws SnapshotException if there are snapshots in the directory.
145   */
146  public void resetSnapshottable(final String path) throws IOException {
147    final INodesInPath iip = fsdir.getINodesInPath4Write(path);
148    final INodeDirectory d = INodeDirectory.valueOf(iip.getLastINode(), path);
149    if (!d.isSnapshottable()) {
150      // the directory is already non-snapshottable
151      return;
152    }
153    final INodeDirectorySnapshottable s = (INodeDirectorySnapshottable) d;
154    if (s.getNumSnapshots() > 0) {
155      throw new SnapshotException("The directory " + path + " has snapshot(s). "
156          + "Please redo the operation after removing all the snapshots.");
157    }
158
159    if (s == fsdir.getRoot()) {
160      s.setSnapshotQuota(0); 
161    } else {
162      s.replaceSelf(iip.getLatestSnapshotId(), fsdir.getINodeMap());
163    }
164    removeSnapshottable(s);
165  }
166
167  /**
168  * Find the source root directory where the snapshot will be taken
169  * for a given path.
170  *
171  * @param path The directory path where the snapshot will be taken.
172  * @return Snapshottable directory.
173  * @throws IOException
174  *           Throw IOException when the given path does not lead to an
175  *           existing snapshottable directory.
176  */
177  public INodeDirectorySnapshottable getSnapshottableRoot(final String path
178      ) throws IOException {
179    final INodesInPath i = fsdir.getINodesInPath4Write(path);
180    return INodeDirectorySnapshottable.valueOf(i.getLastINode(), path);
181  }
182
183  /**
184   * Create a snapshot of the given path.
185   * It is assumed that the caller will perform synchronization.
186   *
187   * @param path
188   *          The directory path where the snapshot will be taken.
189   * @param snapshotName
190   *          The name of the snapshot.
191   * @throws IOException
192   *           Throw IOException when 1) the given path does not lead to an
193   *           existing snapshottable directory, and/or 2) there exists a
194   *           snapshot with the given name for the directory, and/or 3)
195   *           snapshot number exceeds quota
196   */
197  public String createSnapshot(final String path, String snapshotName
198      ) throws IOException {
199    INodeDirectorySnapshottable srcRoot = getSnapshottableRoot(path);
200
201    if (snapshotCounter == getMaxSnapshotID()) {
202      // We have reached the maximum allowable snapshot ID and since we don't
203      // handle rollover we will fail all subsequent snapshot creation
204      // requests.
205      //
206      throw new SnapshotException(
207          "Failed to create the snapshot. The FileSystem has run out of " +
208          "snapshot IDs and ID rollover is not supported.");
209    }
210
211    srcRoot.addSnapshot(snapshotCounter, snapshotName);
212      
213    //create success, update id
214    snapshotCounter++;
215    numSnapshots.getAndIncrement();
216    return Snapshot.getSnapshotPath(path, snapshotName);
217  }
218  
219  /**
220   * Delete a snapshot for a snapshottable directory
221   * @param path Path to the directory where the snapshot was taken
222   * @param snapshotName Name of the snapshot to be deleted
223   * @param collectedBlocks Used to collect information to update blocksMap 
224   * @throws IOException
225   */
226  public void deleteSnapshot(final String path, final String snapshotName,
227      BlocksMapUpdateInfo collectedBlocks, final List<INode> removedINodes)
228      throws IOException {
229    // parse the path, and check if the path is a snapshot path
230    // the INodeDirectorySnapshottable#valueOf method will throw Exception 
231    // if the path is not for a snapshottable directory
232    INodeDirectorySnapshottable srcRoot = getSnapshottableRoot(path);
233    srcRoot.removeSnapshot(snapshotName, collectedBlocks, removedINodes);
234    numSnapshots.getAndDecrement();
235  }
236
237  /**
238   * Rename the given snapshot
239   * @param path
240   *          The directory path where the snapshot was taken
241   * @param oldSnapshotName
242   *          Old name of the snapshot
243   * @param newSnapshotName
244   *          New name of the snapshot
245   * @throws IOException
246   *           Throw IOException when 1) the given path does not lead to an
247   *           existing snapshottable directory, and/or 2) the snapshot with the
248   *           old name does not exist for the directory, and/or 3) there exists
249   *           a snapshot with the new name for the directory
250   */
251  public void renameSnapshot(final String path, final String oldSnapshotName,
252      final String newSnapshotName) throws IOException {
253    // Find the source root directory path where the snapshot was taken.
254    // All the check for path has been included in the valueOf method.
255    final INodeDirectorySnapshottable srcRoot
256        = INodeDirectorySnapshottable.valueOf(fsdir.getINode(path), path);
257    // Note that renameSnapshot and createSnapshot are synchronized externally
258    // through FSNamesystem's write lock
259    srcRoot.renameSnapshot(path, oldSnapshotName, newSnapshotName);
260  }
261  
262  @Override
263  public int getNumSnapshottableDirs() {
264    return snapshottables.size();
265  }
266
267  @Override
268  public int getNumSnapshots() {
269    return numSnapshots.get();
270  }
271  
272  void setNumSnapshots(int num) {
273    numSnapshots.set(num);
274  }
275
276  int getSnapshotCounter() {
277    return snapshotCounter;
278  }
279
280  void setSnapshotCounter(int counter) {
281    snapshotCounter = counter;
282  }
283
284  INodeDirectorySnapshottable[] getSnapshottableDirs() {
285    return snapshottables.values().toArray(
286        new INodeDirectorySnapshottable[snapshottables.size()]);
287  }
288  
289  /**
290   * Read values of {@link #snapshotCounter}, {@link #numSnapshots}, and
291   * all snapshots from the DataInput
292   */
293  public Map<Integer, Snapshot> read(DataInput in, FSImageFormat.Loader loader
294      ) throws IOException {
295    snapshotCounter = in.readInt();
296    numSnapshots.set(in.readInt());
297    
298    // read snapshots
299    final Map<Integer, Snapshot> snapshotMap = new HashMap<Integer, Snapshot>();
300    for(int i = 0; i < numSnapshots.get(); i++) {
301      final Snapshot s = Snapshot.read(in, loader);
302      snapshotMap.put(s.getId(), s);
303    }
304    return snapshotMap;
305  }
306  
307  /**
308   * List all the snapshottable directories that are owned by the current user.
309   * @param userName Current user name.
310   * @return Snapshottable directories that are owned by the current user,
311   *         represented as an array of {@link SnapshottableDirectoryStatus}. If
312   *         {@code userName} is null, return all the snapshottable dirs.
313   */
314  public SnapshottableDirectoryStatus[] getSnapshottableDirListing(
315      String userName) {
316    if (snapshottables.isEmpty()) {
317      return null;
318    }
319    
320    List<SnapshottableDirectoryStatus> statusList = 
321        new ArrayList<SnapshottableDirectoryStatus>();
322    for (INodeDirectorySnapshottable dir : snapshottables.values()) {
323      if (userName == null || userName.equals(dir.getUserName())) {
324        SnapshottableDirectoryStatus status = new SnapshottableDirectoryStatus(
325            dir.getModificationTime(), dir.getAccessTime(),
326            dir.getFsPermission(), dir.getUserName(), dir.getGroupName(),
327            dir.getLocalNameBytes(), dir.getId(), 
328            dir.getChildrenNum(Snapshot.CURRENT_STATE_ID),
329            dir.getNumSnapshots(),
330            dir.getSnapshotQuota(), dir.getParent() == null ? 
331                DFSUtil.EMPTY_BYTES : 
332                DFSUtil.string2Bytes(dir.getParent().getFullPathName()));
333        statusList.add(status);
334      }
335    }
336    Collections.sort(statusList, SnapshottableDirectoryStatus.COMPARATOR);
337    return statusList.toArray(
338        new SnapshottableDirectoryStatus[statusList.size()]);
339  }
340  
341  /**
342   * Compute the difference between two snapshots of a directory, or between a
343   * snapshot of the directory and its current tree.
344   */
345  public SnapshotDiffInfo diff(final String path, final String from,
346      final String to) throws IOException {
347    if ((from == null || from.isEmpty())
348        && (to == null || to.isEmpty())) {
349      // both fromSnapshot and toSnapshot indicate the current tree
350      return null;
351    }
352
353    // Find the source root directory path where the snapshots were taken.
354    // All the check for path has been included in the valueOf method.
355    INodesInPath inodesInPath = fsdir.getINodesInPath4Write(path.toString());
356    final INodeDirectorySnapshottable snapshotRoot = INodeDirectorySnapshottable
357        .valueOf(inodesInPath.getLastINode(), path);
358    
359    return snapshotRoot.computeDiff(from, to);
360  }
361  
362  public void clearSnapshottableDirs() {
363    snapshottables.clear();
364  }
365
366  /**
367   * Returns the maximum allowable snapshot ID based on the bit width of the
368   * snapshot ID.
369   *
370   * @return maximum allowable snapshot ID.
371   */
372   public int getMaxSnapshotID() {
373    return ((1 << SNAPSHOT_ID_BIT_WIDTH) - 1);
374  }
375}