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     */
018    package org.apache.hadoop.hdfs.server.namenode.snapshot;
019    
020    import java.io.DataInput;
021    import java.io.IOException;
022    import java.util.ArrayList;
023    import java.util.Collections;
024    import java.util.HashMap;
025    import java.util.List;
026    import java.util.Map;
027    import java.util.concurrent.atomic.AtomicInteger;
028    
029    import org.apache.hadoop.hdfs.DFSUtil;
030    import org.apache.hadoop.hdfs.protocol.SnapshotException;
031    import org.apache.hadoop.hdfs.protocol.SnapshottableDirectoryStatus;
032    import org.apache.hadoop.hdfs.server.namenode.FSDirectory;
033    import org.apache.hadoop.hdfs.server.namenode.FSImageFormat;
034    import org.apache.hadoop.hdfs.server.namenode.FSNamesystem;
035    import org.apache.hadoop.hdfs.server.namenode.INode;
036    import org.apache.hadoop.hdfs.server.namenode.INode.BlocksMapUpdateInfo;
037    import org.apache.hadoop.hdfs.server.namenode.INodeDirectory;
038    import org.apache.hadoop.hdfs.server.namenode.INodesInPath;
039    import 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     */
053    public 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    }