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