You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ozone.apache.org by sa...@apache.org on 2023/04/27 05:30:21 UTC

[ozone] branch master updated: HDDS-7828. Make Ozone fs rm symbolic links command support posix behaviour (#4246)

This is an automated email from the ASF dual-hosted git repository.

sammichen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 6a7c0706cb HDDS-7828. Make Ozone fs rm symbolic links command support posix behaviour (#4246)
6a7c0706cb is described below

commit 6a7c0706cb86baad0f375b966e050e8cf4f2a5a3
Author: Neil Joshi <ne...@gmail.com>
AuthorDate: Wed Apr 26 23:30:14 2023 -0600

    HDDS-7828. Make Ozone fs rm symbolic links command support posix behaviour (#4246)
---
 .../hadoop/fs/ozone/TestRootedOzoneFileSystem.java | 192 ++++++++++++++++++-
 .../fs/ozone/BasicRootedOzoneFileSystem.java       |  39 +++-
 .../org/apache/hadoop/fs/ozone/OzoneFsDelete.java  | 206 +++++++++++++++++++++
 .../org/apache/hadoop/fs/ozone/OzoneFsShell.java   |  10 +
 .../hadoop/fs/ozone/TestBasicOzoneFileSystems.java |  12 ++
 .../apache/hadoop/fs/ozone/TestOzoneFsShell.java   |  68 +++++++
 6 files changed, 521 insertions(+), 6 deletions(-)

diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystem.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystem.java
index 1f8c86ec8c..32f308790e 100644
--- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystem.java
+++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestRootedOzoneFileSystem.java
@@ -70,6 +70,7 @@ import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLIdentityType;
 import org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType;
 import org.apache.hadoop.ozone.security.acl.OzoneAclConfig;
 import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.util.ToolRunner;
 import org.apache.ozone.test.GenericTestUtils;
 import org.apache.ozone.test.LambdaTestUtils;
 import org.junit.After;
@@ -121,8 +122,9 @@ import static org.apache.hadoop.ozone.OzoneConsts.OZONE_URI_DELIMITER;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ENABLE_OFS_SHARED_TMP_DIR;
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.BUCKET_NOT_FOUND;
-import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.KEY_NOT_FOUND;
 import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.PERMISSION_DENIED;
+import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLUME_NOT_FOUND;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.READ;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.WRITE;
 import static org.apache.hadoop.ozone.security.acl.IAccessAuthorizer.ACLType.DELETE;
@@ -1521,6 +1523,194 @@ public class TestRootedOzoneFileSystem {
     Assert.assertFalse(volumeExist(volumeStr3));
   }
 
+  private void createSymlinkSrcDestPaths(String srcVol,
+      String srcBucket, String destVol, String destBucket) throws IOException {
+    // src srcVol/srcBucket
+    Path volumeSrcPath = new Path(OZONE_URI_DELIMITER + srcVol);
+    Path bucketSrcPath = Path.mergePaths(volumeSrcPath,
+        new Path(OZONE_URI_DELIMITER + srcBucket));
+    fs.mkdirs(volumeSrcPath);
+    OzoneVolume volume = objectStore.getVolume(srcVol);
+    Assert.assertEquals(srcVol, volume.getName());
+    fs.mkdirs(bucketSrcPath);
+    OzoneBucket bucket = volume.getBucket(srcBucket);
+    Assert.assertEquals(srcBucket, bucket.getName());
+
+    // dest link destVol/destBucket -> srcVol/srcBucket
+    Path volumeLinkPath = new Path(OZONE_URI_DELIMITER + destVol);
+    fs.mkdirs(volumeLinkPath);
+    volume = objectStore.getVolume(destVol);
+    Assert.assertEquals(destVol, volume.getName());
+    createLinkBucket(destVol, destBucket, srcVol, srcBucket);
+  }
+
+  @Test
+  public void testSymlinkList() throws Exception {
+    OzoneFsShell shell = new OzoneFsShell(conf);
+    // setup symlink, destVol/destBucket -> srcVol/srcBucket
+    String srcVolume = getRandomNonExistVolumeName();
+    final String srcBucket = "bucket";
+    String destVolume = getRandomNonExistVolumeName();
+    createSymlinkSrcDestPaths(srcVolume, srcBucket, destVolume, srcBucket);
+
+    try {
+      // test symlink -ls -R destVol/destBucket -> srcVol/srcBucket
+      // srcBucket no keys
+      // run toolrunner ozone fs shell commands
+      try (GenericTestUtils.SystemOutCapturer capture =
+               new GenericTestUtils.SystemOutCapturer()) {
+        String linkPathStr = rootPath + destVolume;
+        ToolRunner.run(shell, new String[]{"-ls", "-R", linkPathStr});
+        Assert.assertTrue(capture.getOutput().contains("drwxrwxrwx"));
+        Assert.assertTrue(capture.getOutput().contains(linkPathStr +
+            OZONE_URI_DELIMITER + srcBucket));
+      } finally {
+        shell.close();
+      }
+
+      // add key in source bucket
+      final String key = "object-dir/object-name1";
+      try (OzoneOutputStream outputStream = objectStore.getVolume(srcVolume)
+          .getBucket(srcBucket)
+          .createKey(key, 1)) {
+        outputStream.write(RandomUtils.nextBytes(1));
+      }
+      Assert.assertEquals(objectStore.getVolume(srcVolume)
+          .getBucket(srcBucket).getKey(key).getName(), key);
+
+      // test ls -R /destVol/destBucket, srcBucket with key (non-empty)
+      try (GenericTestUtils.SystemOutCapturer capture =
+               new GenericTestUtils.SystemOutCapturer()) {
+        String linkPathStr = rootPath + destVolume;
+        ToolRunner.run(shell, new String[]{"-ls", "-R",
+            linkPathStr + OZONE_URI_DELIMITER + srcBucket});
+        Assert.assertTrue(capture.getOutput().contains("drwxrwxrwx"));
+        Assert.assertTrue(capture.getOutput().contains(linkPathStr +
+            OZONE_URI_DELIMITER + srcBucket));
+        Assert.assertTrue(capture.getOutput().contains("-rw-rw-rw-"));
+        Assert.assertTrue(capture.getOutput().contains(linkPathStr +
+            OZONE_URI_DELIMITER + srcBucket + OZONE_URI_DELIMITER + key));
+      } finally {
+        shell.close();
+      }
+    } finally {
+      // cleanup; note must delete link before link src bucket
+      // due to bug - HDDS-7884
+      fs.delete(new Path(OZONE_URI_DELIMITER + destVolume +
+          OZONE_URI_DELIMITER + srcBucket));
+      fs.delete(new Path(OZONE_URI_DELIMITER + srcVolume), true);
+      fs.delete(new Path(OZONE_URI_DELIMITER + destVolume), true);
+    }
+  }
+
+  @Test
+  public void testSymlinkPosixDelete() throws Exception {
+    OzoneFsShell shell = new OzoneFsShell(conf);
+    // setup symlink, destVol/destBucket -> srcVol/srcBucket
+    String srcVolume = getRandomNonExistVolumeName();
+    final String srcBucket = "bucket";
+    String destVolume = getRandomNonExistVolumeName();
+    createSymlinkSrcDestPaths(srcVolume, srcBucket, destVolume, srcBucket);
+
+    try {
+      // test symlink destVol/destBucket -> srcVol/srcBucket exists
+      Assert.assertTrue(fs.exists(new Path(OZONE_URI_DELIMITER +
+          destVolume + OZONE_URI_DELIMITER + srcBucket)));
+
+      // add key to srcBucket
+      final String key = "object-dir/object-name1";
+      try (OzoneOutputStream outputStream = objectStore.getVolume(srcVolume)
+          .getBucket(srcBucket)
+          .createKey(key, 1)) {
+        outputStream.write(RandomUtils.nextBytes(1));
+      }
+      Assert.assertEquals(objectStore.getVolume(srcVolume)
+          .getBucket(srcBucket).getKey(key).getName(), key);
+
+      // test symlink -rm destVol/destBucket -> srcVol/srcBucket
+      // should delete only link, srcBucket and key unaltered
+      // run toolrunner ozone fs shell commands
+      try {
+        String linkPathStr = rootPath + destVolume + OZONE_URI_DELIMITER +
+            srcBucket;
+        int res = ToolRunner.run(shell, new String[]{"-rm", "-skipTrash",
+            linkPathStr});
+        Assert.assertEquals(0, res);
+
+        try {
+          objectStore.getVolume(destVolume).getBucket(srcBucket);
+          Assert.fail("Bucket should not exist, should throw OMException");
+        } catch (OMException ex) {
+          Assert.assertEquals(BUCKET_NOT_FOUND, ex.getResult());
+        }
+
+        Assert.assertEquals(srcBucket, objectStore.getVolume(srcVolume)
+            .getBucket(srcBucket).getName());
+        Assert.assertEquals(key, objectStore.getVolume(srcVolume)
+            .getBucket(srcBucket).getKey(key).getName());
+
+        // re-create symlink
+        createLinkBucket(destVolume, srcBucket, srcVolume, srcBucket);
+        Assert.assertTrue(fs.exists(new Path(OZONE_URI_DELIMITER +
+            destVolume + OZONE_URI_DELIMITER + srcBucket)));
+
+        // test symlink -rm -R -f destVol/destBucket/ -> srcVol/srcBucket
+        // should delete only link contents (src bucket contents),
+        // link and srcBucket unaltered
+        // run toolrunner ozone fs shell commands
+        linkPathStr = rootPath + destVolume + OZONE_URI_DELIMITER + srcBucket;
+        res = ToolRunner.run(shell, new String[]{"-rm", "-skipTrash",
+            "-f", "-R", linkPathStr + OZONE_URI_DELIMITER});
+        Assert.assertEquals(0, res);
+
+        Assert.assertEquals(srcBucket, objectStore.getVolume(destVolume)
+            .getBucket(srcBucket).getName());
+        Assert.assertEquals(true, objectStore.getVolume(destVolume)
+            .getBucket(srcBucket).isLink());
+        Assert.assertEquals(srcBucket, objectStore.getVolume(srcVolume)
+            .getBucket(srcBucket).getName());
+        try {
+          objectStore.getVolume(srcVolume).getBucket(srcBucket).getKey(key);
+          Assert.fail("Key should be deleted under srcBucket, " +
+              "OMException expected");
+        } catch (OMException ex) {
+          Assert.assertEquals(KEY_NOT_FOUND, ex.getResult());
+        }
+
+        // test symlink -rm -R -f destVol/destBucket -> srcVol/srcBucket
+        // should delete only link
+        // run toolrunner ozone fs shell commands
+        linkPathStr = rootPath + destVolume + OZONE_URI_DELIMITER + srcBucket;
+        res = ToolRunner.run(shell, new String[]{"-rm", "-skipTrash",
+            "-f", "-R", linkPathStr});
+        Assert.assertEquals(0, res);
+
+        Assert.assertEquals(srcBucket, objectStore.getVolume(srcVolume)
+            .getBucket(srcBucket).getName());
+        // test link existence
+        try {
+          objectStore.getVolume(destVolume).getBucket(srcBucket);
+          Assert.fail("link should not exist, " +
+              "OMException expected");
+        } catch (OMException ex) {
+          Assert.assertEquals(BUCKET_NOT_FOUND, ex.getResult());
+        }
+        // test src bucket existence
+        Assert.assertEquals(objectStore.getVolume(srcVolume)
+            .getBucket(srcBucket).getName(), srcBucket);
+      } finally {
+        shell.close();
+      }
+    } finally {
+      // cleanup; note must delete link before link src bucket
+      // due to bug - HDDS-7884
+      fs.delete(new Path(OZONE_URI_DELIMITER + destVolume + OZONE_URI_DELIMITER
+          + srcBucket));
+      fs.delete(new Path(OZONE_URI_DELIMITER + srcVolume), true);
+      fs.delete(new Path(OZONE_URI_DELIMITER + destVolume), true);
+    }
+  }
+
   @Test
   public void testDeleteBucketLink() throws Exception {
     // Create test volume, bucket, directory
diff --git a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
index b11a40a317..cffb36c144 100644
--- a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
+++ b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/BasicRootedOzoneFileSystem.java
@@ -92,7 +92,7 @@ import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.VOLU
  * This is a basic version which doesn't extend
  * KeyProviderTokenIssuer and doesn't include statistics. It can be used
  * from older hadoop version. For newer hadoop version use the full featured
- * BasicRootedOzoneFileSystem.
+ * RootedOzoneFileSystem.
  */
 @InterfaceAudience.Private
 @InterfaceStability.Evolving
@@ -696,8 +696,13 @@ public class BasicRootedOzoneFileSystem extends FileSystem {
       return false;
     }
 
-    // remove link bucket directly
-    if (isLinkBucket(f, ofsPath)) {
+    // handling posix symlink delete behaviours
+    // i.) rm [-r] <symlink path>, delete symlink not target bucket contents
+    // ii.) rm -r <symlink path>/, delete target bucket contents not symlink
+    boolean handleTrailingSlash = f.toString().endsWith(OZONE_URI_DELIMITER);
+    // remove link bucket directly if link and
+    // rm path does not have trailing slash
+    if (isLinkBucket(f, ofsPath) && !handleTrailingSlash) {
       deleteBucketFromVolume(f, ofsPath);
       return true;
     }
@@ -705,8 +710,12 @@ public class BasicRootedOzoneFileSystem extends FileSystem {
     // delete inner content of bucket
     boolean result = innerDelete(f, recursive);
 
-    // Handle delete bucket
-    deleteBucketFromVolume(f, ofsPath);
+    // check if rm path does not have trailing slash
+    // if so, the contents of bucket were deleted and skip delete bucket
+    // otherwise, Handle delete bucket
+    if (!handleTrailingSlash) {
+      deleteBucketFromVolume(f, ofsPath);
+    }
     return result;
   }
 
@@ -1473,6 +1482,26 @@ public class BasicRootedOzoneFileSystem extends FileSystem {
         spaceConsumed(summary[1]).build();
   }
 
+  @Override
+  public boolean supportsSymlinks() {
+    return true;
+  }
+
+  @Override
+  public Path getLinkTarget(Path f) throws IOException {
+    OFSPath ofsPath = new OFSPath(f,
+        OzoneConfiguration.of(getConfSource()));
+    if (ofsPath.isBucket()) {  // only support bucket links
+      OzoneBucket bucket = adapterImpl.getBucket(ofsPath, false);
+      if (bucket.isLink()) {
+        return new Path(OZONE_URI_DELIMITER +
+            bucket.getSourceVolume() + OZONE_URI_DELIMITER +
+            bucket.getSourceBucket());
+      }
+    }
+    return f;
+  }
+  
   public SnapshotDiffReport getSnapshotDiffReport(final Path snapshotDir,
       final String fromSnapshot, final String toSnapshot)
       throws IOException, InterruptedException {
diff --git a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsDelete.java b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsDelete.java
new file mode 100644
index 0000000000..672d822030
--- /dev/null
+++ b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsDelete.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.fs.ozone;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.classification.InterfaceStability;
+import org.apache.hadoop.fs.ContentSummary;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.PathIOException;
+import org.apache.hadoop.fs.PathIsDirectoryException;
+import org.apache.hadoop.fs.PathNotFoundException;
+import org.apache.hadoop.fs.Trash;
+import org.apache.hadoop.fs.shell.CommandFactory;
+import org.apache.hadoop.fs.shell.CommandFormat;
+import org.apache.hadoop.fs.shell.FsCommand;
+import org.apache.hadoop.fs.shell.PathData;
+import org.apache.hadoop.util.ToolRunner;
+import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SHELL_SAFELY_DELETE_LIMIT_NUM_FILES;
+import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SHELL_SAFELY_DELETE_LIMIT_NUM_FILES_DEFAULT;
+import static org.apache.hadoop.ozone.OzoneConsts.OZONE_URI_DELIMITER;
+
+/**
+ * Classes that delete paths.
+ */
+@InterfaceAudience.Private
+@InterfaceStability.Evolving
+
+public final class OzoneFsDelete {
+
+  private OzoneFsDelete() {
+  }
+
+  public static void registerCommands(CommandFactory factory) {
+    factory.addClass(OzoneFsDelete.Rm.class, "-rm");
+    factory.addClass(OzoneFsDelete.Rmr.class, "-rmr");
+  }
+
+  /** remove non-directory paths. */
+  public static class Rm extends FsCommand {
+    public static final String NAME = "rm";
+    public static final String USAGE = "[-f] [-r|-R] [-skipTrash] " +
+        "[-safely] <src> ...";
+    public static final String DESCRIPTION =
+        "Delete all files that match the specified file pattern. " +
+            "Equivalent to the Unix command \"rm <src>\"\n" +
+            "-f: If the file does not exist, do not display a diagnostic " +
+            "message or modify the exit status to reflect an error.\n" +
+            "-[rR]:  Recursively deletes directories.\n" +
+            "-skipTrash: option bypasses trash, if enabled, and immediately " +
+            "deletes <src>.\n" +
+            "-safely: option requires safety confirmation, if enabled, " +
+            "requires confirmation before deleting large directory with more " +
+            "than <hadoop.shell.delete.limit.num.files> files. Delay is " +
+            "expected when walking over large directory recursively to count " +
+            "the number of files to be deleted before the confirmation.\n";
+
+    private boolean skipTrash = false;
+    private boolean deleteDirs = false;
+    private boolean ignoreFNF = false;
+    private boolean safeDelete = false;
+    private boolean trailing = false;
+
+    @Override
+    protected void processOptions(LinkedList<String> args) throws IOException {
+      CommandFormat cf = new CommandFormat(
+          1, Integer.MAX_VALUE, "f", "r", "R", "skipTrash", "safely");
+      cf.parse(args);
+      ignoreFNF = cf.getOpt("f");
+      deleteDirs = cf.getOpt("r") || cf.getOpt("R");
+      skipTrash = cf.getOpt("skipTrash");
+      safeDelete = cf.getOpt("safely");
+    }
+
+    @Override
+    protected List<PathData> expandArgument(String arg) throws IOException {
+      try {
+        // handle trailing slash for symlinks
+        if (arg.endsWith(OZONE_URI_DELIMITER)) {
+          trailing = true;
+        }
+        return super.expandArgument(arg);
+      } catch (PathNotFoundException e) {
+        if (!ignoreFNF) {
+          throw e;
+        }
+        // prevent -f on a non-existent glob from failing
+        return new LinkedList<PathData>();
+      }
+    }
+
+    @Override
+    protected void processNonexistentPath(PathData item) throws IOException {
+      if (!ignoreFNF) {
+        super.processNonexistentPath(item);
+      }
+    }
+
+    @Override
+    protected void processPath(PathData item) throws IOException {
+      boolean isSymlink = false;
+      if (item.fs.supportsSymlinks()) {
+        isSymlink = item.fs.getLinkTarget(item.path) != item.path;
+      }
+      // support posix rm of symlink in addition to rm -r for directories
+      if (item.stat.isDirectory() && !deleteDirs && !isSymlink) {
+        throw new PathIsDirectoryException(item.toString());
+      }
+      // support posix symlink delete with trailing slash 'rm <path symlink>/'
+      // deletes contents of symlink bucket and retains symlink
+      Path path = item.path;
+      if (isSymlink && trailing) {
+        path = new Path(URI.create(path.toString() + OZONE_URI_DELIMITER));
+      }
+      // Any problem (ie. creating the trash dir,
+      // moving the item to be deleted, etc), has the trash
+      // service throw exceptions.  User can retry correcting
+      // the problem.
+      if (moveToTrash(item) || !canBeSafelyDeleted(item)) {
+        return;
+      }
+      if (!item.fs.delete(path, deleteDirs)) {
+        throw new PathIOException(item.toString());
+      }
+      out.println("Deleted " + item + (trailing ? OZONE_URI_DELIMITER : ""));
+    }
+
+    private boolean canBeSafelyDeleted(PathData item)
+        throws IOException {
+      boolean shouldDelete = true;
+      if (safeDelete) {
+        final long deleteLimit = getConf().getLong(
+            HADOOP_SHELL_SAFELY_DELETE_LIMIT_NUM_FILES,
+            HADOOP_SHELL_SAFELY_DELETE_LIMIT_NUM_FILES_DEFAULT);
+        if (deleteLimit > 0) {
+          ContentSummary cs = item.fs.getContentSummary(item.path);
+          final long numFiles = cs.getFileCount();
+          if (numFiles > deleteLimit) {
+            if (!ToolRunner.confirmPrompt("Proceed deleting " + numFiles +
+                " files?")) {
+              System.err.println("Delete aborted at user request.\n");
+              shouldDelete = false;
+            }
+          }
+        }
+      }
+      return shouldDelete;
+    }
+
+    private boolean moveToTrash(PathData item) throws IOException {
+      boolean success = false;
+      if (!skipTrash) {
+        try {
+          success = Trash.moveToAppropriateTrash(item.fs, item.path, getConf());
+        } catch (FileNotFoundException fnfe) {
+          throw fnfe;
+        } catch (IOException ioe) {
+          String msg = ioe.getMessage();
+          if (ioe.getCause() != null) {
+            msg += ": " + ioe.getCause().getMessage();
+          }
+          throw new IOException(msg + ". Consider using -skipTrash option",
+              ioe);
+        }
+      }
+      return success;
+    }
+  }
+
+  /** remove any path. */
+  static class Rmr extends OzoneFsDelete.Rm {
+    public static final String NAME = "rmr";
+
+    @Override
+    protected void processOptions(LinkedList<String> args) throws IOException {
+      args.addFirst("-r");
+      super.processOptions(args);
+    }
+
+    @Override
+    public String getReplacementCommand() {
+      return "-rm -r";
+    }
+  }
+
+}
diff --git a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsShell.java b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsShell.java
index 4ff4916b61..3e494c1c37 100644
--- a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsShell.java
+++ b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFsShell.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.fs.ozone;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FsShell;
 import org.apache.hadoop.fs.shell.CommandFactory;
@@ -60,6 +61,8 @@ public class OzoneFsShell extends FsShell {
     // commands, and then this method can be abstract
     if (this.getClass().equals(OzoneFsShell.class)) {
       factory.registerCommands(FsCommand.class);
+      // ozone delete rm command registration supersedes fs delete
+      factory.registerCommands(OzoneFsDelete.class);
     }
   }
 
@@ -97,4 +100,11 @@ public class OzoneFsShell extends FsShell {
   protected static OzoneFsShell newShellInstance() {
     return new OzoneFsShell();
   }
+
+  // for testing purposes, ensure that ozone specific
+  // added fs commands are visible
+  @VisibleForTesting
+  public CommandFactory getCommandFactory() {
+    return commandFactory;
+  }
 }
diff --git a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicOzoneFileSystems.java b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicOzoneFileSystems.java
index 1db1ee5b4e..0414fe52cb 100644
--- a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicOzoneFileSystems.java
+++ b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestBasicOzoneFileSystems.java
@@ -22,6 +22,7 @@ import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.conf.StorageSize;
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -72,6 +73,16 @@ public class TestBasicOzoneFileSystems {
     assertDefaultBlockSize(toBytes(customValue));
   }
 
+  // test for filesystem pseduo-posix symlink support
+  @Test
+  public void testFileSystemPosixSymlinkSupport() {
+    if (subject.getClass() == BasicRootedOzoneFileSystem.class) {
+      Assert.assertTrue(subject.supportsSymlinks());
+    } else {
+      Assert.assertFalse(subject.supportsSymlinks());
+    }
+  }
+
   private void assertDefaultBlockSize(long expected) {
     assertEquals(expected, subject.getDefaultBlockSize());
 
@@ -86,4 +97,5 @@ public class TestBasicOzoneFileSystems {
     StorageSize blockSize = StorageSize.parse(value);
     return (long) blockSize.getUnit().toBytes(blockSize.getValue());
   }
+
 }
diff --git a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFsShell.java b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFsShell.java
new file mode 100644
index 0000000000..6974369bba
--- /dev/null
+++ b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFsShell.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.fs.ozone;
+
+import org.apache.hadoop.fs.shell.Command;
+import org.apache.hadoop.fs.shell.CommandFactory;
+import org.apache.hadoop.util.ToolRunner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+
+/**
+ * Tests the behavior of OzoneFsShell.
+ */
+public class TestOzoneFsShell {
+
+  // tests command handler for FsShell bound to OzoneDelete class
+  @Test
+  public void testOzoneFsShellRegisterDeleteCmd() throws IOException {
+    final String rmCmdName = "rm";
+    final String rmCmd = "-" + rmCmdName;
+    final String arg = "arg1";
+    OzoneFsShell shell = new OzoneFsShell();
+    String[] argv = {arg, arg};
+    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+    PrintStream bytesPrintStream = new PrintStream(bytes, false, UTF_8.name());
+    PrintStream oldErr = System.err;
+    System.setErr(bytesPrintStream);
+    try {
+      ToolRunner.run(shell, argv);
+    } catch (Exception e) {
+    } finally {
+      // test command bindings for "rm" command handled by OzoneDelete class
+      CommandFactory factory = shell.getCommandFactory();
+      Assert.assertEquals(1, Arrays.stream(factory.getNames())
+          .filter(c -> c.equals(rmCmd)).count());
+      Command instance = factory.getInstance(rmCmd);
+      Assert.assertNotNull(instance);
+      Assert.assertEquals(OzoneFsDelete.Rm.class, instance.getClass());
+      Assert.assertEquals(rmCmdName, instance.getCommandName());
+      shell.close();
+      System.setErr(oldErr);
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ozone.apache.org
For additional commands, e-mail: commits-help@ozone.apache.org