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