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