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