diff --git a/.gitignore b/.gitignore index d5550364b8f..ac245c7575c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ make-build-debug # Filesystem contract test options and credentials auth-keys.xml azure-auth-keys.xml +azure-bfs-auth-keys.xml # External tool builders */.externalToolBuilders diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java index b101b3b3096..b92d3253aa4 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/CommonConfigurationKeysPublic.java @@ -886,7 +886,9 @@ public class CommonConfigurationKeysPublic { "fs.s3a.*.server-side-encryption.key", "fs.azure\\.account.key.*", "credential$", - "oauth.*token$", + "oauth.*secret", + "oauth.*password", + "oauth.*token", HADOOP_SECURITY_SENSITIVE_CONFIG_KEYS); /** diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index 81502dc24c6..f8eba04bbc0 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -603,7 +603,9 @@ fs.s3a.*.server-side-encryption.key fs.azure.account.key.* credential$ - oauth.*token$ + oauth.*secret + oauth.*password + oauth.*token hadoop.security.sensitive-config-keys A comma-separated or multi-line list of regular expressions to @@ -1618,6 +1620,18 @@ + + fs.AbstractFileSystem.wasb.impl + org.apache.hadoop.fs.azure.Wasb + AbstractFileSystem implementation class of wasb:// + + + + fs.AbstractFileSystem.wasbs.impl + org.apache.hadoop.fs.azure.Wasbs + AbstractFileSystem implementation class of wasbs:// + + fs.wasb.impl org.apache.hadoop.fs.azure.NativeAzureFileSystem @@ -1639,6 +1653,31 @@ SAS keys to communicate with Azure storage. + + + fs.abfs.impl + org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem + The implementation class of the Azure Blob Filesystem + + + + fs.abfss.impl + org.apache.hadoop.fs.azurebfs.SecureAzureBlobFileSystem + The implementation class of the Secure Azure Blob Filesystem + + + + fs.AbstractFileSystem.abfs.impl + org.apache.hadoop.fs.azurebfs.Abfs + AbstractFileSystem implementation class of abfs:// + + + + fs.AbstractFileSystem.abfss.impl + org.apache.hadoop.fs.azurebfs.Abfss + AbstractFileSystem implementation class of abfss:// + + fs.azure.local.sas.key.mode false diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/filesystem.md b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/filesystem.md index 2637f5442d3..28c6fbe240e 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/filesystem.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/filesystem.md @@ -544,15 +544,6 @@ atomic. The combined operation, including `mkdirs(parent(F))` MAY be atomic. The return value is always true—even if a new directory is not created (this is defined in HDFS). -#### Implementation Notes: Local FileSystem - -The local FileSystem does not raise an exception if `mkdirs(p)` is invoked -on a path that exists and is a file. Instead the operation returns false. - - if isFile(FS, p): - FS' = FS - result = False - ### `FSDataOutputStream create(Path, ...)` @@ -641,7 +632,7 @@ Implementations without a compliant call SHOULD throw `UnsupportedOperationExcep if not exists(FS, p) : raise FileNotFoundException - if not isFile(FS, p) : raise [FileNotFoundException, IOException] + if not isFile(FS, p) : raise [FileAlreadyExistsException, FileNotFoundException, IOException] #### Postconditions diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java index 023c83109e5..e10617daaba 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestCommonConfigurationFields.java @@ -113,6 +113,9 @@ public void initializeMemberVariables() { xmlPrefixToSkipCompare.add("fs.wasb.impl"); xmlPrefixToSkipCompare.add("fs.wasbs.impl"); xmlPrefixToSkipCompare.add("fs.azure."); + xmlPrefixToSkipCompare.add("fs.abfs.impl"); + xmlPrefixToSkipCompare.add("fs.abfss.impl"); + // ADL properties are in a different subtree // - org.apache.hadoop.hdfs.web.ADLConfKeys diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigRedactor.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigRedactor.java index 313394293c0..ca53fa7f2bf 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigRedactor.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/conf/TestConfigRedactor.java @@ -55,6 +55,13 @@ private void testRedact(Configuration conf) throws Exception { "fs.s3a.server-side-encryption.key", "fs.s3a.bucket.engineering.server-side-encryption.key", "fs.azure.account.key.abcdefg.blob.core.windows.net", + "fs.azure.account.key.abcdefg.dfs.core.windows.net", + "fs.azure.account.oauth2.client.secret", + "fs.azure.account.oauth2.client.secret.account.dfs.core.windows.net", + "fs.azure.account.oauth2.user.password", + "fs.azure.account.oauth2.user.password.account.dfs.core.windows.net", + "fs.azure.account.oauth2.refresh.token", + "fs.azure.account.oauth2.refresh.token.account.dfs.core.windows.net", "fs.adl.oauth2.refresh.token", "fs.adl.oauth2.credential", "dfs.adls.oauth2.refresh.token", diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractConcatTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractConcatTest.java index 7b120861edc..d30e0d66eff 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractConcatTest.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractConcatTest.java @@ -19,15 +19,16 @@ package org.apache.hadoop.fs.contract; import org.apache.hadoop.fs.Path; + import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.hadoop.fs.contract.ContractTestUtils.assertFileHasLength; -import static org.apache.hadoop.fs.contract.ContractTestUtils.cleanup; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Test concat -if supported @@ -60,25 +61,15 @@ public void setup() throws Exception { @Test public void testConcatEmptyFiles() throws Throwable { touch(getFileSystem(), target); - try { - getFileSystem().concat(target, new Path[0]); - fail("expected a failure"); - } catch (Exception e) { - //expected - handleExpectedException(e); - } + handleExpectedException(intercept(Exception.class, + () -> getFileSystem().concat(target, new Path[0]))); } @Test public void testConcatMissingTarget() throws Throwable { - try { - getFileSystem().concat(target, - new Path[] { zeroByteFile}); - fail("expected a failure"); - } catch (Exception e) { - //expected - handleExpectedException(e); - } + handleExpectedException( + intercept(Exception.class, + () -> getFileSystem().concat(target, new Path[]{zeroByteFile}))); } @Test @@ -98,15 +89,8 @@ public void testConcatFileOnFile() throws Throwable { public void testConcatOnSelf() throws Throwable { byte[] block = dataset(TEST_FILE_LEN, 0, 255); createFile(getFileSystem(), target, false, block); - try { - getFileSystem().concat(target, - new Path[]{target}); - } catch (Exception e) { - //expected - handleExpectedException(e); - } + handleExpectedException(intercept(Exception.class, + () -> getFileSystem().concat(target, new Path[]{target}))); } - - } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractGetFileStatusTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractGetFileStatusTest.java index 269e35ea669..cb706ede917 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractGetFileStatusTest.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractGetFileStatusTest.java @@ -32,6 +32,7 @@ import org.junit.Test; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Test getFileStatus and related listing operations. @@ -275,35 +276,22 @@ public void testListFilesNoDir() throws Throwable { @Test public void testLocatedStatusNoDir() throws Throwable { describe("test the LocatedStatus call on a path which is not present"); - try { - RemoteIterator iterator - = getFileSystem().listLocatedStatus(path("missing")); - fail("Expected an exception, got an iterator: " + iterator); - } catch (FileNotFoundException expected) { - // expected - } + intercept(FileNotFoundException.class, + () -> getFileSystem().listLocatedStatus(path("missing"))); } @Test public void testListStatusNoDir() throws Throwable { describe("test the listStatus(path) call on a path which is not present"); - try { - getFileSystem().listStatus(path("missing")); - fail("Expected an exception"); - } catch (FileNotFoundException expected) { - // expected - } + intercept(FileNotFoundException.class, + () -> getFileSystem().listStatus(path("missing"))); } @Test public void testListStatusFilteredNoDir() throws Throwable { describe("test the listStatus(path, filter) call on a missing path"); - try { - getFileSystem().listStatus(path("missing"), ALL_PATHS); - fail("Expected an exception"); - } catch (FileNotFoundException expected) { - // expected - } + intercept(FileNotFoundException.class, + () -> getFileSystem().listStatus(path("missing"), ALL_PATHS)); } @Test diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractMkdirTest.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractMkdirTest.java index c5a546dccdd..de44bc232e7 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractMkdirTest.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractMkdirTest.java @@ -26,6 +26,7 @@ import java.io.IOException; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertMkdirs; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; @@ -175,4 +176,11 @@ public void testMkdirsDoesNotRemoveParentDirectories() throws IOException { } } + @Test + public void testCreateDirWithExistingDir() throws Exception { + Path path = path("testCreateDirWithExistingDir"); + final FileSystem fs = getFileSystem(); + assertMkdirs(fs, path); + assertMkdirs(fs, path); + } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractFSContract.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractFSContract.java index d3dafe974a5..f09496a6082 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractFSContract.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractFSContract.java @@ -148,7 +148,6 @@ public void setEnabled(boolean enabled) { * @param feature feature to query * @param defval default value * @return true if the feature is supported - * @throws IOException IO problems */ public boolean isSupported(String feature, boolean defval) { return getConf().getBoolean(getConfKey(feature), defval); @@ -160,7 +159,6 @@ public boolean isSupported(String feature, boolean defval) { * @param feature feature to query * @param defval default value * @return true if the feature is supported - * @throws IOException IO problems */ public int getLimit(String feature, int defval) { return getConf().getInt(getConfKey(feature), defval); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java index 38a6fb10138..ba1204848ad 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/ContractTestUtils.java @@ -187,8 +187,11 @@ public static void writeDataset(FileSystem fs, Path path, byte[] src, (short) 1, buffersize); } - out.write(src, 0, len); - out.close(); + try { + out.write(src, 0, len); + } finally { + out.close(); + } assertFileHasLength(fs, path, len); } @@ -1021,6 +1024,18 @@ public static void assertListStatusFinds(FileSystem fs, found); } + /** + * Execute {@link FileSystem#mkdirs(Path)}; expect {@code true} back. + * (Note: does not work for localFS if the directory already exists) + * Does not perform any validation of the created directory. + * @param fs filesystem + * @param dir directory to create + * @throws IOException IO Problem + */ + public static void assertMkdirs(FileSystem fs, Path dir) throws IOException { + assertTrue("mkdirs(" + dir + ") returned false", fs.mkdirs(dir)); + } + /** * Test for the host being an OSX machine. * @return true if the JVM thinks that is running on OSX diff --git a/hadoop-project/pom.xml b/hadoop-project/pom.xml index caf6d4f9d3d..794fdcc5031 100644 --- a/hadoop-project/pom.xml +++ b/hadoop-project/pom.xml @@ -1209,6 +1209,11 @@ jsch 0.1.54 + + org.apache.htrace + htrace-core + 3.1.0-incubating + org.apache.htrace htrace-core4 @@ -1344,6 +1349,19 @@ 7.0.0 + + + org.wildfly.openssl + wildfly-openssl + 1.0.4.Final + + + + org.threadly + threadly + 4.9.0 + + com.aliyun.oss aliyun-sdk-oss diff --git a/hadoop-tools/hadoop-azure/pom.xml b/hadoop-tools/hadoop-azure/pom.xml index 52b5b726a13..52f0bae1008 100644 --- a/hadoop-tools/hadoop-azure/pom.xml +++ b/hadoop-tools/hadoop-azure/pom.xml @@ -67,6 +67,7 @@ src/config/checkstyle.xml + src/config/checkstyle-suppressions.xml @@ -148,11 +149,6 @@ provided - - com.fasterxml.jackson.core - jackson-core - compile - org.apache.httpcomponents @@ -172,10 +168,22 @@ + + com.google.inject + guice + compile + + + + com.google.guava + guava + + + + com.google.guava guava - compile @@ -183,15 +191,26 @@ jetty-util-ajax compile - - - + - commons-io - commons-io - test + org.codehaus.jackson + jackson-mapper-asl + compile + + org.codehaus.jackson + jackson-core-asl + compile + + + + org.wildfly.openssl + wildfly-openssl + compile + + + junit junit @@ -229,19 +248,363 @@ mockito-all test - - com.fasterxml.jackson.core - jackson-databind - + + parallel-tests-wasb + + + parallel-tests + wasb + + + + + + maven-antrun-plugin + + + create-parallel-tests-dirs + test-compile + + + + + + + run + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + test + + + 1 + ${testsThreadCount} + false + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + fork-${surefire.forkNumber} + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.huge.filesize} + ${fs.azure.scale.test.huge.partitionsize} + ${fs.azure.scale.test.timeout} + ${fs.azure.scale.test.list.performance.threads} + ${fs.azure.scale.test.list.performance.files} + + + **/azure/Test*.java + **/azure/**/Test*.java + + + **/azure/**/TestRollingWindowAverage*.java + + + + + serialized-test-wasb + + test + + + 1 + false + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + fork-${surefire.forkNumber} + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.huge.filesize} + ${fs.azure.scale.test.huge.partitionsize} + ${fs.azure.scale.test.timeout} + ${fs.azure.scale.test.list.performance.threads} + ${fs.azure.scale.test.list.performance.files} + + + **/azure/**/TestRollingWindowAverage*.java + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + default-integration-test-wasb + + integration-test + verify + + + 1 + ${testsThreadCount} + false + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + false + + + true + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + + + + + + fork-${surefire.forkNumber} + + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.huge.filesize} + ${fs.azure.scale.test.huge.partitionsize} + ${fs.azure.scale.test.timeout} + ${fs.azure.scale.test.list.performance.threads} + ${fs.azure.scale.test.list.performance.files} + + + + **/azure/ITest*.java + **/azure/**/ITest*.java + + + **/azure/ITestNativeFileSystemStatistics.java + + + + + + + sequential-integration-tests-wasb + + integration-test + verify + + + ${fs.azure.scale.test.timeout} + false + + false + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.huge.filesize} + ${fs.azure.scale.test.huge.partitionsize} + ${fs.azure.scale.test.timeout} + ${fs.azure.scale.test.list.performance.threads} + ${fs.azure.scale.test.list.performance.files} + + + **/azure/ITestNativeFileSystemStatistics.java + + + + + + + + + + + parallel-tests-abfs + + + parallel-tests + abfs + + + + + + maven-antrun-plugin + + + create-parallel-tests-dirs + test-compile + + + + + + + run + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + test + + + ${testsThreadCount} + false + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + fork-${surefire.forkNumber} + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.huge.filesize} + ${fs.azure.scale.test.huge.partitionsize} + ${fs.azure.scale.test.timeout} + ${fs.azure.scale.test.list.performance.threads} + ${fs.azure.scale.test.list.performance.files} + + + **/azurebfs/Test*.java + **/azurebfs/**/Test*.java + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-test-abfs-parallel-classesAndMethods + + integration-test + verify + + + ${testsThreadCount} + true + both + ${testsThreadCount} + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + false + + + true + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + + + + + fork-${surefire.forkNumber} + + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.timeout} + + + + **/azurebfs/ITest*.java + **/azurebfs/**/ITest*.java + + + **/azurebfs/contract/ITest*.java + **/azurebfs/ITestAzureBlobFileSystemE2EScale.java + **/azurebfs/ITestAbfsReadWriteAndSeek.java + **/azurebfs/ITestAzureBlobFileSystemListStatus.java + + + + + + integration-test-abfs-parallel-classes + + integration-test + verify + + + ${testsThreadCount} + false + + ${maven-surefire-plugin.argLine} -DminiClusterDedicatedDirs=true + ${fs.azure.scale.test.timeout} + false + + + true + ${test.build.data}/${surefire.forkNumber} + ${test.build.dir}/${surefire.forkNumber} + ${hadoop.tmp.dir}/${surefire.forkNumber} + + + + + + fork-${surefire.forkNumber} + + ${fs.azure.scale.test.enabled} + ${fs.azure.scale.test.timeout} + + + **/azurebfs/contract/ITest*.java + **/azurebfs/ITestAzureBlobFileSystemE2EScale.java + **/azurebfs/ITestAbfsReadWriteAndSeek.java + **/azurebfs/ITestAzureBlobFileSystemListStatus.java + + + + + + + + + parallel-tests parallel-tests + both @@ -398,8 +761,11 @@ **/ITestNativeAzureFileSystemConcurrencyLive.java **/ITestNativeAzureFileSystemLive.java **/ITestNativeAzureFSPageBlobLive.java + **/ITestAzureBlobFileSystemRandomRead.java **/ITestWasbRemoteCallHelper.java **/ITestBlockBlobInputStream.java + **/ITestWasbAbfsCompatibility.java + **/ITestNativeFileSystemStatistics.java @@ -424,14 +790,18 @@ ${fs.azure.scale.test.list.performance.files} + **/ITestWasbAbfsCompatibility.java **/ITestFileSystemOperationsExceptionHandlingMultiThreaded.java **/ITestFileSystemOperationsWithThreads.java **/ITestOutOfBandAzureBlobOperationsLive.java **/ITestNativeAzureFileSystemAuthorizationWithOwner.java **/ITestNativeAzureFileSystemConcurrencyLive.java **/ITestNativeAzureFileSystemLive.java + **/ITestNativeAzureFSPageBlobLive.java + **/ITestAzureBlobFileSystemRandomRead.java **/ITestWasbRemoteCallHelper.java **/ITestBlockBlobInputStream.java + **/ITestNativeFileSystemStatistics.java @@ -440,6 +810,7 @@ + sequential-tests diff --git a/hadoop-tools/hadoop-azure/src/config/checkstyle-suppressions.xml b/hadoop-tools/hadoop-azure/src/config/checkstyle-suppressions.xml new file mode 100644 index 00000000000..10cf77e0c2b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/config/checkstyle-suppressions.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/ClientThrottlingAnalyzer.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/ClientThrottlingAnalyzer.java index 850e552758d..859a608a1e1 100644 --- a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/ClientThrottlingAnalyzer.java +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azure/ClientThrottlingAnalyzer.java @@ -99,7 +99,7 @@ private ClientThrottlingAnalyzer() { this.blobMetrics = new AtomicReference( new BlobOperationMetrics(System.currentTimeMillis())); this.timer = new Timer( - String.format("wasb-timer-client-throttling-analyzer-%s", name)); + String.format("wasb-timer-client-throttling-analyzer-%s", name), true); this.timer.schedule(new TimerTaskImpl(), analysisPeriodMs, analysisPeriodMs); diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfs.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfs.java new file mode 100644 index 00000000000..32df9422386 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfs.java @@ -0,0 +1,46 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.DelegateToFileSystem; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; + +/** + * Azure Blob File System implementation of AbstractFileSystem. + * This impl delegates to the old FileSystem + */ +@InterfaceStability.Evolving +public class Abfs extends DelegateToFileSystem { + + Abfs(final URI theUri, final Configuration conf) throws IOException, + URISyntaxException { + super(theUri, new AzureBlobFileSystem(), conf, FileSystemUriSchemes.ABFS_SCHEME, false); + } + + @Override + public int getUriDefaultPort() { + return -1; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsConfiguration.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsConfiguration.java new file mode 100644 index 00000000000..f0088ff1279 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AbfsConfiguration.java @@ -0,0 +1,576 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Map; + +import com.google.common.annotations.VisibleForTesting; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.IntegerConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.LongConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.StringConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.Base64StringConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.BooleanConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.ConfigurationPropertyNotFoundException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.KeyProviderException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.TokenAccessProviderException; +import org.apache.hadoop.fs.azurebfs.diagnostics.Base64StringConfigurationBasicValidator; +import org.apache.hadoop.fs.azurebfs.diagnostics.BooleanConfigurationBasicValidator; +import org.apache.hadoop.fs.azurebfs.diagnostics.IntegerConfigurationBasicValidator; +import org.apache.hadoop.fs.azurebfs.diagnostics.LongConfigurationBasicValidator; +import org.apache.hadoop.fs.azurebfs.diagnostics.StringConfigurationBasicValidator; +import org.apache.hadoop.fs.azurebfs.extensions.CustomTokenProviderAdaptee; +import org.apache.hadoop.fs.azurebfs.oauth2.AccessTokenProvider; +import org.apache.hadoop.fs.azurebfs.oauth2.ClientCredsTokenProvider; +import org.apache.hadoop.fs.azurebfs.oauth2.CustomTokenProviderAdapter; +import org.apache.hadoop.fs.azurebfs.oauth2.MsiTokenProvider; +import org.apache.hadoop.fs.azurebfs.oauth2.RefreshTokenBasedTokenProvider; +import org.apache.hadoop.fs.azurebfs.oauth2.UserPasswordTokenProvider; +import org.apache.hadoop.fs.azurebfs.security.AbfsDelegationTokenManager; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.apache.hadoop.fs.azurebfs.services.KeyProvider; +import org.apache.hadoop.fs.azurebfs.services.SimpleKeyProvider; +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; +import org.apache.hadoop.security.ProviderUtils; +import org.apache.hadoop.util.ReflectionUtils; + +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.*; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.*; + +/** + * Configuration for Azure Blob FileSystem. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class AbfsConfiguration{ + private final Configuration rawConfig; + private final String accountName; + private final boolean isSecure; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_WRITE_BUFFER_SIZE, + MinValue = MIN_BUFFER_SIZE, + MaxValue = MAX_BUFFER_SIZE, + DefaultValue = DEFAULT_WRITE_BUFFER_SIZE) + private int writeBufferSize; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_READ_BUFFER_SIZE, + MinValue = MIN_BUFFER_SIZE, + MaxValue = MAX_BUFFER_SIZE, + DefaultValue = DEFAULT_READ_BUFFER_SIZE) + private int readBufferSize; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_MIN_BACKOFF_INTERVAL, + DefaultValue = DEFAULT_MIN_BACKOFF_INTERVAL) + private int minBackoffInterval; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_MAX_BACKOFF_INTERVAL, + DefaultValue = DEFAULT_MAX_BACKOFF_INTERVAL) + private int maxBackoffInterval; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_BACKOFF_INTERVAL, + DefaultValue = DEFAULT_BACKOFF_INTERVAL) + private int backoffInterval; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_MAX_IO_RETRIES, + MinValue = 0, + DefaultValue = DEFAULT_MAX_RETRY_ATTEMPTS) + private int maxIoRetries; + + @LongConfigurationValidatorAnnotation(ConfigurationKey = AZURE_BLOCK_SIZE_PROPERTY_NAME, + MinValue = 0, + MaxValue = MAX_AZURE_BLOCK_SIZE, + DefaultValue = MAX_AZURE_BLOCK_SIZE) + private long azureBlockSize; + + @StringConfigurationValidatorAnnotation(ConfigurationKey = AZURE_BLOCK_LOCATION_HOST_PROPERTY_NAME, + DefaultValue = AZURE_BLOCK_LOCATION_HOST_DEFAULT) + private String azureBlockLocationHost; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_CONCURRENT_CONNECTION_VALUE_OUT, + MinValue = 1, + DefaultValue = MAX_CONCURRENT_WRITE_THREADS) + private int maxConcurrentWriteThreads; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = AZURE_CONCURRENT_CONNECTION_VALUE_IN, + MinValue = 1, + DefaultValue = MAX_CONCURRENT_READ_THREADS) + private int maxConcurrentReadThreads; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = AZURE_TOLERATE_CONCURRENT_APPEND, + DefaultValue = DEFAULT_READ_TOLERATE_CONCURRENT_APPEND) + private boolean tolerateOobAppends; + + @StringConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_ATOMIC_RENAME_KEY, + DefaultValue = DEFAULT_FS_AZURE_ATOMIC_RENAME_DIRECTORIES) + private String azureAtomicDirs; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION, + DefaultValue = DEFAULT_AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION) + private boolean createRemoteFileSystemDuringInitialization; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = AZURE_SKIP_USER_GROUP_METADATA_DURING_INITIALIZATION, + DefaultValue = DEFAULT_AZURE_SKIP_USER_GROUP_METADATA_DURING_INITIALIZATION) + private boolean skipUserGroupMetadataDuringInitialization; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_READ_AHEAD_QUEUE_DEPTH, + DefaultValue = DEFAULT_READ_AHEAD_QUEUE_DEPTH) + private int readAheadQueueDepth; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_ENABLE_FLUSH, + DefaultValue = DEFAULT_ENABLE_FLUSH) + private boolean enableFlush; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_ENABLE_AUTOTHROTTLING, + DefaultValue = DEFAULT_ENABLE_AUTOTHROTTLING) + private boolean enableAutoThrottling; + + @StringConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_USER_AGENT_PREFIX_KEY, + DefaultValue = "") + private String userAgentId; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = FS_AZURE_ENABLE_DELEGATION_TOKEN, + DefaultValue = DEFAULT_ENABLE_DELEGATION_TOKEN) + private boolean enableDelegationToken; + + private Map storageAccountKeys; + + public AbfsConfiguration(final Configuration rawConfig, String accountName) + throws IllegalAccessException, InvalidConfigurationValueException, IOException { + this.rawConfig = ProviderUtils.excludeIncompatibleCredentialProviders( + rawConfig, AzureBlobFileSystem.class); + this.accountName = accountName; + this.isSecure = getBoolean(FS_AZURE_SECURE_MODE, false); + + validateStorageAccountKeys(); + Field[] fields = this.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + if (field.isAnnotationPresent(IntegerConfigurationValidatorAnnotation.class)) { + field.set(this, validateInt(field)); + } else if (field.isAnnotationPresent(LongConfigurationValidatorAnnotation.class)) { + field.set(this, validateLong(field)); + } else if (field.isAnnotationPresent(StringConfigurationValidatorAnnotation.class)) { + field.set(this, validateString(field)); + } else if (field.isAnnotationPresent(Base64StringConfigurationValidatorAnnotation.class)) { + field.set(this, validateBase64String(field)); + } else if (field.isAnnotationPresent(BooleanConfigurationValidatorAnnotation.class)) { + field.set(this, validateBoolean(field)); + } + } + } + + /** + * Appends an account name to a configuration key yielding the + * account-specific form. + * @param key Account-agnostic configuration key + * @return Account-specific configuration key + */ + public String accountConf(String key) { + return key + "." + accountName; + } + + /** + * Returns the account-specific value if it exists, then looks for an + * account-agnostic value. + * @param key Account-agnostic configuration key + * @return value if one exists, else null + */ + public String get(String key) { + return rawConfig.get(accountConf(key), rawConfig.get(key)); + } + + /** + * Returns the account-specific value if it exists, then looks for an + * account-agnostic value, and finally tries the default value. + * @param key Account-agnostic configuration key + * @param defaultValue Value returned if none is configured + * @return value if one exists, else the default value + */ + public boolean getBoolean(String key, boolean defaultValue) { + return rawConfig.getBoolean(accountConf(key), rawConfig.getBoolean(key, defaultValue)); + } + + /** + * Returns the account-specific value if it exists, then looks for an + * account-agnostic value, and finally tries the default value. + * @param key Account-agnostic configuration key + * @param defaultValue Value returned if none is configured + * @return value if one exists, else the default value + */ + public long getLong(String key, long defaultValue) { + return rawConfig.getLong(accountConf(key), rawConfig.getLong(key, defaultValue)); + } + + /** + * Returns the account-specific password in string form if it exists, then + * looks for an account-agnostic value. + * @param key Account-agnostic configuration key + * @return value in String form if one exists, else null + * @throws IOException + */ + public String getPasswordString(String key) throws IOException { + char[] passchars = rawConfig.getPassword(accountConf(key)); + if (passchars == null) { + passchars = rawConfig.getPassword(key); + } + if (passchars != null) { + return new String(passchars); + } + return null; + } + + /** + * Returns the account-specific Class if it exists, then looks for an + * account-agnostic value, and finally tries the default value. + * @param name Account-agnostic configuration key + * @param defaultValue Class returned if none is configured + * @param xface Interface shared by all possible values + * @return Highest-precedence Class object that was found + */ + public Class getClass(String name, Class defaultValue, Class xface) { + return rawConfig.getClass(accountConf(name), + rawConfig.getClass(name, defaultValue, xface), + xface); + } + + /** + * Returns the account-specific password in string form if it exists, then + * looks for an account-agnostic value. + * @param name Account-agnostic configuration key + * @param defaultValue Value returned if none is configured + * @return value in String form if one exists, else null + */ + public > T getEnum(String name, T defaultValue) { + return rawConfig.getEnum(accountConf(name), + rawConfig.getEnum(name, defaultValue)); + } + + /** + * Unsets parameter in the underlying Configuration object. + * Provided only as a convenience; does not add any account logic. + * @param key Configuration key + */ + public void unset(String key) { + rawConfig.unset(key); + } + + /** + * Sets String in the underlying Configuration object. + * Provided only as a convenience; does not add any account logic. + * @param key Configuration key + * @param value Configuration value + */ + public void set(String key, String value) { + rawConfig.set(key, value); + } + + /** + * Sets boolean in the underlying Configuration object. + * Provided only as a convenience; does not add any account logic. + * @param key Configuration key + * @param value Configuration value + */ + public void setBoolean(String key, boolean value) { + rawConfig.setBoolean(key, value); + } + + public boolean isSecureMode() { + return isSecure; + } + + public String getStorageAccountKey() throws AzureBlobFileSystemException { + String key; + String keyProviderClass = get(AZURE_KEY_ACCOUNT_KEYPROVIDER); + KeyProvider keyProvider; + + if (keyProviderClass == null) { + // No key provider was provided so use the provided key as is. + keyProvider = new SimpleKeyProvider(); + } else { + // create an instance of the key provider class and verify it + // implements KeyProvider + Object keyProviderObject; + try { + Class clazz = rawConfig.getClassByName(keyProviderClass); + keyProviderObject = clazz.newInstance(); + } catch (Exception e) { + throw new KeyProviderException("Unable to load key provider class.", e); + } + if (!(keyProviderObject instanceof KeyProvider)) { + throw new KeyProviderException(keyProviderClass + + " specified in config is not a valid KeyProvider class."); + } + keyProvider = (KeyProvider) keyProviderObject; + } + key = keyProvider.getStorageAccountKey(accountName, rawConfig); + + if (key == null) { + throw new ConfigurationPropertyNotFoundException(accountName); + } + + return key; + } + + public Configuration getRawConfiguration() { + return this.rawConfig; + } + + public int getWriteBufferSize() { + return this.writeBufferSize; + } + + public int getReadBufferSize() { + return this.readBufferSize; + } + + public int getMinBackoffIntervalMilliseconds() { + return this.minBackoffInterval; + } + + public int getMaxBackoffIntervalMilliseconds() { + return this.maxBackoffInterval; + } + + public int getBackoffIntervalMilliseconds() { + return this.backoffInterval; + } + + public int getMaxIoRetries() { + return this.maxIoRetries; + } + + public long getAzureBlockSize() { + return this.azureBlockSize; + } + + public String getAzureBlockLocationHost() { + return this.azureBlockLocationHost; + } + + public int getMaxConcurrentWriteThreads() { + return this.maxConcurrentWriteThreads; + } + + public int getMaxConcurrentReadThreads() { + return this.maxConcurrentReadThreads; + } + + public boolean getTolerateOobAppends() { + return this.tolerateOobAppends; + } + + public String getAzureAtomicRenameDirs() { + return this.azureAtomicDirs; + } + + public boolean getCreateRemoteFileSystemDuringInitialization() { + return this.createRemoteFileSystemDuringInitialization; + } + + public boolean getSkipUserGroupMetadataDuringInitialization() { + return this.skipUserGroupMetadataDuringInitialization; + } + + public int getReadAheadQueueDepth() { + return this.readAheadQueueDepth; + } + + public boolean isFlushEnabled() { + return this.enableFlush; + } + + public boolean isAutoThrottlingEnabled() { + return this.enableAutoThrottling; + } + + public String getCustomUserAgentPrefix() { + return this.userAgentId; + } + + public SSLSocketFactoryEx.SSLChannelMode getPreferredSSLFactoryOption() { + return getEnum(FS_AZURE_SSL_CHANNEL_MODE_KEY, DEFAULT_FS_AZURE_SSL_CHANNEL_MODE); + } + + public AuthType getAuthType(String accountName) { + return getEnum(FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME, AuthType.SharedKey); + } + + public boolean isDelegationTokenManagerEnabled() { + return enableDelegationToken; + } + + public AbfsDelegationTokenManager getDelegationTokenManager() throws IOException { + return new AbfsDelegationTokenManager(getRawConfiguration()); + } + + public AccessTokenProvider getTokenProvider() throws TokenAccessProviderException { + AuthType authType = getEnum(FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME, AuthType.SharedKey); + if (authType == AuthType.OAuth) { + try { + Class tokenProviderClass = + getClass(FS_AZURE_ACCOUNT_TOKEN_PROVIDER_TYPE_PROPERTY_NAME, null, + AccessTokenProvider.class); + AccessTokenProvider tokenProvider = null; + if (tokenProviderClass == ClientCredsTokenProvider.class) { + String authEndpoint = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ENDPOINT); + String clientId = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID); + String clientSecret = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_SECRET); + tokenProvider = new ClientCredsTokenProvider(authEndpoint, clientId, clientSecret); + } else if (tokenProviderClass == UserPasswordTokenProvider.class) { + String authEndpoint = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ENDPOINT); + String username = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_USER_NAME); + String password = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_USER_PASSWORD); + tokenProvider = new UserPasswordTokenProvider(authEndpoint, username, password); + } else if (tokenProviderClass == MsiTokenProvider.class) { + String tenantGuid = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT); + String clientId = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID); + tokenProvider = new MsiTokenProvider(tenantGuid, clientId); + } else if (tokenProviderClass == RefreshTokenBasedTokenProvider.class) { + String refreshToken = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_REFRESH_TOKEN); + String clientId = getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID); + tokenProvider = new RefreshTokenBasedTokenProvider(clientId, refreshToken); + } else { + throw new IllegalArgumentException("Failed to initialize " + tokenProviderClass); + } + return tokenProvider; + } catch(IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new TokenAccessProviderException("Unable to load key provider class.", e); + } + + } else if (authType == AuthType.Custom) { + try { + String configKey = FS_AZURE_ACCOUNT_TOKEN_PROVIDER_TYPE_PROPERTY_NAME; + Class customTokenProviderClass = + getClass(configKey, null, CustomTokenProviderAdaptee.class); + if (customTokenProviderClass == null) { + throw new IllegalArgumentException( + String.format("The configuration value for \"%s\" is invalid.", configKey)); + } + CustomTokenProviderAdaptee azureTokenProvider = ReflectionUtils + .newInstance(customTokenProviderClass, rawConfig); + if (azureTokenProvider == null) { + throw new IllegalArgumentException("Failed to initialize " + customTokenProviderClass); + } + azureTokenProvider.initialize(rawConfig, accountName); + return new CustomTokenProviderAdapter(azureTokenProvider); + } catch(IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new TokenAccessProviderException("Unable to load custom token provider class.", e); + } + + } else { + throw new TokenAccessProviderException(String.format( + "Invalid auth type: %s is being used, expecting OAuth", authType)); + } + } + + void validateStorageAccountKeys() throws InvalidConfigurationValueException { + Base64StringConfigurationBasicValidator validator = new Base64StringConfigurationBasicValidator( + FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME, "", true); + this.storageAccountKeys = rawConfig.getValByRegex(FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME_REGX); + + for (Map.Entry account : storageAccountKeys.entrySet()) { + validator.validate(account.getValue()); + } + } + + int validateInt(Field field) throws IllegalAccessException, InvalidConfigurationValueException { + IntegerConfigurationValidatorAnnotation validator = field.getAnnotation(IntegerConfigurationValidatorAnnotation.class); + String value = get(validator.ConfigurationKey()); + + // validate + return new IntegerConfigurationBasicValidator( + validator.MinValue(), + validator.MaxValue(), + validator.DefaultValue(), + validator.ConfigurationKey(), + validator.ThrowIfInvalid()).validate(value); + } + + long validateLong(Field field) throws IllegalAccessException, InvalidConfigurationValueException { + LongConfigurationValidatorAnnotation validator = field.getAnnotation(LongConfigurationValidatorAnnotation.class); + String value = rawConfig.get(validator.ConfigurationKey()); + + // validate + return new LongConfigurationBasicValidator( + validator.MinValue(), + validator.MaxValue(), + validator.DefaultValue(), + validator.ConfigurationKey(), + validator.ThrowIfInvalid()).validate(value); + } + + String validateString(Field field) throws IllegalAccessException, InvalidConfigurationValueException { + StringConfigurationValidatorAnnotation validator = field.getAnnotation(StringConfigurationValidatorAnnotation.class); + String value = rawConfig.get(validator.ConfigurationKey()); + + // validate + return new StringConfigurationBasicValidator( + validator.ConfigurationKey(), + validator.DefaultValue(), + validator.ThrowIfInvalid()).validate(value); + } + + String validateBase64String(Field field) throws IllegalAccessException, InvalidConfigurationValueException { + Base64StringConfigurationValidatorAnnotation validator = field.getAnnotation((Base64StringConfigurationValidatorAnnotation.class)); + String value = rawConfig.get(validator.ConfigurationKey()); + + // validate + return new Base64StringConfigurationBasicValidator( + validator.ConfigurationKey(), + validator.DefaultValue(), + validator.ThrowIfInvalid()).validate(value); + } + + boolean validateBoolean(Field field) throws IllegalAccessException, InvalidConfigurationValueException { + BooleanConfigurationValidatorAnnotation validator = field.getAnnotation(BooleanConfigurationValidatorAnnotation.class); + String value = rawConfig.get(validator.ConfigurationKey()); + + // validate + return new BooleanConfigurationBasicValidator( + validator.ConfigurationKey(), + validator.DefaultValue(), + validator.ThrowIfInvalid()).validate(value); + } + + @VisibleForTesting + void setReadBufferSize(int bufferSize) { + this.readBufferSize = bufferSize; + } + + @VisibleForTesting + void setWriteBufferSize(int bufferSize) { + this.writeBufferSize = bufferSize; + } + + @VisibleForTesting + void setEnableFlush(boolean enableFlush) { + this.enableFlush = enableFlush; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfss.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfss.java new file mode 100644 index 00000000000..c33265ce324 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/Abfss.java @@ -0,0 +1,46 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.DelegateToFileSystem; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; + +/** + * Azure Blob File System implementation of AbstractFileSystem. + * This impl delegates to the old FileSystem + */ +@InterfaceStability.Evolving +public class Abfss extends DelegateToFileSystem { + + Abfss(final URI theUri, final Configuration conf) throws IOException, + URISyntaxException { + super(theUri, new SecureAzureBlobFileSystem(), conf, FileSystemUriSchemes.ABFS_SECURE_SCHEME, false); + } + + @Override + public int getUriDefaultPort() { + return -1; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystem.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystem.java new file mode 100644 index 00000000000..200f3e77e0b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystem.java @@ -0,0 +1,953 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.apache.hadoop.fs.azurebfs.services.AbfsClient; +import org.apache.hadoop.fs.azurebfs.services.AbfsClientThrottlingIntercept; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.BlockLocation; +import org.apache.hadoop.fs.CreateFlag; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileAlreadyExistsException; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.FileSystemOperationUnhandledException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriAuthorityException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriException; +import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; +import org.apache.hadoop.fs.azurebfs.security.AbfsDelegationTokenManager; +import org.apache.hadoop.fs.permission.AclEntry; +import org.apache.hadoop.fs.permission.AclStatus; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.util.Progressable; + +/** + * A {@link org.apache.hadoop.fs.FileSystem} for reading and writing files stored on Windows Azure + */ +@InterfaceStability.Evolving +public class AzureBlobFileSystem extends FileSystem { + public static final Logger LOG = LoggerFactory.getLogger(AzureBlobFileSystem.class); + private URI uri; + private Path workingDir; + private UserGroupInformation userGroupInformation; + private String user; + private String primaryUserGroup; + private AzureBlobFileSystemStore abfsStore; + private boolean isClosed; + + private boolean delegationTokenEnabled = false; + private AbfsDelegationTokenManager delegationTokenManager; + + @Override + public void initialize(URI uri, Configuration configuration) + throws IOException { + uri = ensureAuthority(uri, configuration); + super.initialize(uri, configuration); + setConf(configuration); + + LOG.debug("Initializing AzureBlobFileSystem for {}", uri); + + this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority()); + this.userGroupInformation = UserGroupInformation.getCurrentUser(); + this.user = userGroupInformation.getUserName(); + this.abfsStore = new AzureBlobFileSystemStore(uri, this.isSecure(), configuration, userGroupInformation); + final AbfsConfiguration abfsConfiguration = abfsStore.getAbfsConfiguration(); + + this.setWorkingDirectory(this.getHomeDirectory()); + + if (abfsConfiguration.getCreateRemoteFileSystemDuringInitialization()) { + if (!this.fileSystemExists()) { + try { + this.createFileSystem(); + } catch (AzureBlobFileSystemException ex) { + checkException(null, ex, AzureServiceErrorCode.FILE_SYSTEM_ALREADY_EXISTS); + } + } + } + + if (!abfsConfiguration.getSkipUserGroupMetadataDuringInitialization()) { + this.primaryUserGroup = userGroupInformation.getPrimaryGroupName(); + } else { + //Provide a default group name + this.primaryUserGroup = this.user; + } + + if (UserGroupInformation.isSecurityEnabled()) { + this.delegationTokenEnabled = abfsConfiguration.isDelegationTokenManagerEnabled(); + + if (this.delegationTokenEnabled) { + LOG.debug("Initializing DelegationTokenManager for {}", uri); + this.delegationTokenManager = abfsConfiguration.getDelegationTokenManager(); + } + } + + AbfsClientThrottlingIntercept.initializeSingleton(abfsConfiguration.isAutoThrottlingEnabled()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder( + "AzureBlobFileSystem{"); + sb.append("uri=").append(uri); + sb.append(", user='").append(user).append('\''); + sb.append(", primaryUserGroup='").append(primaryUserGroup).append('\''); + sb.append('}'); + return sb.toString(); + } + + public boolean isSecure() { + return false; + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public FSDataInputStream open(final Path path, final int bufferSize) throws IOException { + LOG.debug("AzureBlobFileSystem.open path: {} bufferSize: {}", path, bufferSize); + + try { + InputStream inputStream = abfsStore.openFileForRead(makeQualified(path), statistics); + return new FSDataInputStream(inputStream); + } catch(AzureBlobFileSystemException ex) { + checkException(path, ex); + return null; + } + } + + @Override + public FSDataOutputStream create(final Path f, final FsPermission permission, final boolean overwrite, final int bufferSize, + final short replication, final long blockSize, final Progressable progress) throws IOException { + LOG.debug("AzureBlobFileSystem.create path: {} permission: {} overwrite: {} bufferSize: {}", + f, + permission, + overwrite, + blockSize); + + try { + OutputStream outputStream = abfsStore.createFile(makeQualified(f), overwrite, + permission == null ? FsPermission.getFileDefault() : permission, FsPermission.getUMask(getConf())); + return new FSDataOutputStream(outputStream, statistics); + } catch(AzureBlobFileSystemException ex) { + checkException(f, ex); + return null; + } + } + + @Override + @SuppressWarnings("deprecation") + public FSDataOutputStream createNonRecursive(final Path f, final FsPermission permission, + final boolean overwrite, final int bufferSize, final short replication, final long blockSize, + final Progressable progress) throws IOException { + + final Path parent = f.getParent(); + final FileStatus parentFileStatus = tryGetFileStatus(parent); + + if (parentFileStatus == null) { + throw new FileNotFoundException("Cannot create file " + + f.getName() + " because parent folder does not exist."); + } + + return create(f, permission, overwrite, bufferSize, replication, blockSize, progress); + } + + @Override + @SuppressWarnings("deprecation") + public FSDataOutputStream createNonRecursive(final Path f, final FsPermission permission, + final EnumSet flags, final int bufferSize, final short replication, final long blockSize, + final Progressable progress) throws IOException { + + // Check if file should be appended or overwritten. Assume that the file + // is overwritten on if the CREATE and OVERWRITE create flags are set. + final EnumSet createflags = + EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE); + final boolean overwrite = flags.containsAll(createflags); + + // Delegate the create non-recursive call. + return this.createNonRecursive(f, permission, overwrite, + bufferSize, replication, blockSize, progress); + } + + @Override + @SuppressWarnings("deprecation") + public FSDataOutputStream createNonRecursive(final Path f, + final boolean overwrite, final int bufferSize, final short replication, final long blockSize, + final Progressable progress) throws IOException { + return this.createNonRecursive(f, FsPermission.getFileDefault(), + overwrite, bufferSize, replication, blockSize, progress); + } + + @Override + public FSDataOutputStream append(final Path f, final int bufferSize, final Progressable progress) throws IOException { + LOG.debug( + "AzureBlobFileSystem.append path: {} bufferSize: {}", + f.toString(), + bufferSize); + + try { + OutputStream outputStream = abfsStore.openFileForWrite(makeQualified(f), false); + return new FSDataOutputStream(outputStream, statistics); + } catch(AzureBlobFileSystemException ex) { + checkException(f, ex); + return null; + } + } + + public boolean rename(final Path src, final Path dst) throws IOException { + LOG.debug( + "AzureBlobFileSystem.rename src: {} dst: {}", src.toString(), dst.toString()); + + Path parentFolder = src.getParent(); + if (parentFolder == null) { + return false; + } + + final FileStatus dstFileStatus = tryGetFileStatus(dst); + try { + String sourceFileName = src.getName(); + Path adjustedDst = dst; + + if (dstFileStatus != null) { + if (!dstFileStatus.isDirectory()) { + return src.equals(dst); + } + + adjustedDst = new Path(dst, sourceFileName); + } + + abfsStore.rename(makeQualified(src), makeQualified(adjustedDst)); + return true; + } catch(AzureBlobFileSystemException ex) { + checkException( + src, + ex, + AzureServiceErrorCode.PATH_ALREADY_EXISTS, + AzureServiceErrorCode.INVALID_RENAME_SOURCE_PATH, + AzureServiceErrorCode.SOURCE_PATH_NOT_FOUND, + AzureServiceErrorCode.INVALID_SOURCE_OR_DESTINATION_RESOURCE_TYPE, + AzureServiceErrorCode.RENAME_DESTINATION_PARENT_PATH_NOT_FOUND, + AzureServiceErrorCode.INTERNAL_OPERATION_ABORT); + return false; + } + + } + + @Override + public boolean delete(final Path f, final boolean recursive) throws IOException { + LOG.debug( + "AzureBlobFileSystem.delete path: {} recursive: {}", f.toString(), recursive); + + if (f.isRoot()) { + if (!recursive) { + return false; + } + + return deleteRoot(); + } + + try { + abfsStore.delete(makeQualified(f), recursive); + return true; + } catch (AzureBlobFileSystemException ex) { + checkException(f, ex, AzureServiceErrorCode.PATH_NOT_FOUND); + return false; + } + + } + + @Override + public FileStatus[] listStatus(final Path f) throws IOException { + LOG.debug( + "AzureBlobFileSystem.listStatus path: {}", f.toString()); + + try { + FileStatus[] result = abfsStore.listStatus(makeQualified(f)); + return result; + } catch (AzureBlobFileSystemException ex) { + checkException(f, ex); + return null; + } + } + + @Override + public boolean mkdirs(final Path f, final FsPermission permission) throws IOException { + LOG.debug( + "AzureBlobFileSystem.mkdirs path: {} permissions: {}", f, permission); + + final Path parentFolder = f.getParent(); + if (parentFolder == null) { + // Cannot create root + return true; + } + + try { + abfsStore.createDirectory(makeQualified(f), permission == null ? FsPermission.getDirDefault() : permission, + FsPermission.getUMask(getConf())); + return true; + } catch (AzureBlobFileSystemException ex) { + checkException(f, ex, AzureServiceErrorCode.PATH_ALREADY_EXISTS); + return true; + } + } + + @Override + public synchronized void close() throws IOException { + if (isClosed) { + return; + } + + super.close(); + LOG.debug("AzureBlobFileSystem.close"); + this.isClosed = true; + } + + @Override + public FileStatus getFileStatus(final Path f) throws IOException { + LOG.debug("AzureBlobFileSystem.getFileStatus path: {}", f); + + try { + return abfsStore.getFileStatus(makeQualified(f)); + } catch(AzureBlobFileSystemException ex) { + checkException(f, ex); + return null; + } + } + + /** + * Qualify a path to one which uses this FileSystem and, if relative, + * made absolute. + * @param path to qualify. + * @return this path if it contains a scheme and authority and is absolute, or + * a new path that includes a path and authority and is fully qualified + * @see Path#makeQualified(URI, Path) + * @throws IllegalArgumentException if the path has a schema/URI different + * from this FileSystem. + */ + @Override + public Path makeQualified(Path path) { + // To support format: abfs://{dfs.nameservices}/file/path, + // path need to be first converted to URI, then get the raw path string, + // during which {dfs.nameservices} will be omitted. + if (path != null) { + String uriPath = path.toUri().getPath(); + path = uriPath.isEmpty() ? path : new Path(uriPath); + } + return super.makeQualified(path); + } + + + @Override + public Path getWorkingDirectory() { + return this.workingDir; + } + + @Override + public void setWorkingDirectory(final Path newDir) { + if (newDir.isAbsolute()) { + this.workingDir = newDir; + } else { + this.workingDir = new Path(workingDir, newDir); + } + } + + @Override + public String getScheme() { + return FileSystemUriSchemes.ABFS_SCHEME; + } + + @Override + public Path getHomeDirectory() { + return makeQualified(new Path( + FileSystemConfigurations.USER_HOME_DIRECTORY_PREFIX + + "/" + this.userGroupInformation.getShortUserName())); + } + + /** + * Return an array containing hostnames, offset and size of + * portions of the given file. For ABFS we'll just lie and give + * fake hosts to make sure we get many splits in MR jobs. + */ + @Override + public BlockLocation[] getFileBlockLocations(FileStatus file, + long start, long len) { + if (file == null) { + return null; + } + + if ((start < 0) || (len < 0)) { + throw new IllegalArgumentException("Invalid start or len parameter"); + } + + if (file.getLen() < start) { + return new BlockLocation[0]; + } + final String blobLocationHost = abfsStore.getAbfsConfiguration().getAzureBlockLocationHost(); + + final String[] name = { blobLocationHost }; + final String[] host = { blobLocationHost }; + long blockSize = file.getBlockSize(); + if (blockSize <= 0) { + throw new IllegalArgumentException( + "The block size for the given file is not a positive number: " + + blockSize); + } + int numberOfLocations = (int) (len / blockSize) + + ((len % blockSize == 0) ? 0 : 1); + BlockLocation[] locations = new BlockLocation[numberOfLocations]; + for (int i = 0; i < locations.length; i++) { + long currentOffset = start + (i * blockSize); + long currentLength = Math.min(blockSize, start + len - currentOffset); + locations[i] = new BlockLocation(name, host, currentOffset, currentLength); + } + + return locations; + } + + @Override + protected void finalize() throws Throwable { + LOG.debug("finalize() called."); + close(); + super.finalize(); + } + + public String getOwnerUser() { + return user; + } + + public String getOwnerUserPrimaryGroup() { + return primaryUserGroup; + } + + private boolean deleteRoot() throws IOException { + LOG.debug("Deleting root content"); + + final ExecutorService executorService = Executors.newFixedThreadPool(10); + + try { + final FileStatus[] ls = listStatus(makeQualified(new Path(File.separator))); + final ArrayList deleteTasks = new ArrayList<>(); + for (final FileStatus fs : ls) { + final Future deleteTask = executorService.submit(new Callable() { + @Override + public Void call() throws Exception { + delete(fs.getPath(), fs.isDirectory()); + return null; + } + }); + deleteTasks.add(deleteTask); + } + + for (final Future deleteTask : deleteTasks) { + execute("deleteRoot", new Callable() { + @Override + public Void call() throws Exception { + deleteTask.get(); + return null; + } + }); + } + } + finally { + executorService.shutdownNow(); + } + + return true; + } + + /** + * Set owner of a path (i.e. a file or a directory). + * The parameters owner and group cannot both be null. + * + * @param path The path + * @param owner If it is null, the original username remains unchanged. + * @param group If it is null, the original groupname remains unchanged. + */ + @Override + public void setOwner(final Path path, final String owner, final String group) + throws IOException { + LOG.debug( + "AzureBlobFileSystem.setOwner path: {}", path); + if (!getIsNamespaceEnabeld()) { + super.setOwner(path, owner, group); + return; + } + + if ((owner == null || owner.isEmpty()) && (group == null || group.isEmpty())) { + throw new IllegalArgumentException("A valid owner or group must be specified."); + } + + try { + abfsStore.setOwner(makeQualified(path), + owner, + group); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Set permission of a path. + * + * @param path The path + * @param permission Access permission + */ + @Override + public void setPermission(final Path path, final FsPermission permission) + throws IOException { + LOG.debug("AzureBlobFileSystem.setPermission path: {}", path); + if (!getIsNamespaceEnabeld()) { + super.setPermission(path, permission); + return; + } + + if (permission == null) { + throw new IllegalArgumentException("The permission can't be null"); + } + + try { + abfsStore.setPermission(makeQualified(path), + permission); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Modifies ACL entries of files and directories. This method can add new ACL + * entries or modify the permissions on existing ACL entries. All existing + * ACL entries that are not specified in this call are retained without + * changes. (Modifications are merged into the current ACL.) + * + * @param path Path to modify + * @param aclSpec List of AbfsAclEntry describing modifications + * @throws IOException if an ACL could not be modified + */ + @Override + public void modifyAclEntries(final Path path, final List aclSpec) + throws IOException { + LOG.debug("AzureBlobFileSystem.modifyAclEntries path: {}", path.toString()); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "modifyAclEntries is only supported by storage accounts with the " + + "hierarchical namespace enabled."); + } + + if (aclSpec == null || aclSpec.isEmpty()) { + throw new IllegalArgumentException("The value of the aclSpec parameter is invalid."); + } + + try { + abfsStore.modifyAclEntries(makeQualified(path), + aclSpec); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Removes ACL entries from files and directories. Other ACL entries are + * retained. + * + * @param path Path to modify + * @param aclSpec List of AclEntry describing entries to remove + * @throws IOException if an ACL could not be modified + */ + @Override + public void removeAclEntries(final Path path, final List aclSpec) + throws IOException { + LOG.debug("AzureBlobFileSystem.removeAclEntries path: {}", path); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "removeAclEntries is only supported by storage accounts with the " + + "hierarchical namespace enabled."); + } + + if (aclSpec == null || aclSpec.isEmpty()) { + throw new IllegalArgumentException("The aclSpec argument is invalid."); + } + + try { + abfsStore.removeAclEntries(makeQualified(path), aclSpec); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Removes all default ACL entries from files and directories. + * + * @param path Path to modify + * @throws IOException if an ACL could not be modified + */ + @Override + public void removeDefaultAcl(final Path path) throws IOException { + LOG.debug("AzureBlobFileSystem.removeDefaultAcl path: {}", path); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "removeDefaultAcl is only supported by storage accounts with the " + + "hierarchical namespace enabled."); + } + + try { + abfsStore.removeDefaultAcl(makeQualified(path)); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Removes all but the base ACL entries of files and directories. The entries + * for user, group, and others are retained for compatibility with permission + * bits. + * + * @param path Path to modify + * @throws IOException if an ACL could not be removed + */ + @Override + public void removeAcl(final Path path) throws IOException { + LOG.debug("AzureBlobFileSystem.removeAcl path: {}", path); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "removeAcl is only supported by storage accounts with the " + + "hierarchical namespace enabled."); + } + + try { + abfsStore.removeAcl(makeQualified(path)); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Fully replaces ACL of files and directories, discarding all existing + * entries. + * + * @param path Path to modify + * @param aclSpec List of AclEntry describing modifications, must include + * entries for user, group, and others for compatibility with + * permission bits. + * @throws IOException if an ACL could not be modified + */ + @Override + public void setAcl(final Path path, final List aclSpec) + throws IOException { + LOG.debug("AzureBlobFileSystem.setAcl path: {}", path); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "setAcl is only supported by storage accounts with the hierarchical " + + "namespace enabled."); + } + + if (aclSpec == null || aclSpec.size() == 0) { + throw new IllegalArgumentException("The aclSpec argument is invalid."); + } + + try { + abfsStore.setAcl(makeQualified(path), aclSpec); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + } + } + + /** + * Gets the ACL of a file or directory. + * + * @param path Path to get + * @return AbfsAclStatus describing the ACL of the file or directory + * @throws IOException if an ACL could not be read + */ + @Override + public AclStatus getAclStatus(final Path path) throws IOException { + LOG.debug("AzureBlobFileSystem.getAclStatus path: {}", path.toString()); + + if (!getIsNamespaceEnabeld()) { + throw new UnsupportedOperationException( + "getAclStatus is only supported by storage account with the " + + "hierarchical namespace enabled."); + } + + try { + return abfsStore.getAclStatus(makeQualified(path)); + } catch (AzureBlobFileSystemException ex) { + checkException(path, ex); + return null; + } + } + + private FileStatus tryGetFileStatus(final Path f) { + try { + return getFileStatus(f); + } catch (IOException ex) { + LOG.debug("File not found {}", f); + return null; + } + } + + private boolean fileSystemExists() throws IOException { + LOG.debug( + "AzureBlobFileSystem.fileSystemExists uri: {}", uri); + try { + abfsStore.getFilesystemProperties(); + } catch (AzureBlobFileSystemException ex) { + try { + checkException(null, ex); + // Because HEAD request won't contain message body, + // there is not way to get the storage error code + // workaround here is to check its status code. + } catch (FileNotFoundException e) { + return false; + } + } + return true; + } + + private void createFileSystem() throws IOException { + LOG.debug( + "AzureBlobFileSystem.createFileSystem uri: {}", uri); + try { + abfsStore.createFilesystem(); + } catch (AzureBlobFileSystemException ex) { + checkException(null, ex); + } + } + + private URI ensureAuthority(URI uri, final Configuration conf) { + + Preconditions.checkNotNull(uri, "uri"); + + if (uri.getAuthority() == null) { + final URI defaultUri = FileSystem.getDefaultUri(conf); + + if (defaultUri != null && isAbfsScheme(defaultUri.getScheme())) { + try { + // Reconstruct the URI with the authority from the default URI. + uri = new URI( + uri.getScheme(), + defaultUri.getAuthority(), + uri.getPath(), + uri.getQuery(), + uri.getFragment()); + } catch (URISyntaxException e) { + // This should never happen. + throw new IllegalArgumentException(new InvalidUriException(uri.toString())); + } + } + } + + if (uri.getAuthority() == null) { + throw new IllegalArgumentException(new InvalidUriAuthorityException(uri.toString())); + } + + return uri; + } + + private boolean isAbfsScheme(final String scheme) { + if (scheme == null) { + return false; + } + + if (scheme.equals(FileSystemUriSchemes.ABFS_SCHEME) + || scheme.equals(FileSystemUriSchemes.ABFS_SECURE_SCHEME)) { + return true; + } + + return false; + } + + @VisibleForTesting + FileSystemOperation execute( + final String scopeDescription, + final Callable callableFileOperation) throws IOException { + return execute(scopeDescription, callableFileOperation, null); + } + + @VisibleForTesting + FileSystemOperation execute( + final String scopeDescription, + final Callable callableFileOperation, + T defaultResultValue) throws IOException { + + try { + final T executionResult = callableFileOperation.call(); + return new FileSystemOperation<>(executionResult, null); + } catch (AbfsRestOperationException abfsRestOperationException) { + return new FileSystemOperation<>(defaultResultValue, abfsRestOperationException); + } catch (AzureBlobFileSystemException azureBlobFileSystemException) { + throw new IOException(azureBlobFileSystemException); + } catch (Exception exception) { + if (exception instanceof ExecutionException) { + exception = (Exception) getRootCause(exception); + } + final FileSystemOperationUnhandledException fileSystemOperationUnhandledException + = new FileSystemOperationUnhandledException(exception); + throw new IOException(fileSystemOperationUnhandledException); + } + } + + /** + * Given a path and exception, choose which IOException subclass + * to create. + * Will return if and only iff the error code is in the list of allowed + * error codes. + * @param path path of operation triggering exception; may be null + * @param exception the exception caught + * @param allowedErrorCodesList varargs list of error codes. + * @throws IOException if the exception error code is not on the allowed list. + */ + private void checkException(final Path path, + final AzureBlobFileSystemException exception, + final AzureServiceErrorCode... allowedErrorCodesList) throws IOException { + if (exception instanceof AbfsRestOperationException) { + AbfsRestOperationException ere = (AbfsRestOperationException) exception; + + if (ArrayUtils.contains(allowedErrorCodesList, ere.getErrorCode())) { + return; + } + int statusCode = ere.getStatusCode(); + + //AbfsRestOperationException.getMessage() contains full error info including path/uri. + if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { + throw (IOException) new FileNotFoundException(ere.getMessage()) + .initCause(exception); + } else if (statusCode == HttpURLConnection.HTTP_CONFLICT) { + throw (IOException) new FileAlreadyExistsException(ere.getMessage()) + .initCause(exception); + } else { + throw ere; + } + } else { + if (path == null) { + throw exception; + } + // record info of path + throw new PathIOException(path.toString(), exception); + } + } + + /** + * Gets the root cause of a provided {@link Throwable}. If there is no cause for the + * {@link Throwable} provided into this function, the original {@link Throwable} is returned. + * + * @param throwable starting {@link Throwable} + * @return root cause {@link Throwable} + */ + private Throwable getRootCause(Throwable throwable) { + if (throwable == null) { + throw new IllegalArgumentException("throwable can not be null"); + } + + Throwable result = throwable; + while (result.getCause() != null) { + result = result.getCause(); + } + + return result; + } + + /** + * Get a delegation token from remote service endpoint if + * 'fs.azure.enable.kerberos.support' is set to 'true', and + * 'fs.azure.enable.delegation.token' is set to 'true'. + * @param renewer the account name that is allowed to renew the token. + * @return delegation token + * @throws IOException thrown when getting the current user. + */ + @Override + public synchronized Token getDelegationToken(final String renewer) throws IOException { + return this.delegationTokenEnabled ? this.delegationTokenManager.getDelegationToken(renewer) + : super.getDelegationToken(renewer); + } + + @VisibleForTesting + FileSystem.Statistics getFsStatistics() { + return this.statistics; + } + + @VisibleForTesting + static class FileSystemOperation { + private final T result; + private final AbfsRestOperationException exception; + + FileSystemOperation(final T result, final AbfsRestOperationException exception) { + this.result = result; + this.exception = exception; + } + + public boolean failed() { + return this.exception != null; + } + } + + @VisibleForTesting + AzureBlobFileSystemStore getAbfsStore() { + return abfsStore; + } + + @VisibleForTesting + AbfsClient getAbfsClient() { + return abfsStore.getClient(); + } + + @VisibleForTesting + boolean getIsNamespaceEnabeld() throws AzureBlobFileSystemException { + return abfsStore.getIsNamespaceEnabled(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystemStore.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystemStore.java new file mode 100644 index 00000000000..cf7387b6e6e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/AzureBlobFileSystemStore.java @@ -0,0 +1,1028 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.FileSystemOperationUnhandledException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidAbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidFileSystemPropertyException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriAuthorityException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.TimeoutException; +import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; +import org.apache.hadoop.fs.azurebfs.contracts.services.ListResultEntrySchema; +import org.apache.hadoop.fs.azurebfs.contracts.services.ListResultSchema; +import org.apache.hadoop.fs.azurebfs.oauth2.AccessTokenProvider; +import org.apache.hadoop.fs.azurebfs.services.AbfsAclHelper; +import org.apache.hadoop.fs.azurebfs.services.AbfsClient; +import org.apache.hadoop.fs.azurebfs.services.AbfsHttpOperation; +import org.apache.hadoop.fs.azurebfs.services.AbfsInputStream; +import org.apache.hadoop.fs.azurebfs.services.AbfsOutputStream; +import org.apache.hadoop.fs.azurebfs.services.AbfsPermission; +import org.apache.hadoop.fs.azurebfs.services.AbfsRestOperation; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.apache.hadoop.fs.azurebfs.services.ExponentialRetryPolicy; +import org.apache.hadoop.fs.azurebfs.services.SharedKeyCredentials; +import org.apache.hadoop.fs.azurebfs.utils.Base64; +import org.apache.hadoop.fs.azurebfs.utils.UriUtils; +import org.apache.hadoop.fs.permission.AclEntry; +import org.apache.hadoop.fs.permission.AclStatus; +import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.http.client.utils.URIBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.AZURE_ABFS_ENDPOINT; +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME; +import static org.apache.hadoop.util.Time.now; + +/** + * Provides the bridging logic between Hadoop's abstract filesystem and Azure Storage. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class AzureBlobFileSystemStore { + private static final Logger LOG = LoggerFactory.getLogger(AzureBlobFileSystemStore.class); + + private AbfsClient client; + private URI uri; + private final UserGroupInformation userGroupInformation; + private static final String DATE_TIME_PATTERN = "E, dd MMM yyyy HH:mm:ss 'GMT'"; + private static final String XMS_PROPERTIES_ENCODING = "ISO-8859-1"; + private static final int LIST_MAX_RESULTS = 5000; + private static final int DELETE_DIRECTORY_TIMEOUT_MILISECONDS = 180000; + private static final int RENAME_TIMEOUT_MILISECONDS = 180000; + + private final AbfsConfiguration abfsConfiguration; + private final Set azureAtomicRenameDirSet; + private boolean isNamespaceEnabledSet; + private boolean isNamespaceEnabled; + + public AzureBlobFileSystemStore(URI uri, boolean isSecure, Configuration configuration, UserGroupInformation userGroupInformation) + throws AzureBlobFileSystemException, IOException { + this.uri = uri; + + String[] authorityParts = authorityParts(uri); + final String fileSystemName = authorityParts[0]; + final String accountName = authorityParts[1]; + + try { + this.abfsConfiguration = new AbfsConfiguration(configuration, accountName); + } catch (IllegalAccessException exception) { + throw new FileSystemOperationUnhandledException(exception); + } + + this.userGroupInformation = userGroupInformation; + this.azureAtomicRenameDirSet = new HashSet<>(Arrays.asList( + abfsConfiguration.getAzureAtomicRenameDirs().split(AbfsHttpConstants.COMMA))); + + if (AuthType.OAuth == abfsConfiguration.getEnum(FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME, AuthType.SharedKey) + && !FileSystemUriSchemes.ABFS_SECURE_SCHEME.equals(uri.getScheme())) { + throw new IllegalArgumentException( + String.format("Incorrect URI %s, URI scheme must be abfss when authenticating using Oauth.", uri)); + } + + initializeClient(uri, fileSystemName, accountName, isSecure); + } + + private String[] authorityParts(URI uri) throws InvalidUriAuthorityException, InvalidUriException { + final String authority = uri.getRawAuthority(); + if (null == authority) { + throw new InvalidUriAuthorityException(uri.toString()); + } + + if (!authority.contains(AbfsHttpConstants.AZURE_DISTRIBUTED_FILE_SYSTEM_AUTHORITY_DELIMITER)) { + throw new InvalidUriAuthorityException(uri.toString()); + } + + final String[] authorityParts = authority.split(AbfsHttpConstants.AZURE_DISTRIBUTED_FILE_SYSTEM_AUTHORITY_DELIMITER, 2); + + if (authorityParts.length < 2 || authorityParts[0] != null + && authorityParts[0].isEmpty()) { + final String errMsg = String + .format("'%s' has a malformed authority, expected container name. " + + "Authority takes the form " + + FileSystemUriSchemes.ABFS_SCHEME + "://[@]", + uri.toString()); + throw new InvalidUriException(errMsg); + } + return authorityParts; + } + + public boolean getIsNamespaceEnabled() throws AzureBlobFileSystemException { + if (!isNamespaceEnabledSet) { + LOG.debug("getFilesystemProperties for filesystem: {}", + client.getFileSystem()); + + final AbfsRestOperation op = client.getFilesystemProperties(); + isNamespaceEnabled = Boolean.parseBoolean( + op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_NAMESPACE_ENABLED)); + isNamespaceEnabledSet = true; + } + + return isNamespaceEnabled; + } + + @VisibleForTesting + URIBuilder getURIBuilder(final String hostName, boolean isSecure) { + String scheme = isSecure ? FileSystemUriSchemes.HTTPS_SCHEME : FileSystemUriSchemes.HTTP_SCHEME; + + final URIBuilder uriBuilder = new URIBuilder(); + uriBuilder.setScheme(scheme); + + // For testing purposes, an IP address and port may be provided to override + // the host specified in the FileSystem URI. Also note that the format of + // the Azure Storage Service URI changes from + // http[s]://[account][domain-suffix]/[filesystem] to + // http[s]://[ip]:[port]/[account]/[filesystem]. + String endPoint = abfsConfiguration.get(AZURE_ABFS_ENDPOINT); + if (endPoint == null || !endPoint.contains(AbfsHttpConstants.COLON)) { + uriBuilder.setHost(hostName); + return uriBuilder; + } + + // Split ip and port + String[] data = endPoint.split(AbfsHttpConstants.COLON); + if (data.length != 2) { + throw new RuntimeException(String.format("ABFS endpoint is not set correctly : %s, " + + "Do not specify scheme when using {IP}:{PORT}", endPoint)); + } + uriBuilder.setHost(data[0].trim()); + uriBuilder.setPort(Integer.parseInt(data[1].trim())); + uriBuilder.setPath("/" + UriUtils.extractAccountNameFromHostName(hostName)); + + return uriBuilder; + } + + public AbfsConfiguration getAbfsConfiguration() { + return this.abfsConfiguration; + } + + public Hashtable getFilesystemProperties() throws AzureBlobFileSystemException { + LOG.debug("getFilesystemProperties for filesystem: {}", + client.getFileSystem()); + + final Hashtable parsedXmsProperties; + + final AbfsRestOperation op = client.getFilesystemProperties(); + final String xMsProperties = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_PROPERTIES); + + parsedXmsProperties = parseCommaSeparatedXmsProperties(xMsProperties); + + return parsedXmsProperties; + } + + public void setFilesystemProperties(final Hashtable properties) + throws AzureBlobFileSystemException { + if (properties == null || properties.isEmpty()) { + return; + } + + LOG.debug("setFilesystemProperties for filesystem: {} with properties: {}", + client.getFileSystem(), + properties); + + final String commaSeparatedProperties; + try { + commaSeparatedProperties = convertXmsPropertiesToCommaSeparatedString(properties); + } catch (CharacterCodingException ex) { + throw new InvalidAbfsRestOperationException(ex); + } + + client.setFilesystemProperties(commaSeparatedProperties); + } + + public Hashtable getPathProperties(final Path path) throws AzureBlobFileSystemException { + LOG.debug("getPathProperties for filesystem: {} path: {}", + client.getFileSystem(), + path); + + final Hashtable parsedXmsProperties; + final AbfsRestOperation op = client.getPathProperties(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path)); + + final String xMsProperties = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_PROPERTIES); + + parsedXmsProperties = parseCommaSeparatedXmsProperties(xMsProperties); + + return parsedXmsProperties; + } + + public void setPathProperties(final Path path, final Hashtable properties) throws AzureBlobFileSystemException { + LOG.debug("setFilesystemProperties for filesystem: {} path: {} with properties: {}", + client.getFileSystem(), + path, + properties); + + final String commaSeparatedProperties; + try { + commaSeparatedProperties = convertXmsPropertiesToCommaSeparatedString(properties); + } catch (CharacterCodingException ex) { + throw new InvalidAbfsRestOperationException(ex); + } + client.setPathProperties(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), commaSeparatedProperties); + } + + public void createFilesystem() throws AzureBlobFileSystemException { + LOG.debug("createFilesystem for filesystem: {}", + client.getFileSystem()); + + client.createFilesystem(); + } + + public void deleteFilesystem() throws AzureBlobFileSystemException { + LOG.debug("deleteFilesystem for filesystem: {}", + client.getFileSystem()); + + client.deleteFilesystem(); + } + + public OutputStream createFile(final Path path, final boolean overwrite, final FsPermission permission, + final FsPermission umask) throws AzureBlobFileSystemException { + boolean isNamespaceEnabled = getIsNamespaceEnabled(); + LOG.debug("createFile filesystem: {} path: {} overwrite: {} permission: {} umask: {} isNamespaceEnabled: {}", + client.getFileSystem(), + path, + overwrite, + permission.toString(), + umask.toString(), + isNamespaceEnabled); + + client.createPath(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), true, overwrite, + isNamespaceEnabled ? getOctalNotation(permission) : null, + isNamespaceEnabled ? getOctalNotation(umask) : null); + + return new AbfsOutputStream( + client, + AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), + 0, + abfsConfiguration.getWriteBufferSize(), + abfsConfiguration.isFlushEnabled()); + } + + public void createDirectory(final Path path, final FsPermission permission, final FsPermission umask) + throws AzureBlobFileSystemException { + boolean isNamespaceEnabled = getIsNamespaceEnabled(); + LOG.debug("createDirectory filesystem: {} path: {} permission: {} umask: {} isNamespaceEnabled: {}", + client.getFileSystem(), + path, + permission, + umask, + isNamespaceEnabled); + + client.createPath(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), false, true, + isNamespaceEnabled ? getOctalNotation(permission) : null, + isNamespaceEnabled ? getOctalNotation(umask) : null); + } + + public AbfsInputStream openFileForRead(final Path path, final FileSystem.Statistics statistics) + throws AzureBlobFileSystemException { + LOG.debug("openFileForRead filesystem: {} path: {}", + client.getFileSystem(), + path); + + final AbfsRestOperation op = client.getPathProperties(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path)); + + final String resourceType = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_RESOURCE_TYPE); + final long contentLength = Long.parseLong(op.getResult().getResponseHeader(HttpHeaderConfigurations.CONTENT_LENGTH)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + + if (parseIsDirectory(resourceType)) { + throw new AbfsRestOperationException( + AzureServiceErrorCode.PATH_NOT_FOUND.getStatusCode(), + AzureServiceErrorCode.PATH_NOT_FOUND.getErrorCode(), + "openFileForRead must be used with files and not directories", + null); + } + + // Add statistics for InputStream + return new AbfsInputStream(client, statistics, + AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), contentLength, + abfsConfiguration.getReadBufferSize(), abfsConfiguration.getReadAheadQueueDepth(), eTag); + } + + public OutputStream openFileForWrite(final Path path, final boolean overwrite) throws + AzureBlobFileSystemException { + LOG.debug("openFileForWrite filesystem: {} path: {} overwrite: {}", + client.getFileSystem(), + path, + overwrite); + + final AbfsRestOperation op = client.getPathProperties(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path)); + + final String resourceType = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_RESOURCE_TYPE); + final Long contentLength = Long.valueOf(op.getResult().getResponseHeader(HttpHeaderConfigurations.CONTENT_LENGTH)); + + if (parseIsDirectory(resourceType)) { + throw new AbfsRestOperationException( + AzureServiceErrorCode.PATH_NOT_FOUND.getStatusCode(), + AzureServiceErrorCode.PATH_NOT_FOUND.getErrorCode(), + "openFileForRead must be used with files and not directories", + null); + } + + final long offset = overwrite ? 0 : contentLength; + + return new AbfsOutputStream( + client, + AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), + offset, + abfsConfiguration.getWriteBufferSize(), + abfsConfiguration.isFlushEnabled()); + } + + public void rename(final Path source, final Path destination) throws + AzureBlobFileSystemException { + + if (isAtomicRenameKey(source.getName())) { + LOG.warn("The atomic rename feature is not supported by the ABFS scheme; however rename," + +" create and delete operations are atomic if Namespace is enabled for your Azure Storage account."); + } + + LOG.debug("renameAsync filesystem: {} source: {} destination: {}", + client.getFileSystem(), + source, + destination); + + String continuation = null; + long deadline = now() + RENAME_TIMEOUT_MILISECONDS; + + do { + if (now() > deadline) { + LOG.debug("Rename {} to {} timed out.", + source, + destination); + + throw new TimeoutException("Rename timed out."); + } + + AbfsRestOperation op = client.renamePath(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(source), + AbfsHttpConstants.FORWARD_SLASH + getRelativePath(destination), continuation); + continuation = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_CONTINUATION); + + } while (continuation != null && !continuation.isEmpty()); + } + + public void delete(final Path path, final boolean recursive) + throws AzureBlobFileSystemException { + LOG.debug("delete filesystem: {} path: {} recursive: {}", + client.getFileSystem(), + path, + String.valueOf(recursive)); + + String continuation = null; + long deadline = now() + DELETE_DIRECTORY_TIMEOUT_MILISECONDS; + + do { + if (now() > deadline) { + LOG.debug("Delete directory {} timed out.", path); + + throw new TimeoutException("Delete directory timed out."); + } + + AbfsRestOperation op = client.deletePath( + AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path), recursive, continuation); + continuation = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_CONTINUATION); + + } while (continuation != null && !continuation.isEmpty()); + } + + public FileStatus getFileStatus(final Path path) throws IOException { + boolean isNamespaceEnabled = getIsNamespaceEnabled(); + LOG.debug("getFileStatus filesystem: {} path: {} isNamespaceEnabled: {}", + client.getFileSystem(), + path, + isNamespaceEnabled); + + if (path.isRoot()) { + final AbfsRestOperation op = isNamespaceEnabled + ? client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + AbfsHttpConstants.ROOT_PATH) + : client.getFilesystemProperties(); + + final long blockSize = abfsConfiguration.getAzureBlockSize(); + final String owner = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_OWNER); + final String group = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_GROUP); + final String permissions = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + final String lastModified = op.getResult().getResponseHeader(HttpHeaderConfigurations.LAST_MODIFIED); + final boolean hasAcl = AbfsPermission.isExtendedAcl(permissions); + + return new VersionedFileStatus( + owner == null ? userGroupInformation.getUserName() : owner, + group == null ? userGroupInformation.getPrimaryGroupName() : group, + permissions == null ? new AbfsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL) + : AbfsPermission.valueOf(permissions), + hasAcl, + 0, + true, + 1, + blockSize, + parseLastModifiedTime(lastModified), + path, + eTag); + } else { + AbfsRestOperation op = client.getPathProperties(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path)); + + final long blockSize = abfsConfiguration.getAzureBlockSize(); + final AbfsHttpOperation result = op.getResult(); + final String eTag = result.getResponseHeader(HttpHeaderConfigurations.ETAG); + final String lastModified = result.getResponseHeader(HttpHeaderConfigurations.LAST_MODIFIED); + final String contentLength = result.getResponseHeader(HttpHeaderConfigurations.CONTENT_LENGTH); + final String resourceType = result.getResponseHeader(HttpHeaderConfigurations.X_MS_RESOURCE_TYPE); + final String owner = result.getResponseHeader(HttpHeaderConfigurations.X_MS_OWNER); + final String group = result.getResponseHeader(HttpHeaderConfigurations.X_MS_GROUP); + final String permissions = result.getResponseHeader((HttpHeaderConfigurations.X_MS_PERMISSIONS)); + final boolean hasAcl = AbfsPermission.isExtendedAcl(permissions); + + return new VersionedFileStatus( + owner == null ? userGroupInformation.getUserName() : owner, + group == null ? userGroupInformation.getPrimaryGroupName() : group, + permissions == null ? new AbfsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL) + : AbfsPermission.valueOf(permissions), + hasAcl, + parseContentLength(contentLength), + parseIsDirectory(resourceType), + 1, + blockSize, + parseLastModifiedTime(lastModified), + path, + eTag); + } + } + + public FileStatus[] listStatus(final Path path) throws IOException { + LOG.debug("listStatus filesystem: {} path: {}", + client.getFileSystem(), + path); + + String relativePath = path.isRoot() ? AbfsHttpConstants.EMPTY_STRING : getRelativePath(path); + String continuation = null; + ArrayList fileStatuses = new ArrayList<>(); + + do { + AbfsRestOperation op = client.listPath(relativePath, false, LIST_MAX_RESULTS, continuation); + continuation = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_CONTINUATION); + ListResultSchema retrievedSchema = op.getResult().getListResultSchema(); + if (retrievedSchema == null) { + throw new AbfsRestOperationException( + AzureServiceErrorCode.PATH_NOT_FOUND.getStatusCode(), + AzureServiceErrorCode.PATH_NOT_FOUND.getErrorCode(), + "listStatusAsync path not found", + null, op.getResult()); + } + + long blockSize = abfsConfiguration.getAzureBlockSize(); + + for (ListResultEntrySchema entry : retrievedSchema.paths()) { + final String owner = entry.owner() == null ? userGroupInformation.getUserName() : entry.owner(); + final String group = entry.group() == null ? userGroupInformation.getPrimaryGroupName() : entry.group(); + final FsPermission fsPermission = entry.permissions() == null + ? new AbfsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL) + : AbfsPermission.valueOf(entry.permissions()); + final boolean hasAcl = AbfsPermission.isExtendedAcl(entry.permissions()); + + long lastModifiedMillis = 0; + long contentLength = entry.contentLength() == null ? 0 : entry.contentLength(); + boolean isDirectory = entry.isDirectory() == null ? false : entry.isDirectory(); + if (entry.lastModified() != null && !entry.lastModified().isEmpty()) { + lastModifiedMillis = parseLastModifiedTime(entry.lastModified()); + } + + Path entryPath = new Path(File.separator + entry.name()); + entryPath = entryPath.makeQualified(this.uri, entryPath); + + fileStatuses.add( + new VersionedFileStatus( + owner, + group, + fsPermission, + hasAcl, + contentLength, + isDirectory, + 1, + blockSize, + lastModifiedMillis, + entryPath, + entry.eTag())); + } + + } while (continuation != null && !continuation.isEmpty()); + + return fileStatuses.toArray(new FileStatus[0]); + } + + public void setOwner(final Path path, final String owner, final String group) throws + AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "setOwner filesystem: {} path: {} owner: {} group: {}", + client.getFileSystem(), + path.toString(), + owner, + group); + client.setOwner(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), owner, group); + } + + public void setPermission(final Path path, final FsPermission permission) throws + AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "setPermission filesystem: {} path: {} permission: {}", + client.getFileSystem(), + path.toString(), + permission.toString()); + client.setPermission(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + String.format(AbfsHttpConstants.PERMISSION_FORMAT, permission.toOctal())); + } + + public void modifyAclEntries(final Path path, final List aclSpec) throws + AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "modifyAclEntries filesystem: {} path: {} aclSpec: {}", + client.getFileSystem(), + path.toString(), + AclEntry.aclSpecToString(aclSpec)); + + final Map modifyAclEntries = AbfsAclHelper.deserializeAclSpec(AclEntry.aclSpecToString(aclSpec)); + + final AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + + final Map aclEntries = AbfsAclHelper.deserializeAclSpec(op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL)); + + for (Map.Entry modifyAclEntry : modifyAclEntries.entrySet()) { + aclEntries.put(modifyAclEntry.getKey(), modifyAclEntry.getValue()); + } + + if (!modifyAclEntries.containsKey(AbfsHttpConstants.ACCESS_MASK)) { + aclEntries.remove(AbfsHttpConstants.ACCESS_MASK); + } + + if (!modifyAclEntries.containsKey(AbfsHttpConstants.DEFAULT_MASK)) { + aclEntries.remove(AbfsHttpConstants.DEFAULT_MASK); + } + + client.setAcl(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + AbfsAclHelper.serializeAclSpec(aclEntries), eTag); + } + + public void removeAclEntries(final Path path, final List aclSpec) throws AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "removeAclEntries filesystem: {} path: {} aclSpec: {}", + client.getFileSystem(), + path.toString(), + AclEntry.aclSpecToString(aclSpec)); + + final Map removeAclEntries = AbfsAclHelper.deserializeAclSpec(AclEntry.aclSpecToString(aclSpec)); + final AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + + final Map aclEntries = AbfsAclHelper.deserializeAclSpec(op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL)); + + AbfsAclHelper.removeAclEntriesInternal(aclEntries, removeAclEntries); + + client.setAcl(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + AbfsAclHelper.serializeAclSpec(aclEntries), eTag); + } + + public void removeDefaultAcl(final Path path) throws AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "removeDefaultAcl filesystem: {} path: {}", + client.getFileSystem(), + path.toString()); + + final AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + final Map aclEntries = AbfsAclHelper.deserializeAclSpec(op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL)); + final Map defaultAclEntries = new HashMap<>(); + + for (Map.Entry aclEntry : aclEntries.entrySet()) { + if (aclEntry.getKey().startsWith("default:")) { + defaultAclEntries.put(aclEntry.getKey(), aclEntry.getValue()); + } + } + + for (Map.Entry defaultAclEntry : defaultAclEntries.entrySet()) { + aclEntries.remove(defaultAclEntry.getKey()); + } + + client.setAcl(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + AbfsAclHelper.serializeAclSpec(aclEntries), eTag); + } + + public void removeAcl(final Path path) throws AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "removeAcl filesystem: {} path: {}", + client.getFileSystem(), + path.toString()); + final AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + + final Map aclEntries = AbfsAclHelper.deserializeAclSpec(op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL)); + final Map newAclEntries = new HashMap<>(); + + newAclEntries.put(AbfsHttpConstants.ACCESS_USER, aclEntries.get(AbfsHttpConstants.ACCESS_USER)); + newAclEntries.put(AbfsHttpConstants.ACCESS_GROUP, aclEntries.get(AbfsHttpConstants.ACCESS_GROUP)); + newAclEntries.put(AbfsHttpConstants.ACCESS_OTHER, aclEntries.get(AbfsHttpConstants.ACCESS_OTHER)); + + client.setAcl(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + AbfsAclHelper.serializeAclSpec(newAclEntries), eTag); + } + + public void setAcl(final Path path, final List aclSpec) throws AzureBlobFileSystemException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "setAcl filesystem: {} path: {} aclspec: {}", + client.getFileSystem(), + path.toString(), + AclEntry.aclSpecToString(aclSpec)); + final Map aclEntries = AbfsAclHelper.deserializeAclSpec(AclEntry.aclSpecToString(aclSpec)); + final AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + final String eTag = op.getResult().getResponseHeader(HttpHeaderConfigurations.ETAG); + + final Map getAclEntries = AbfsAclHelper.deserializeAclSpec(op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL)); + for (Map.Entry ace : getAclEntries.entrySet()) { + if (ace.getKey().startsWith("default:") && (ace.getKey() != AbfsHttpConstants.DEFAULT_MASK) + && !aclEntries.containsKey(ace.getKey())) { + aclEntries.put(ace.getKey(), ace.getValue()); + } + } + + client.setAcl(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true), + AbfsAclHelper.serializeAclSpec(aclEntries), eTag); + } + + public AclStatus getAclStatus(final Path path) throws IOException { + if (!getIsNamespaceEnabled()) { + throw new UnsupportedOperationException( + "This operation is only valid for storage accounts with the hierarchical namespace enabled."); + } + + LOG.debug( + "getAclStatus filesystem: {} path: {}", + client.getFileSystem(), + path.toString()); + AbfsRestOperation op = client.getAclStatus(AbfsHttpConstants.FORWARD_SLASH + getRelativePath(path, true)); + AbfsHttpOperation result = op.getResult(); + + final String owner = result.getResponseHeader(HttpHeaderConfigurations.X_MS_OWNER); + final String group = result.getResponseHeader(HttpHeaderConfigurations.X_MS_GROUP); + final String permissions = result.getResponseHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS); + final String aclSpecString = op.getResult().getResponseHeader(HttpHeaderConfigurations.X_MS_ACL); + + final List processedAclEntries = AclEntry.parseAclSpec(AbfsAclHelper.processAclString(aclSpecString), true); + final FsPermission fsPermission = permissions == null ? new AbfsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL) + : AbfsPermission.valueOf(permissions); + + final AclStatus.Builder aclStatusBuilder = new AclStatus.Builder(); + aclStatusBuilder.owner(owner == null ? userGroupInformation.getUserName() : owner); + aclStatusBuilder.group(group == null ? userGroupInformation.getPrimaryGroupName() : group); + + aclStatusBuilder.setPermission(fsPermission); + aclStatusBuilder.stickyBit(fsPermission.getStickyBit()); + aclStatusBuilder.addEntries(processedAclEntries); + return aclStatusBuilder.build(); + } + + public boolean isAtomicRenameKey(String key) { + return isKeyForDirectorySet(key, azureAtomicRenameDirSet); + } + + private void initializeClient(URI uri, String fileSystemName, String accountName, boolean isSecure) throws AzureBlobFileSystemException { + if (this.client != null) { + return; + } + + final URIBuilder uriBuilder = getURIBuilder(accountName, isSecure); + + final String url = uriBuilder.toString() + AbfsHttpConstants.FORWARD_SLASH + fileSystemName; + + URL baseUrl; + try { + baseUrl = new URL(url); + } catch (MalformedURLException e) { + throw new InvalidUriException(uri.toString()); + } + + SharedKeyCredentials creds = null; + AccessTokenProvider tokenProvider = null; + + if (abfsConfiguration.getAuthType(accountName) == AuthType.SharedKey) { + int dotIndex = accountName.indexOf(AbfsHttpConstants.DOT); + if (dotIndex <= 0) { + throw new InvalidUriException( + uri.toString() + " - account name is not fully qualified."); + } + creds = new SharedKeyCredentials(accountName.substring(0, dotIndex), + abfsConfiguration.getStorageAccountKey()); + } else { + tokenProvider = abfsConfiguration.getTokenProvider(); + } + + this.client = new AbfsClient(baseUrl, creds, abfsConfiguration, new ExponentialRetryPolicy(), tokenProvider); + } + + private String getOctalNotation(FsPermission fsPermission) { + Preconditions.checkNotNull(fsPermission, "fsPermission"); + return String.format(AbfsHttpConstants.PERMISSION_FORMAT, fsPermission.toOctal()); + } + + private String getRelativePath(final Path path) { + return getRelativePath(path, false); + } + + private String getRelativePath(final Path path, final boolean allowRootPath) { + Preconditions.checkNotNull(path, "path"); + final String relativePath = path.toUri().getPath(); + + if (relativePath.length() == 0 || (relativePath.length() == 1 && relativePath.charAt(0) == Path.SEPARATOR_CHAR)) { + return allowRootPath ? AbfsHttpConstants.ROOT_PATH : AbfsHttpConstants.EMPTY_STRING; + } + + if (relativePath.charAt(0) == Path.SEPARATOR_CHAR) { + return relativePath.substring(1); + } + + return relativePath; + } + + private long parseContentLength(final String contentLength) { + if (contentLength == null) { + return -1; + } + + return Long.parseLong(contentLength); + } + + private boolean parseIsDirectory(final String resourceType) { + return resourceType != null + && resourceType.equalsIgnoreCase(AbfsHttpConstants.DIRECTORY); + } + + private long parseLastModifiedTime(final String lastModifiedTime) { + long parsedTime = 0; + try { + Date utcDate = new SimpleDateFormat(DATE_TIME_PATTERN).parse(lastModifiedTime); + parsedTime = utcDate.getTime(); + } catch (ParseException e) { + LOG.error("Failed to parse the date {}", lastModifiedTime); + } finally { + return parsedTime; + } + } + + private String convertXmsPropertiesToCommaSeparatedString(final Hashtable properties) throws + CharacterCodingException { + StringBuilder commaSeparatedProperties = new StringBuilder(); + + final CharsetEncoder encoder = Charset.forName(XMS_PROPERTIES_ENCODING).newEncoder(); + + for (Map.Entry propertyEntry : properties.entrySet()) { + String key = propertyEntry.getKey(); + String value = propertyEntry.getValue(); + + Boolean canEncodeValue = encoder.canEncode(value); + if (!canEncodeValue) { + throw new CharacterCodingException(); + } + + String encodedPropertyValue = Base64.encode(encoder.encode(CharBuffer.wrap(value)).array()); + commaSeparatedProperties.append(key) + .append(AbfsHttpConstants.EQUAL) + .append(encodedPropertyValue); + + commaSeparatedProperties.append(AbfsHttpConstants.COMMA); + } + + if (commaSeparatedProperties.length() != 0) { + commaSeparatedProperties.deleteCharAt(commaSeparatedProperties.length() - 1); + } + + return commaSeparatedProperties.toString(); + } + + private Hashtable parseCommaSeparatedXmsProperties(String xMsProperties) throws + InvalidFileSystemPropertyException, InvalidAbfsRestOperationException { + Hashtable properties = new Hashtable<>(); + + final CharsetDecoder decoder = Charset.forName(XMS_PROPERTIES_ENCODING).newDecoder(); + + if (xMsProperties != null && !xMsProperties.isEmpty()) { + String[] userProperties = xMsProperties.split(AbfsHttpConstants.COMMA); + + if (userProperties.length == 0) { + return properties; + } + + for (String property : userProperties) { + if (property.isEmpty()) { + throw new InvalidFileSystemPropertyException(xMsProperties); + } + + String[] nameValue = property.split(AbfsHttpConstants.EQUAL, 2); + if (nameValue.length != 2) { + throw new InvalidFileSystemPropertyException(xMsProperties); + } + + byte[] decodedValue = Base64.decode(nameValue[1]); + + final String value; + try { + value = decoder.decode(ByteBuffer.wrap(decodedValue)).toString(); + } catch (CharacterCodingException ex) { + throw new InvalidAbfsRestOperationException(ex); + } + properties.put(nameValue[0], value); + } + } + + return properties; + } + + private boolean isKeyForDirectorySet(String key, Set dirSet) { + for (String dir : dirSet) { + if (dir.isEmpty() || key.startsWith(dir + AbfsHttpConstants.FORWARD_SLASH)) { + return true; + } + + try { + URI uri = new URI(dir); + if (null == uri.getAuthority()) { + if (key.startsWith(dir + "/")){ + return true; + } + } + } catch (URISyntaxException e) { + LOG.info("URI syntax error creating URI for {}", dir); + } + } + + return false; + } + + private static class VersionedFileStatus extends FileStatus { + private final String version; + + VersionedFileStatus( + final String owner, final String group, final FsPermission fsPermission, final boolean hasAcl, + final long length, final boolean isdir, final int blockReplication, + final long blocksize, final long modificationTime, final Path path, + String version) { + super(length, isdir, blockReplication, blocksize, modificationTime, 0, + fsPermission, + owner, + group, + null, + path, + hasAcl, false, false); + + this.version = version; + } + + /** Compare if this object is equal to another object. + * @param obj the object to be compared. + * @return true if two file status has the same path name; false if not. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FileStatus)) { + return false; + } + + FileStatus other = (FileStatus) obj; + + if (!other.equals(this)) {// compare the path + return false; + } + + if (other instanceof VersionedFileStatus) { + return this.version.equals(((VersionedFileStatus) other).version); + } + + return true; + } + + /** + * Returns a hash code value for the object, which is defined as + * the hash code of the path name. + * + * @return a hash code value for the path name and version + */ + @Override + public int hashCode() { + int hash = getPath().hashCode(); + hash = 89 * hash + (this.version != null ? this.version.hashCode() : 0); + return hash; + } + + /** + * Returns the version of this FileStatus + * + * @return a string value for the FileStatus version + */ + public String getVersion() { + return this.version; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder( + "VersionedFileStatus{"); + sb.append(super.toString()); + sb.append("; version='").append(version).append('\''); + sb.append('}'); + return sb.toString(); + } + } + + @VisibleForTesting + AbfsClient getClient() { + return this.client; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/SecureAzureBlobFileSystem.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/SecureAzureBlobFileSystem.java new file mode 100644 index 00000000000..15fe5427252 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/SecureAzureBlobFileSystem.java @@ -0,0 +1,39 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; + +/** + * A secure {@link org.apache.hadoop.fs.FileSystem} for reading and writing files stored on Windows Azure + */ +@InterfaceStability.Evolving +public class SecureAzureBlobFileSystem extends AzureBlobFileSystem { + @Override + public boolean isSecure() { + return true; + } + + @Override + public String getScheme() { + return FileSystemUriSchemes.ABFS_SECURE_SCHEME; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/AbfsHttpConstants.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/AbfsHttpConstants.java new file mode 100644 index 00000000000..447b6819e3b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/AbfsHttpConstants.java @@ -0,0 +1,91 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Responsible to keep all constant keys used in abfs rest client here. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class AbfsHttpConstants { + // Abfs Http client constants + public static final String FILESYSTEM = "filesystem"; + public static final String FILE = "file"; + public static final String DIRECTORY = "directory"; + public static final String APPEND_ACTION = "append"; + public static final String FLUSH_ACTION = "flush"; + public static final String SET_PROPERTIES_ACTION = "setProperties"; + public static final String SET_ACCESS_CONTROL = "setAccessControl"; + public static final String GET_ACCESS_CONTROL = "getAccessControl"; + public static final String DEFAULT_TIMEOUT = "90"; + + public static final String JAVA_VERSION = "java.version"; + public static final String OS_NAME = "os.name"; + public static final String OS_VERSION = "os.version"; + + public static final String CLIENT_VERSION = "Azure Blob FS/1.0"; + + // Abfs Http Verb + public static final String HTTP_METHOD_DELETE = "DELETE"; + public static final String HTTP_METHOD_GET = "GET"; + public static final String HTTP_METHOD_HEAD = "HEAD"; + public static final String HTTP_METHOD_PATCH = "PATCH"; + public static final String HTTP_METHOD_POST = "POST"; + public static final String HTTP_METHOD_PUT = "PUT"; + + // Abfs generic constants + public static final String SINGLE_WHITE_SPACE = " "; + public static final String EMPTY_STRING = ""; + public static final String FORWARD_SLASH = "/"; + public static final String DOT = "."; + public static final String PLUS = "+"; + public static final String STAR = "*"; + public static final String COMMA = ","; + public static final String COLON = ":"; + public static final String EQUAL = "="; + public static final String QUESTION_MARK = "?"; + public static final String AND_MARK = "&"; + public static final String SEMICOLON = ";"; + public static final String HTTP_HEADER_PREFIX = "x-ms-"; + + public static final String PLUS_ENCODE = "%20"; + public static final String FORWARD_SLASH_ENCODE = "%2F"; + public static final String AZURE_DISTRIBUTED_FILE_SYSTEM_AUTHORITY_DELIMITER = "@"; + public static final String UTF_8 = "utf-8"; + public static final String GMT_TIMEZONE = "GMT"; + public static final String APPLICATION_JSON = "application/json"; + public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + + public static final String ROOT_PATH = "/"; + public static final String ACCESS_MASK = "mask:"; + public static final String ACCESS_USER = "user:"; + public static final String ACCESS_GROUP = "group:"; + public static final String ACCESS_OTHER = "other:"; + public static final String DEFAULT_MASK = "default:mask:"; + public static final String DEFAULT_USER = "default:user:"; + public static final String DEFAULT_GROUP = "default:group:"; + public static final String DEFAULT_OTHER = "default:other:"; + public static final String DEFAULT_SCOPE = "default:"; + public static final String PERMISSION_FORMAT = "%04d"; + + private AbfsHttpConstants() {} +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/ConfigurationKeys.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/ConfigurationKeys.java new file mode 100644 index 00000000000..13cdaeb4349 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/ConfigurationKeys.java @@ -0,0 +1,89 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Responsible to keep all the Azure Blob File System configurations keys in Hadoop configuration file. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class ConfigurationKeys { + public static final String FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME = "fs.azure.account.key"; + public static final String FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME_REGX = "fs\\.azure\\.account\\.key\\.(.*)"; + public static final String FS_AZURE_SECURE_MODE = "fs.azure.secure.mode"; + + // Retry strategy defined by the user + public static final String AZURE_MIN_BACKOFF_INTERVAL = "fs.azure.io.retry.min.backoff.interval"; + public static final String AZURE_MAX_BACKOFF_INTERVAL = "fs.azure.io.retry.max.backoff.interval"; + public static final String AZURE_BACKOFF_INTERVAL = "fs.azure.io.retry.backoff.interval"; + public static final String AZURE_MAX_IO_RETRIES = "fs.azure.io.retry.max.retries"; + + // Read and write buffer sizes defined by the user + public static final String AZURE_WRITE_BUFFER_SIZE = "fs.azure.write.request.size"; + public static final String AZURE_READ_BUFFER_SIZE = "fs.azure.read.request.size"; + public static final String AZURE_BLOCK_SIZE_PROPERTY_NAME = "fs.azure.block.size"; + public static final String AZURE_BLOCK_LOCATION_HOST_PROPERTY_NAME = "fs.azure.block.location.impersonatedhost"; + public static final String AZURE_CONCURRENT_CONNECTION_VALUE_OUT = "fs.azure.concurrentRequestCount.out"; + public static final String AZURE_CONCURRENT_CONNECTION_VALUE_IN = "fs.azure.concurrentRequestCount.in"; + public static final String AZURE_TOLERATE_CONCURRENT_APPEND = "fs.azure.io.read.tolerate.concurrent.append"; + public static final String AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION = "fs.azure.createRemoteFileSystemDuringInitialization"; + public static final String AZURE_SKIP_USER_GROUP_METADATA_DURING_INITIALIZATION = "fs.azure.skipUserGroupMetadataDuringInitialization"; + public static final String FS_AZURE_ENABLE_AUTOTHROTTLING = "fs.azure.enable.autothrottling"; + public static final String FS_AZURE_ATOMIC_RENAME_KEY = "fs.azure.atomic.rename.key"; + public static final String FS_AZURE_READ_AHEAD_QUEUE_DEPTH = "fs.azure.readaheadqueue.depth"; + public static final String FS_AZURE_ENABLE_FLUSH = "fs.azure.enable.flush"; + public static final String FS_AZURE_USER_AGENT_PREFIX_KEY = "fs.azure.user.agent.prefix"; + public static final String FS_AZURE_SSL_CHANNEL_MODE_KEY = "fs.azure.ssl.channel.mode"; + + public static final String AZURE_KEY_ACCOUNT_KEYPROVIDER = "fs.azure.account.keyprovider"; + public static final String AZURE_KEY_ACCOUNT_SHELLKEYPROVIDER_SCRIPT = "fs.azure.shellkeyprovider.script"; + + /** End point of ABFS account: {@value}. */ + public static final String AZURE_ABFS_ENDPOINT = "fs.azure.abfs.endpoint"; + /** Key for auth type properties: {@value}. */ + public static final String FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME = "fs.azure.account.auth.type"; + /** Key for oauth token provider type: {@value}. */ + public static final String FS_AZURE_ACCOUNT_TOKEN_PROVIDER_TYPE_PROPERTY_NAME = "fs.azure.account.oauth.provider.type"; + /** Key for oauth AAD client id: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID = "fs.azure.account.oauth2.client.id"; + /** Key for oauth AAD client secret: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_CLIENT_SECRET = "fs.azure.account.oauth2.client.secret"; + /** Key for oauth AAD client endpoint: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_CLIENT_ENDPOINT = "fs.azure.account.oauth2.client.endpoint"; + /** Key for oauth msi tenant id: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT = "fs.azure.account.oauth2.msi.tenant"; + /** Key for oauth user name: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_USER_NAME = "fs.azure.account.oauth2.user.name"; + /** Key for oauth user password: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_USER_PASSWORD = "fs.azure.account.oauth2.user.password"; + /** Key for oauth refresh token: {@value}. */ + public static final String FS_AZURE_ACCOUNT_OAUTH_REFRESH_TOKEN = "fs.azure.account.oauth2.refresh.token"; + + public static String accountProperty(String property, String account) { + return property + "." + account; + } + + public static final String FS_AZURE_ENABLE_DELEGATION_TOKEN = "fs.azure.enable.delegation.token"; + public static final String FS_AZURE_DELEGATION_TOKEN_PROVIDER_TYPE = "fs.azure.delegation.token.provider.type"; + + private ConfigurationKeys() {} +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemConfigurations.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemConfigurations.java new file mode 100644 index 00000000000..a9412a961c0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemConfigurations.java @@ -0,0 +1,67 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; + +/** + * Responsible to keep all the Azure Blob File System related configurations. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class FileSystemConfigurations { + public static final String USER_HOME_DIRECTORY_PREFIX = "/user"; + + // Retry parameter defaults. + public static final int DEFAULT_MIN_BACKOFF_INTERVAL = 3 * 1000; // 3s + public static final int DEFAULT_MAX_BACKOFF_INTERVAL = 30 * 1000; // 30s + public static final int DEFAULT_BACKOFF_INTERVAL = 3 * 1000; // 3s + public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 30; + + private static final int ONE_KB = 1024; + private static final int ONE_MB = ONE_KB * ONE_KB; + + // Default upload and download buffer size + public static final int DEFAULT_WRITE_BUFFER_SIZE = 8 * ONE_MB; // 8 MB + public static final int DEFAULT_READ_BUFFER_SIZE = 4 * ONE_MB; // 4 MB + public static final int MIN_BUFFER_SIZE = 16 * ONE_KB; // 16 KB + public static final int MAX_BUFFER_SIZE = 100 * ONE_MB; // 100 MB + public static final long MAX_AZURE_BLOCK_SIZE = 512 * 1024 * 1024L; + public static final String AZURE_BLOCK_LOCATION_HOST_DEFAULT = "localhost"; + + public static final int MAX_CONCURRENT_READ_THREADS = 12; + public static final int MAX_CONCURRENT_WRITE_THREADS = 8; + public static final boolean DEFAULT_READ_TOLERATE_CONCURRENT_APPEND = false; + public static final boolean DEFAULT_AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION = false; + public static final boolean DEFAULT_AZURE_SKIP_USER_GROUP_METADATA_DURING_INITIALIZATION = false; + + public static final String DEFAULT_FS_AZURE_ATOMIC_RENAME_DIRECTORIES = "/hbase"; + + public static final int DEFAULT_READ_AHEAD_QUEUE_DEPTH = -1; + public static final boolean DEFAULT_ENABLE_FLUSH = true; + public static final boolean DEFAULT_ENABLE_AUTOTHROTTLING = true; + + public static final SSLSocketFactoryEx.SSLChannelMode DEFAULT_FS_AZURE_SSL_CHANNEL_MODE + = SSLSocketFactoryEx.SSLChannelMode.Default; + + public static final boolean DEFAULT_ENABLE_DELEGATION_TOKEN = false; + private FileSystemConfigurations() {} +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemUriSchemes.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemUriSchemes.java new file mode 100644 index 00000000000..c7a0cdad605 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/FileSystemUriSchemes.java @@ -0,0 +1,42 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Responsible to keep all Azure Blob File System valid URI schemes. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class FileSystemUriSchemes { + public static final String ABFS_SCHEME = "abfs"; + public static final String ABFS_SECURE_SCHEME = "abfss"; + public static final String ABFS_DNS_PREFIX = "dfs"; + + public static final String HTTP_SCHEME = "http"; + public static final String HTTPS_SCHEME = "https"; + + public static final String WASB_SCHEME = "wasb"; + public static final String WASB_SECURE_SCHEME = "wasbs"; + public static final String WASB_DNS_PREFIX = "blob"; + + private FileSystemUriSchemes() {} +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpHeaderConfigurations.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpHeaderConfigurations.java new file mode 100644 index 00000000000..c8d43904deb --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpHeaderConfigurations.java @@ -0,0 +1,63 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Responsible to keep all abfs http headers here. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class HttpHeaderConfigurations { + public static final String ACCEPT = "Accept"; + public static final String ACCEPT_CHARSET = "Accept-Charset"; + public static final String AUTHORIZATION = "Authorization"; + public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since"; + public static final String IF_MATCH = "If-Match"; + public static final String IF_NONE_MATCH = "If-None-Match"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String CONTENT_ENCODING = "Content-Encoding"; + public static final String CONTENT_LANGUAGE = "Content-Language"; + public static final String CONTENT_MD5 = "Content-MD5"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String RANGE = "Range"; + public static final String TRANSFER_ENCODING = "Transfer-Encoding"; + public static final String USER_AGENT = "User-Agent"; + public static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; + public static final String X_MS_CLIENT_REQUEST_ID = "x-ms-client-request-id"; + public static final String X_MS_DATE = "x-ms-date"; + public static final String X_MS_REQUEST_ID = "x-ms-request-id"; + public static final String X_MS_VERSION = "x-ms-version"; + public static final String X_MS_RESOURCE_TYPE = "x-ms-resource-type"; + public static final String X_MS_CONTINUATION = "x-ms-continuation"; + public static final String ETAG = "ETag"; + public static final String X_MS_PROPERTIES = "x-ms-properties"; + public static final String X_MS_RENAME_SOURCE = "x-ms-rename-source"; + public static final String LAST_MODIFIED = "Last-Modified"; + public static final String X_MS_OWNER = "x-ms-owner"; + public static final String X_MS_GROUP = "x-ms-group"; + public static final String X_MS_ACL = "x-ms-acl"; + public static final String X_MS_PERMISSIONS = "x-ms-permissions"; + public static final String X_MS_UMASK = "x-ms-umask"; + public static final String X_MS_NAMESPACE_ENABLED = "x-ms-namespace-enabled"; + + private HttpHeaderConfigurations() {} +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpQueryParams.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpQueryParams.java new file mode 100644 index 00000000000..f58d33a1302 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/HttpQueryParams.java @@ -0,0 +1,40 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Responsible to keep all Http Query params here. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class HttpQueryParams { + public static final String QUERY_PARAM_RESOURCE = "resource"; + public static final String QUERY_PARAM_DIRECTORY = "directory"; + public static final String QUERY_PARAM_CONTINUATION = "continuation"; + public static final String QUERY_PARAM_RECURSIVE = "recursive"; + public static final String QUERY_PARAM_MAXRESULTS = "maxResults"; + public static final String QUERY_PARAM_ACTION = "action"; + public static final String QUERY_PARAM_POSITION = "position"; + public static final String QUERY_PARAM_TIMEOUT = "timeout"; + public static final String QUERY_PARAM_RETAIN_UNCOMMITTED_DATA = "retainUncommittedData"; + + private HttpQueryParams() {} +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java new file mode 100644 index 00000000000..e6a471bca8d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.constants; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/ConfigurationValidationAnnotations.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/ConfigurationValidationAnnotations.java new file mode 100644 index 00000000000..82c571a3b03 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/ConfigurationValidationAnnotations.java @@ -0,0 +1,104 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Definitions of Annotations for all types of the validators. + */ +@InterfaceStability.Evolving +public class ConfigurationValidationAnnotations { + /** + * Describes the requirements when validating the annotated int field. + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface IntegerConfigurationValidatorAnnotation { + String ConfigurationKey(); + + int MaxValue() default Integer.MAX_VALUE; + + int MinValue() default Integer.MIN_VALUE; + + int DefaultValue(); + + boolean ThrowIfInvalid() default false; + } + + /** + * Describes the requirements when validating the annotated long field. + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface LongConfigurationValidatorAnnotation { + String ConfigurationKey(); + + long MaxValue() default Long.MAX_VALUE; + + long MinValue() default Long.MIN_VALUE; + + long DefaultValue(); + + boolean ThrowIfInvalid() default false; + } + + /** + * Describes the requirements when validating the annotated String field. + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface StringConfigurationValidatorAnnotation { + String ConfigurationKey(); + + String DefaultValue(); + + boolean ThrowIfInvalid() default false; + } + + /** + * Describes the requirements when validating the annotated String field. + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface Base64StringConfigurationValidatorAnnotation { + String ConfigurationKey(); + + String DefaultValue(); + + boolean ThrowIfInvalid() default false; + } + + /** + * Describes the requirements when validating the annotated boolean field. + */ + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + public @interface BooleanConfigurationValidatorAnnotation { + String ConfigurationKey(); + + boolean DefaultValue(); + + boolean ThrowIfInvalid() default false; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/package-info.java new file mode 100644 index 00000000000..0fc4deb3a2b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/annotations/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contracts.annotations; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/ConfigurationValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/ConfigurationValidator.java new file mode 100644 index 00000000000..e0121b612fe --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/ConfigurationValidator.java @@ -0,0 +1,37 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.diagnostics; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * ConfigurationValidator to validate the value of a configuration key + * @param the type of the validator and the validated value. + */ +@InterfaceStability.Evolving +public interface ConfigurationValidator { + /** + * Validates a configuration value. + * @param configValue the configuration value to be validated. + * @return validated value of type T + * @throws InvalidConfigurationValueException if the configuration value is invalid. + */ + T validate(String configValue) throws InvalidConfigurationValueException; +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/package-info.java new file mode 100644 index 00000000000..f8d27b28bf4 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/diagnostics/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contracts.diagnostics; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AbfsRestOperationException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AbfsRestOperationException.java new file mode 100644 index 00000000000..f0b69ef91de --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AbfsRestOperationException.java @@ -0,0 +1,84 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; +import org.apache.hadoop.fs.azurebfs.services.AbfsHttpOperation; + +/** + * Exception to wrap Azure service error responses. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class AbfsRestOperationException extends AzureBlobFileSystemException { + private final int statusCode; + private final AzureServiceErrorCode errorCode; + private final String errorMessage; + + public AbfsRestOperationException( + final int statusCode, + final String errorCode, + final String errorMessage, + final Exception innerException) { + super("Status code: " + statusCode + " error code: " + errorCode + " error message: " + errorMessage, innerException); + + this.statusCode = statusCode; + this.errorCode = AzureServiceErrorCode.getAzureServiceCode(this.statusCode, errorCode); + this.errorMessage = errorMessage; + } + + public AbfsRestOperationException( + final int statusCode, + final String errorCode, + final String errorMessage, + final Exception innerException, + final AbfsHttpOperation abfsHttpOperation) { + super(formatMessage(abfsHttpOperation)); + + this.statusCode = statusCode; + this.errorCode = AzureServiceErrorCode.getAzureServiceCode(this.statusCode, errorCode); + this.errorMessage = errorMessage; + } + + public int getStatusCode() { + return this.statusCode; + } + + public AzureServiceErrorCode getErrorCode() { + return this.errorCode; + } + + public String getErrorMessage() { + return this.errorMessage; + } + + private static String formatMessage(final AbfsHttpOperation abfsHttpOperation) { + return String.format( + "%1$s %2$s%nStatusCode=%3$s%nStatusDescription=%4$s%nErrorCode=%5$s%nErrorMessage=%6$s", + abfsHttpOperation.getMethod(), + abfsHttpOperation.getUrl().toString(), + abfsHttpOperation.getStatusCode(), + abfsHttpOperation.getStatusDescription(), + abfsHttpOperation.getStorageErrorCode(), + abfsHttpOperation.getStorageErrorMessage()); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AzureBlobFileSystemException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AzureBlobFileSystemException.java new file mode 100644 index 00000000000..9b1bead886e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/AzureBlobFileSystemException.java @@ -0,0 +1,56 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import java.io.IOException; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Base exception for any Azure Blob File System driver exceptions. All the exceptions must inherit this class. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public abstract class AzureBlobFileSystemException extends IOException { + public AzureBlobFileSystemException(final String message) { + super(message); + } + + public AzureBlobFileSystemException(final String message, final Exception innerException) { + super(message, innerException); + } + + @Override + public String toString() { + if (this.getMessage() == null && this.getCause() == null) { + return "AzureBlobFileSystemException"; + } + + if (this.getCause() == null) { + return this.getMessage(); + } + + if (this.getMessage() == null) { + return this.getCause().toString(); + } + + return this.getMessage() + this.getCause().toString(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/ConfigurationPropertyNotFoundException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/ConfigurationPropertyNotFoundException.java new file mode 100644 index 00000000000..43a71ab43cb --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/ConfigurationPropertyNotFoundException.java @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when a searched for element is not found + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class ConfigurationPropertyNotFoundException extends AzureBlobFileSystemException { + public ConfigurationPropertyNotFoundException(String property) { + super("Configuration property " + property + " not found."); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/FileSystemOperationUnhandledException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/FileSystemOperationUnhandledException.java new file mode 100644 index 00000000000..484c8385b35 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/FileSystemOperationUnhandledException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when an unhandled exception is occurred during a file system operation. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class FileSystemOperationUnhandledException extends AzureBlobFileSystemException { + public FileSystemOperationUnhandledException(Exception innerException) { + super("An unhandled file operation exception", innerException); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAbfsRestOperationException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAbfsRestOperationException.java new file mode 100644 index 00000000000..aba1d8c1efa --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAbfsRestOperationException.java @@ -0,0 +1,40 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; + +/** + * Exception to wrap invalid Azure service error responses. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class InvalidAbfsRestOperationException extends AbfsRestOperationException { + public InvalidAbfsRestOperationException( + final Exception innerException) { + super( + AzureServiceErrorCode.UNKNOWN.getStatusCode(), + AzureServiceErrorCode.UNKNOWN.getErrorCode(), + "InvalidAbfsRestOperationException", + innerException); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAclOperationException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAclOperationException.java new file mode 100644 index 00000000000..9c186baab9d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidAclOperationException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when there is an attempt to perform an invalid operation on an ACL. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class InvalidAclOperationException extends AzureBlobFileSystemException { + public InvalidAclOperationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidConfigurationValueException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidConfigurationValueException.java new file mode 100644 index 00000000000..7591bac59e2 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidConfigurationValueException.java @@ -0,0 +1,37 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when a configuration value is invalid + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class InvalidConfigurationValueException extends AzureBlobFileSystemException { + public InvalidConfigurationValueException(String configKey, Exception innerException) { + super("Invalid configuration value detected for " + configKey, innerException); + } + + public InvalidConfigurationValueException(String configKey) { + super("Invalid configuration value detected for " + configKey); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidFileSystemPropertyException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidFileSystemPropertyException.java new file mode 100644 index 00000000000..5823fd2c589 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidFileSystemPropertyException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when a file system property is invalid. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class InvalidFileSystemPropertyException extends AzureBlobFileSystemException { + public InvalidFileSystemPropertyException(String property) { + super(String.format("%s is invalid.", property)); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriAuthorityException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriAuthorityException.java new file mode 100644 index 00000000000..7aa319c90c8 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriAuthorityException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when URI authority is invalid. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class InvalidUriAuthorityException extends AzureBlobFileSystemException { + public InvalidUriAuthorityException(String url) { + super(String.format("%s has invalid authority.", url)); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriException.java new file mode 100644 index 00000000000..4fa01509779 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/InvalidUriException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when URI is invalid. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class InvalidUriException extends AzureBlobFileSystemException { + public InvalidUriException(String url) { + super(String.format("Invalid URI %s", url)); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/KeyProviderException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/KeyProviderException.java new file mode 100644 index 00000000000..6723d699f56 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/KeyProviderException.java @@ -0,0 +1,42 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; + +/** + * Thrown if there is a problem instantiating a KeyProvider or retrieving a key + * using a KeyProvider object. + */ +@InterfaceAudience.Private +public class KeyProviderException extends AzureBlobFileSystemException { + private static final long serialVersionUID = 1L; + + public KeyProviderException(String message) { + super(message); + } + + public KeyProviderException(String message, Throwable cause) { + super(message); + } + + public KeyProviderException(Throwable t) { + super(t.getMessage()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TimeoutException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TimeoutException.java new file mode 100644 index 00000000000..8dd5d71d683 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TimeoutException.java @@ -0,0 +1,33 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Thrown when a timeout happens. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public final class TimeoutException extends AzureBlobFileSystemException { + public TimeoutException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TokenAccessProviderException.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TokenAccessProviderException.java new file mode 100644 index 00000000000..b40b34ac13e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/TokenAccessProviderException.java @@ -0,0 +1,36 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.exceptions; + +import org.apache.hadoop.classification.InterfaceAudience; + +/** + * Thrown if there is a problem instantiating a TokenAccessProvider or retrieving a configuration + * using a TokenAccessProvider object. + */ +@InterfaceAudience.Private +public class TokenAccessProviderException extends AzureBlobFileSystemException { + + public TokenAccessProviderException(String message) { + super(message); + } + + public TokenAccessProviderException(String message, Throwable cause) { + super(message); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/package-info.java new file mode 100644 index 00000000000..e4c75f460f9 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/exceptions/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contracts.exceptions; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/package-info.java new file mode 100644 index 00000000000..67f5633c3a7 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contracts; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/AzureServiceErrorCode.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/AzureServiceErrorCode.java new file mode 100644 index 00000000000..60e7f92d270 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/AzureServiceErrorCode.java @@ -0,0 +1,115 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.services; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; + +/** + * Azure service error codes. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public enum AzureServiceErrorCode { + FILE_SYSTEM_ALREADY_EXISTS("FilesystemAlreadyExists", HttpURLConnection.HTTP_CONFLICT, null), + PATH_ALREADY_EXISTS("PathAlreadyExists", HttpURLConnection.HTTP_CONFLICT, null), + INTERNAL_OPERATION_ABORT("InternalOperationAbortError", HttpURLConnection.HTTP_CONFLICT, null), + PATH_CONFLICT("PathConflict", HttpURLConnection.HTTP_CONFLICT, null), + FILE_SYSTEM_NOT_FOUND("FilesystemNotFound", HttpURLConnection.HTTP_NOT_FOUND, null), + PATH_NOT_FOUND("PathNotFound", HttpURLConnection.HTTP_NOT_FOUND, null), + PRE_CONDITION_FAILED("PreconditionFailed", HttpURLConnection.HTTP_PRECON_FAILED, null), + SOURCE_PATH_NOT_FOUND("SourcePathNotFound", HttpURLConnection.HTTP_NOT_FOUND, null), + INVALID_SOURCE_OR_DESTINATION_RESOURCE_TYPE("InvalidSourceOrDestinationResourceType", HttpURLConnection.HTTP_CONFLICT, null), + RENAME_DESTINATION_PARENT_PATH_NOT_FOUND("RenameDestinationParentPathNotFound", HttpURLConnection.HTTP_NOT_FOUND, null), + INVALID_RENAME_SOURCE_PATH("InvalidRenameSourcePath", HttpURLConnection.HTTP_CONFLICT, null), + INGRESS_OVER_ACCOUNT_LIMIT(null, HttpURLConnection.HTTP_UNAVAILABLE, "Ingress is over the account limit."), + EGRESS_OVER_ACCOUNT_LIMIT(null, HttpURLConnection.HTTP_UNAVAILABLE, "Egress is over the account limit."), + INVALID_QUERY_PARAMETER_VALUE("InvalidQueryParameterValue", HttpURLConnection.HTTP_BAD_REQUEST, null), + AUTHORIZATION_PERMISSION_MISS_MATCH("AuthorizationPermissionMismatch", HttpURLConnection.HTTP_FORBIDDEN, null), + UNKNOWN(null, -1, null); + + private final String errorCode; + private final int httpStatusCode; + private final String errorMessage; + AzureServiceErrorCode(String errorCode, int httpStatusCodes, String errorMessage) { + this.errorCode = errorCode; + this.httpStatusCode = httpStatusCodes; + this.errorMessage = errorMessage; + } + + public int getStatusCode() { + return this.httpStatusCode; + } + + public String getErrorCode() { + return this.errorCode; + } + + public static List getAzureServiceCode(int httpStatusCode) { + List errorCodes = new ArrayList<>(); + if (httpStatusCode == UNKNOWN.httpStatusCode) { + errorCodes.add(UNKNOWN); + return errorCodes; + } + + for (AzureServiceErrorCode azureServiceErrorCode : AzureServiceErrorCode.values()) { + if (azureServiceErrorCode.httpStatusCode == httpStatusCode) { + errorCodes.add(azureServiceErrorCode); + } + } + + return errorCodes; + } + + public static AzureServiceErrorCode getAzureServiceCode(int httpStatusCode, String errorCode) { + if (errorCode == null || errorCode.isEmpty() || httpStatusCode == UNKNOWN.httpStatusCode) { + return UNKNOWN; + } + + for (AzureServiceErrorCode azureServiceErrorCode : AzureServiceErrorCode.values()) { + if (errorCode.equalsIgnoreCase(azureServiceErrorCode.errorCode) + && azureServiceErrorCode.httpStatusCode == httpStatusCode) { + return azureServiceErrorCode; + } + } + + return UNKNOWN; + } + + public static AzureServiceErrorCode getAzureServiceCode(int httpStatusCode, String errorCode, final String errorMessage) { + if (errorCode == null || errorCode.isEmpty() || httpStatusCode == UNKNOWN.httpStatusCode || errorMessage == null || errorMessage.isEmpty()) { + return UNKNOWN; + } + + for (AzureServiceErrorCode azureServiceErrorCode : AzureServiceErrorCode.values()) { + if (azureServiceErrorCode.httpStatusCode == httpStatusCode + && errorCode.equalsIgnoreCase(azureServiceErrorCode.errorCode) + && errorMessage.equalsIgnoreCase(azureServiceErrorCode.errorMessage) + ) { + return azureServiceErrorCode; + } + } + + return UNKNOWN; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultEntrySchema.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultEntrySchema.java new file mode 100644 index 00000000000..1de9dfaeeb9 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultEntrySchema.java @@ -0,0 +1,239 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.contracts.services; + +import org.codehaus.jackson.annotate.JsonProperty; + +import org.apache.hadoop.classification.InterfaceStability; + +/** + * The ListResultEntrySchema model. + */ +@InterfaceStability.Evolving +public class ListResultEntrySchema { + /** + * The name property. + */ + @JsonProperty(value = "name") + private String name; + + /** + * The isDirectory property. + */ + @JsonProperty(value = "isDirectory") + private Boolean isDirectory; + + /** + * The lastModified property. + */ + @JsonProperty(value = "lastModified") + private String lastModified; + + /** + * The eTag property. + */ + @JsonProperty(value = "etag") + private String eTag; + + /** + * The contentLength property. + */ + @JsonProperty(value = "contentLength") + private Long contentLength; + + /** + * The owner property. + */ + @JsonProperty(value = "owner") + private String owner; + + /** + * The group property. + */ + @JsonProperty(value = "group") + private String group; + + /** + * The permissions property. + */ + @JsonProperty(value = "permissions") + private String permissions; + + /** + * Get the name value. + * + * @return the name value + */ + public String name() { + return name; + } + + /** + * Set the name value. + * + * @param name the name value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withName(String name) { + this.name = name; + return this; + } + + /** + * Get the isDirectory value. + * + * @return the isDirectory value + */ + public Boolean isDirectory() { + return isDirectory; + } + + /** + * Set the isDirectory value. + * + * @param isDirectory the isDirectory value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withIsDirectory(final Boolean isDirectory) { + this.isDirectory = isDirectory; + return this; + } + + /** + * Get the lastModified value. + * + * @return the lastModified value + */ + public String lastModified() { + return lastModified; + } + + /** + * Set the lastModified value. + * + * @param lastModified the lastModified value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withLastModified(String lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * Get the etag value. + * + * @return the etag value + */ + public String eTag() { + return eTag; + } + + /** + * Set the eTag value. + * + * @param eTag the eTag value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withETag(final String eTag) { + this.eTag = eTag; + return this; + } + + /** + * Get the contentLength value. + * + * @return the contentLength value + */ + public Long contentLength() { + return contentLength; + } + + /** + * Set the contentLength value. + * + * @param contentLength the contentLength value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withContentLength(final Long contentLength) { + this.contentLength = contentLength; + return this; + } + + /** + * + Get the owner value. + * + * @return the owner value + */ + public String owner() { + return owner; + } + + /** + * Set the owner value. + * + * @param owner the owner value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withOwner(final String owner) { + this.owner = owner; + return this; + } + + /** + * Get the group value. + * + * @return the group value + */ + public String group() { + return group; + } + + /** + * Set the group value. + * + * @param group the group value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withGroup(final String group) { + this.group = group; + return this; + } + + /** + * Get the permissions value. + * + * @return the permissions value + */ + public String permissions() { + return permissions; + } + + /** + * Set the permissions value. + * + * @param permissions the permissions value to set + * @return the ListEntrySchema object itself. + */ + public ListResultEntrySchema withPermissions(final String permissions) { + this.permissions = permissions; + return this; + } + +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultSchema.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultSchema.java new file mode 100644 index 00000000000..32597423c86 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ListResultSchema.java @@ -0,0 +1,58 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.contracts.services; + +import java.util.List; + +import org.codehaus.jackson.annotate.JsonProperty; + +import org.apache.hadoop.classification.InterfaceStability; + +/** + * The ListResultSchema model. + */ +@InterfaceStability.Evolving +public class ListResultSchema { + /** + * The paths property. + */ + @JsonProperty(value = "paths") + private List paths; + + /** + * * Get the paths value. + * + * @return the paths value + */ + public List paths() { + return this.paths; + } + + /** + * Set the paths value. + * + * @param paths the paths value to set + * @return the ListSchema object itself. + */ + public ListResultSchema withPaths(final List paths) { + this.paths = paths; + return this; + } + +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ReadBufferStatus.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ReadBufferStatus.java new file mode 100644 index 00000000000..ad750c87a5d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/ReadBufferStatus.java @@ -0,0 +1,29 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contracts.services; + +/** + * The ReadBufferStatus for Rest AbfsClient + */ +public enum ReadBufferStatus { + NOT_AVAILABLE, // buffers sitting in readaheadqueue have this stats + READING_IN_PROGRESS, // reading is in progress on this buffer. Buffer should be in inProgressList + AVAILABLE, // data is available in buffer. It should be in completedList + READ_FAILED // read completed, but failed. +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/package-info.java new file mode 100644 index 00000000000..8b8a597cd24 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/contracts/services/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contracts.services; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/Base64StringConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/Base64StringConfigurationBasicValidator.java new file mode 100644 index 00000000000..fc7d713cb41 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/Base64StringConfigurationBasicValidator.java @@ -0,0 +1,50 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.diagnostics; + + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.diagnostics.ConfigurationValidator; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; +import org.apache.hadoop.fs.azurebfs.utils.Base64; + +/** +* String Base64 configuration value Validator. +*/ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class Base64StringConfigurationBasicValidator extends ConfigurationBasicValidator implements ConfigurationValidator{ + + public Base64StringConfigurationBasicValidator(final String configKey, final String defaultVal, final boolean throwIfInvalid){ + super(configKey, defaultVal, throwIfInvalid); + } + + public String validate(final String configValue) throws InvalidConfigurationValueException { + String result = super.validate((configValue)); + if (result != null) { + return result; + } + + if (!Base64.validateIsBase64String(configValue)) { + throw new InvalidConfigurationValueException(getConfigKey()); + } + return configValue; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/BooleanConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/BooleanConfigurationBasicValidator.java new file mode 100644 index 00000000000..b16abdd09b5 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/BooleanConfigurationBasicValidator.java @@ -0,0 +1,50 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.diagnostics; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * Boolean configuration value validator. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class BooleanConfigurationBasicValidator extends ConfigurationBasicValidator { + private static final String TRUE = "true"; + private static final String FALSE = "false"; + + public BooleanConfigurationBasicValidator(final String configKey, final boolean defaultVal, final boolean throwIfInvalid) { + super(configKey, defaultVal, throwIfInvalid); + } + + public Boolean validate(final String configValue) throws InvalidConfigurationValueException { + Boolean result = super.validate(configValue); + if (result != null) { + return result; + } + + if (configValue.equalsIgnoreCase(TRUE) || configValue.equalsIgnoreCase(FALSE)) { + return Boolean.valueOf(configValue); + } + + throw new InvalidConfigurationValueException(getConfigKey()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/ConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/ConfigurationBasicValidator.java new file mode 100644 index 00000000000..8555a29805a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/ConfigurationBasicValidator.java @@ -0,0 +1,67 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.diagnostics; + +import org.apache.hadoop.fs.azurebfs.contracts.diagnostics.ConfigurationValidator; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * ConfigurationBasicValidator covers the base case of missing user defined configuration value + * @param the type of the validated value + */ +abstract class ConfigurationBasicValidator implements ConfigurationValidator { + private final T defaultVal; + private final String configKey; + private final boolean throwIfInvalid; + + ConfigurationBasicValidator(final String configKey, final T defaultVal, final boolean throwIfInvalid) { + this.configKey = configKey; + this.defaultVal = defaultVal; + this.throwIfInvalid = throwIfInvalid; + } + + /** + * This method handles the base case where the configValue is null, based on the throwIfInvalid it either throws or returns the defaultVal, + * otherwise it returns null indicating that the configValue needs to be validated further. + * @param configValue the configuration value set by the user + * @return the defaultVal in case the configValue is null and not required to be set, null in case the configValue not null + * @throws InvalidConfigurationValueException in case the configValue is null and required to be set + */ + public T validate(final String configValue) throws InvalidConfigurationValueException { + if (configValue == null) { + if (this.throwIfInvalid) { + throw new InvalidConfigurationValueException(this.configKey); + } + return this.defaultVal; + } + return null; + } + + public T getDefaultVal() { + return this.defaultVal; + } + + public String getConfigKey() { + return this.configKey; + } + + public boolean getThrowIfInvalid() { + return this.throwIfInvalid; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/IntegerConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/IntegerConfigurationBasicValidator.java new file mode 100644 index 00000000000..26c7d2f0ac1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/IntegerConfigurationBasicValidator.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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.diagnostics; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.diagnostics.ConfigurationValidator; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * Integer configuration value Validator. + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public class IntegerConfigurationBasicValidator extends ConfigurationBasicValidator implements ConfigurationValidator { + private final int min; + private final int max; + + public IntegerConfigurationBasicValidator(final int min, final int max, final int defaultVal, final String configKey, final boolean throwIfInvalid) { + super(configKey, defaultVal, throwIfInvalid); + this.min = min; + this.max = max; + } + + public Integer validate(final String configValue) throws InvalidConfigurationValueException { + Integer result = super.validate(configValue); + if (result != null) { + return result; + } + + try { + result = Integer.parseInt(configValue); + // throw an exception if a 'within bounds' value is missing + if (getThrowIfInvalid() && (result < this.min || result > this.max)) { + throw new InvalidConfigurationValueException(getConfigKey()); + } + + // set the value to the nearest bound if it's out of bounds + if (result < this.min) { + return this.min; + } + + if (result > this.max) { + return this.max; + } + } catch (NumberFormatException ex) { + throw new InvalidConfigurationValueException(getConfigKey(), ex); + } + + return result; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/LongConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/LongConfigurationBasicValidator.java new file mode 100644 index 00000000000..32ac14cea61 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/LongConfigurationBasicValidator.java @@ -0,0 +1,63 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.diagnostics; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.diagnostics.ConfigurationValidator; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * Long configuration value Validator. + */ +@InterfaceStability.Evolving +public class LongConfigurationBasicValidator extends ConfigurationBasicValidator implements ConfigurationValidator { + private final long min; + private final long max; + + public LongConfigurationBasicValidator(final long min, final long max, final long defaultVal, final String configKey, final boolean throwIfInvalid) { + super(configKey, defaultVal, throwIfInvalid); + this.min = min; + this.max = max; + } + + public Long validate(final String configValue) throws InvalidConfigurationValueException { + Long result = super.validate(configValue); + if (result != null) { + return result; + } + + try { + result = Long.parseLong(configValue); + // throw an exception if a 'within bounds' value is missing + if (getThrowIfInvalid() && (result < this.min || result > this.max)) { + throw new InvalidConfigurationValueException(getConfigKey()); + } + + // set the value to the nearest bound if it's out of bounds + if (result < this.min) { + return this.min; + } else if (result > this.max) { + return this.max; + } + } catch (NumberFormatException ex) { + throw new InvalidConfigurationValueException(getConfigKey(), ex); + } + + return result; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/StringConfigurationBasicValidator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/StringConfigurationBasicValidator.java new file mode 100644 index 00000000000..0d344d13434 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/StringConfigurationBasicValidator.java @@ -0,0 +1,43 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.diagnostics; + +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.contracts.diagnostics.ConfigurationValidator; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +/** + * String configuration value Validator. + */ +@InterfaceStability.Evolving +public class StringConfigurationBasicValidator extends ConfigurationBasicValidator implements ConfigurationValidator{ + + public StringConfigurationBasicValidator(final String configKey, final String defaultVal, final boolean throwIfInvalid){ + super(configKey, defaultVal, throwIfInvalid); + } + + public String validate(final String configValue) throws InvalidConfigurationValueException { + String result = super.validate((configValue)); + if (result != null) { + return result; + } + + return configValue; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java new file mode 100644 index 00000000000..c3434acfc2c --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.diagnostics; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomDelegationTokenManager.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomDelegationTokenManager.java new file mode 100644 index 00000000000..422f8c25110 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomDelegationTokenManager.java @@ -0,0 +1,70 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.extensions; + +import java.io.IOException; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.token.delegation.web.DelegationTokenIdentifier; +import org.apache.hadoop.security.token.Token; + +/** + * Interface for Managing the Delegation tokens. + */ +@InterfaceAudience.LimitedPrivate("authorization-subsystems") +@InterfaceStability.Unstable +public interface CustomDelegationTokenManager { + + /** + * Initialize with supported configuration. This method is invoked when the + * (URI, Configuration)} method is invoked. + * + * @param configuration Configuration object + * @throws IOException if instance can not be configured. + */ + void initialize(Configuration configuration) + throws IOException; + + /** + * Get Delegation token. + * @param renewer delegation token renewer + * @return delegation token + * @throws IOException when error in getting the delegation token + */ + Token getDelegationToken(String renewer) + throws IOException; + + /** + * Renew the delegation token. + * @param token delegation token. + * @return renewed time. + * @throws IOException when error in renewing the delegation token + */ + long renewDelegationToken(Token token) throws IOException; + + /** + * Cancel the delegation token. + * @param token delegation token. + * @throws IOException when error in cancelling the delegation token. + */ + void cancelDelegationToken(Token token) throws IOException; +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomTokenProviderAdaptee.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomTokenProviderAdaptee.java new file mode 100644 index 00000000000..d57eef6548b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/CustomTokenProviderAdaptee.java @@ -0,0 +1,75 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.extensions; + +import java.io.IOException; +import java.util.Date; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configuration; + + +/** + * This interface provides an extensibility model for customizing the acquisition + * of Azure Active Directory Access Tokens. When "fs.azure.account.auth.type" is + * set to "Custom", implementors may use the + * "fs.azure.account.oauth.provider.type.{accountName}" configuration property + * to specify a class with a custom implementation of CustomTokenProviderAdaptee. + * This class will be dynamically loaded, initialized, and invoked to provide + * AAD Access Tokens and their Expiry. + */ +@InterfaceAudience.LimitedPrivate("authorization-subsystems") +@InterfaceStability.Unstable +public interface CustomTokenProviderAdaptee { + + /** + * Initialize with supported configuration. This method is invoked when the + * (URI, Configuration)} method is invoked. + * + * @param configuration Configuration object + * @param accountName Account Name + * @throws IOException if instance can not be configured. + */ + void initialize(Configuration configuration, String accountName) + throws IOException; + + /** + * Obtain the access token that should be added to https connection's header. + * Will be called depending upon {@link #getExpiryTime()} expiry time is set, + * so implementations should be performant. Implementations are responsible + * for any refreshing of the token. + * + * @return String containing the access token + * @throws IOException if there is an error fetching the token + */ + String getAccessToken() throws IOException; + + /** + * Obtain expiry time of the token. If implementation is performant enough to + * maintain expiry and expect {@link #getAccessToken()} call for every + * connection then safe to return current or past time. + * + * However recommended to use the token expiry time received from Azure Active + * Directory. + * + * @return Date to expire access token retrieved from AAD. + */ + Date getExpiryTime(); +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/package-info.java new file mode 100644 index 00000000000..caf4bdae6a6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/extensions/package-info.java @@ -0,0 +1,32 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/** + * This package is for extension points under ABFS; + * There are no stability guarantees as these extension points are + * deep inside the ABFS implementation code. + * + * Note, however: this is how the ABFS client integrates with + * authorization services and other aspects of Azure's infrastructure. + * Do not change these APIs without good reason or detailed discussion. + */ +@InterfaceAudience.LimitedPrivate("authorization-subsystems") +@InterfaceStability.Unstable +package org.apache.hadoop.fs.azurebfs.extensions; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AccessTokenProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AccessTokenProvider.java new file mode 100644 index 00000000000..72f37a1dc1a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AccessTokenProvider.java @@ -0,0 +1,98 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Returns an Azure Active Directory token when requested. The provider can + * cache the token if it has already retrieved one. If it does, then the + * provider is responsible for checking expiry and refreshing as needed. + * + * In other words, this is is a token cache that fetches tokens when + * requested, if the cached token has expired. + * + */ +public abstract class AccessTokenProvider { + + private AzureADToken token; + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + /** + * returns the {@link AzureADToken} cached (or retrieved) by this instance. + * + * @return {@link AzureADToken} containing the access token + * @throws IOException if there is an error fetching the token + */ + public synchronized AzureADToken getToken() throws IOException { + if (isTokenAboutToExpire()) { + LOG.debug("AAD Token is missing or expired:" + + " Calling refresh-token from abstract base class"); + token = refreshToken(); + } + return token; + } + + /** + * the method to fetch the access token. Derived classes should override + * this method to actually get the token from Azure Active Directory. + * + * This method will be called initially, and then once when the token + * is about to expire. + * + * + * @return {@link AzureADToken} containing the access token + * @throws IOException if there is an error fetching the token + */ + protected abstract AzureADToken refreshToken() throws IOException; + + /** + * Checks if the token is about to expire in the next 5 minutes. + * The 5 minute allowance is to allow for clock skew and also to + * allow for token to be refreshed in that much time. + * + * @return true if the token is expiring in next 5 minutes + */ + private boolean isTokenAboutToExpire() { + if (token == null) { + LOG.debug("AADToken: no token. Returning expiring=true"); + return true; // no token should have same response as expired token + } + boolean expiring = false; + // allow 5 minutes for clock skew + long approximatelyNow = System.currentTimeMillis() + FIVE_MINUTES; + if (token.getExpiry().getTime() < approximatelyNow) { + expiring = true; + } + if (expiring) { + LOG.debug("AADToken: token expiring: " + + token.getExpiry().toString() + + " : Five-minute window: " + + new Date(approximatelyNow).toString()); + } + + return expiring; + } + + // 5 minutes in milliseconds + private static final long FIVE_MINUTES = 300 * 1000; +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADAuthenticator.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADAuthenticator.java new file mode 100644 index 00000000000..e82dc954f59 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADAuthenticator.java @@ -0,0 +1,344 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Hashtable; +import java.util.Map; + +import com.google.common.base.Preconditions; +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.services.ExponentialRetryPolicy; + +/** + * This class provides convenience methods to obtain AAD tokens. + * While convenient, it is not necessary to use these methods to + * obtain the tokens. Customers can use any other method + * (e.g., using the adal4j client) to obtain tokens. + */ + +@InterfaceAudience.Private +@InterfaceStability.Evolving +public final class AzureADAuthenticator { + + private static final Logger LOG = LoggerFactory.getLogger(AzureADAuthenticator.class); + private static final String RESOURCE_NAME = "https://storage.azure.com/"; + private static final int CONNECT_TIMEOUT = 30 * 1000; + private static final int READ_TIMEOUT = 30 * 1000; + + private AzureADAuthenticator() { + // no operation + } + + /** + * gets Azure Active Directory token using the user ID and password of + * a service principal (that is, Web App in Azure Active Directory). + * + * Azure Active Directory allows users to set up a web app as a + * service principal. Users can optionally obtain service principal keys + * from AAD. This method gets a token using a service principal's client ID + * and keys. In addition, it needs the token endpoint associated with the + * user's directory. + * + * + * @param authEndpoint the OAuth 2.0 token endpoint associated + * with the user's directory (obtain from + * Active Directory configuration) + * @param clientId the client ID (GUID) of the client web app + * btained from Azure Active Directory configuration + * @param clientSecret the secret key of the client web app + * @return {@link AzureADToken} obtained using the creds + * @throws IOException throws IOException if there is a failure in connecting to Azure AD + */ + public static AzureADToken getTokenUsingClientCreds(String authEndpoint, + String clientId, String clientSecret) + throws IOException { + Preconditions.checkNotNull(authEndpoint, "authEndpoint"); + Preconditions.checkNotNull(clientId, "clientId"); + Preconditions.checkNotNull(clientSecret, "clientSecret"); + + QueryParams qp = new QueryParams(); + qp.add("resource", RESOURCE_NAME); + qp.add("grant_type", "client_credentials"); + qp.add("client_id", clientId); + qp.add("client_secret", clientSecret); + LOG.debug("AADToken: starting to fetch token using client creds for client ID " + clientId); + + return getTokenCall(authEndpoint, qp.serialize(), null, null); + } + + /** + * Gets AAD token from the local virtual machine's VM extension. This only works on + * an Azure VM with MSI extension + * enabled. + * + * @param tenantGuid (optional) The guid of the AAD tenant. Can be {@code null}. + * @param clientId (optional) The clientId guid of the MSI service + * principal to use. Can be {@code null}. + * @param bypassCache {@code boolean} specifying whether a cached token is acceptable or a fresh token + * request should me made to AAD + * @return {@link AzureADToken} obtained using the creds + * @throws IOException throws IOException if there is a failure in obtaining the token + */ + public static AzureADToken getTokenFromMsi(String tenantGuid, String clientId, + boolean bypassCache) throws IOException { + Preconditions.checkNotNull(tenantGuid, "tenantGuid"); + Preconditions.checkNotNull(clientId, "clientId"); + + String authEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"; + + QueryParams qp = new QueryParams(); + qp.add("api-version", "2018-02-01"); + qp.add("resource", RESOURCE_NAME); + + + if (tenantGuid.length() > 0) { + String authority = "https://login.microsoftonline.com/" + tenantGuid; + qp.add("authority", authority); + } + + if (clientId.length() > 0) { + qp.add("client_id", clientId); + } + + if (bypassCache) { + qp.add("bypass_cache", "true"); + } + + Hashtable headers = new Hashtable<>(); + headers.put("Metadata", "true"); + + LOG.debug("AADToken: starting to fetch token using MSI"); + return getTokenCall(authEndpoint, qp.serialize(), headers, "GET"); + } + + /** + * Gets Azure Active Directory token using refresh token. + * + * @param clientId the client ID (GUID) of the client web app obtained from Azure Active Directory configuration + * @param refreshToken the refresh token + * @return {@link AzureADToken} obtained using the refresh token + * @throws IOException throws IOException if there is a failure in connecting to Azure AD + */ + public static AzureADToken getTokenUsingRefreshToken(String clientId, + String refreshToken) throws IOException { + String authEndpoint = "https://login.microsoftonline.com/Common/oauth2/token"; + QueryParams qp = new QueryParams(); + qp.add("grant_type", "refresh_token"); + qp.add("refresh_token", refreshToken); + if (clientId != null) { + qp.add("client_id", clientId); + } + LOG.debug("AADToken: starting to fetch token using refresh token for client ID " + clientId); + return getTokenCall(authEndpoint, qp.serialize(), null, null); + } + + private static class HttpException extends IOException { + private int httpErrorCode; + private String requestId; + + public int getHttpErrorCode() { + return this.httpErrorCode; + } + + public String getRequestId() { + return this.requestId; + } + + HttpException(int httpErrorCode, String requestId, String message) { + super(message); + this.httpErrorCode = httpErrorCode; + this.requestId = requestId; + } + } + + private static AzureADToken getTokenCall(String authEndpoint, String body, + Hashtable headers, String httpMethod) + throws IOException { + AzureADToken token = null; + ExponentialRetryPolicy retryPolicy + = new ExponentialRetryPolicy(3, 0, 1000, 2); + + int httperror = 0; + String requestId; + String httpExceptionMessage = null; + IOException ex = null; + boolean succeeded = false; + int retryCount = 0; + do { + httperror = 0; + requestId = ""; + ex = null; + try { + token = getTokenSingleCall(authEndpoint, body, headers, httpMethod); + } catch (HttpException e) { + httperror = e.httpErrorCode; + requestId = e.requestId; + httpExceptionMessage = e.getMessage(); + } catch (IOException e) { + ex = e; + } + succeeded = ((httperror == 0) && (ex == null)); + retryCount++; + } while (!succeeded && retryPolicy.shouldRetry(retryCount, httperror)); + if (!succeeded) { + if (ex != null) { + throw ex; + } + if (httperror != 0) { + throw new IOException(httpExceptionMessage); + } + } + return token; + } + + private static AzureADToken getTokenSingleCall( + String authEndpoint, String payload, Hashtable headers, String httpMethod) + throws IOException { + + AzureADToken token = null; + HttpURLConnection conn = null; + String urlString = authEndpoint; + + httpMethod = (httpMethod == null) ? "POST" : httpMethod; + if (httpMethod.equals("GET")) { + urlString = urlString + "?" + payload; + } + + try { + URL url = new URL(urlString); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(httpMethod); + conn.setReadTimeout(READ_TIMEOUT); + conn.setConnectTimeout(CONNECT_TIMEOUT); + + if (headers != null && headers.size() > 0) { + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + conn.setRequestProperty("Connection", "close"); + + if (httpMethod.equals("POST")) { + conn.setDoOutput(true); + conn.getOutputStream().write(payload.getBytes("UTF-8")); + } + + int httpResponseCode = conn.getResponseCode(); + String requestId = conn.getHeaderField("x-ms-request-id"); + String responseContentType = conn.getHeaderField("Content-Type"); + long responseContentLength = conn.getHeaderFieldLong("Content-Length", 0); + + requestId = requestId == null ? "" : requestId; + if (httpResponseCode == HttpURLConnection.HTTP_OK + && responseContentType.startsWith("application/json") && responseContentLength > 0) { + InputStream httpResponseStream = conn.getInputStream(); + token = parseTokenFromStream(httpResponseStream); + } else { + String responseBody = consumeInputStream(conn.getInputStream(), 1024); + String proxies = "none"; + String httpProxy = System.getProperty("http.proxy"); + String httpsProxy = System.getProperty("https.proxy"); + if (httpProxy != null || httpsProxy != null) { + proxies = "http:" + httpProxy + "; https:" + httpsProxy; + } + String logMessage = + "AADToken: HTTP connection failed for getting token from AzureAD. Http response: " + + httpResponseCode + " " + conn.getResponseMessage() + + " Content-Type: " + responseContentType + + " Content-Length: " + responseContentLength + + " Request ID: " + requestId.toString() + + " Proxies: " + proxies + + " First 1K of Body: " + responseBody; + LOG.debug(logMessage); + throw new HttpException(httpResponseCode, requestId, logMessage); + } + } finally { + if (conn != null) { + conn.disconnect(); + } + } + return token; + } + + private static AzureADToken parseTokenFromStream(InputStream httpResponseStream) throws IOException { + AzureADToken token = new AzureADToken(); + try { + int expiryPeriod = 0; + + JsonFactory jf = new JsonFactory(); + JsonParser jp = jf.createJsonParser(httpResponseStream); + String fieldName, fieldValue; + jp.nextToken(); + while (jp.hasCurrentToken()) { + if (jp.getCurrentToken() == JsonToken.FIELD_NAME) { + fieldName = jp.getCurrentName(); + jp.nextToken(); // field value + fieldValue = jp.getText(); + + if (fieldName.equals("access_token")) { + token.setAccessToken(fieldValue); + } + if (fieldName.equals("expires_in")) { + expiryPeriod = Integer.parseInt(fieldValue); + } + } + jp.nextToken(); + } + jp.close(); + long expiry = System.currentTimeMillis(); + expiry = expiry + expiryPeriod * 1000L; // convert expiryPeriod to milliseconds and add + token.setExpiry(new Date(expiry)); + LOG.debug("AADToken: fetched token with expiry " + token.getExpiry().toString()); + } catch (Exception ex) { + LOG.debug("AADToken: got exception when parsing json token " + ex.toString()); + throw ex; + } finally { + httpResponseStream.close(); + } + return token; + } + + private static String consumeInputStream(InputStream inStream, int length) throws IOException { + byte[] b = new byte[length]; + int totalBytesRead = 0; + int bytesRead = 0; + + do { + bytesRead = inStream.read(b, totalBytesRead, length - totalBytesRead); + if (bytesRead > 0) { + totalBytesRead += bytesRead; + } + } while (bytesRead >= 0 && totalBytesRead < length); + + return new String(b, 0, totalBytesRead, StandardCharsets.UTF_8); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADToken.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADToken.java new file mode 100644 index 00000000000..daa5a93bf6c --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/AzureADToken.java @@ -0,0 +1,47 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.util.Date; + + +/** + * Object representing the AAD access token to use when making HTTP requests to Azure Data Lake Storage. + */ +public class AzureADToken { + private String accessToken; + private Date expiry; + + public String getAccessToken() { + return this.accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public Date getExpiry() { + return new Date(this.expiry.getTime()); + } + + public void setExpiry(Date expiry) { + this.expiry = new Date(expiry.getTime()); + } + +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/ClientCredsTokenProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/ClientCredsTokenProvider.java new file mode 100644 index 00000000000..9a46018ec62 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/ClientCredsTokenProvider.java @@ -0,0 +1,62 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Provides tokens based on client credentials. + */ +public class ClientCredsTokenProvider extends AccessTokenProvider { + + private final String authEndpoint; + + private final String clientId; + + private final String clientSecret; + + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + + public ClientCredsTokenProvider(final String authEndpoint, + final String clientId, final String clientSecret) { + + Preconditions.checkNotNull(authEndpoint, "authEndpoint"); + Preconditions.checkNotNull(clientId, "clientId"); + Preconditions.checkNotNull(clientSecret, "clientSecret"); + + this.authEndpoint = authEndpoint; + this.clientId = clientId; + this.clientSecret = clientSecret; + } + + + @Override + protected AzureADToken refreshToken() throws IOException { + LOG.debug("AADToken: refreshing client-credential based token"); + return AzureADAuthenticator.getTokenUsingClientCreds(authEndpoint, clientId, clientSecret); + } + + +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/CustomTokenProviderAdapter.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/CustomTokenProviderAdapter.java new file mode 100644 index 00000000000..6e9f6350a1e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/CustomTokenProviderAdapter.java @@ -0,0 +1,58 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azurebfs.extensions.CustomTokenProviderAdaptee; + +/** + * Provides tokens based on custom implementation, following the Adapter Design + * Pattern. + */ +public final class CustomTokenProviderAdapter extends AccessTokenProvider { + + private CustomTokenProviderAdaptee adaptee; + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + /** + * Constructs a token provider based on the custom token provider. + * + * @param adaptee the custom token provider + */ + public CustomTokenProviderAdapter(CustomTokenProviderAdaptee adaptee) { + Preconditions.checkNotNull(adaptee, "adaptee"); + this.adaptee = adaptee; + } + + protected AzureADToken refreshToken() throws IOException { + LOG.debug("AADToken: refreshing custom based token"); + + AzureADToken azureADToken = new AzureADToken(); + azureADToken.setAccessToken(adaptee.getAccessToken()); + azureADToken.setExpiry(adaptee.getExpiryTime()); + + return azureADToken; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/MsiTokenProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/MsiTokenProvider.java new file mode 100644 index 00000000000..2deb9d246d1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/MsiTokenProvider.java @@ -0,0 +1,48 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides tokens based on Azure VM's Managed Service Identity. + */ +public class MsiTokenProvider extends AccessTokenProvider { + + private final String tenantGuid; + + private final String clientId; + + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + public MsiTokenProvider(final String tenantGuid, final String clientId) { + this.tenantGuid = tenantGuid; + this.clientId = clientId; + } + + @Override + protected AzureADToken refreshToken() throws IOException { + LOG.debug("AADToken: refreshing token from MSI"); + AzureADToken token = AzureADAuthenticator.getTokenFromMsi(tenantGuid, clientId, false); + return token; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/QueryParams.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/QueryParams.java new file mode 100644 index 00000000000..ff6e06f9501 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/QueryParams.java @@ -0,0 +1,69 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.oauth2; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities class http query parameters. + */ +public class QueryParams { + private Map params = new HashMap<>(); + private String apiVersion = null; + private String separator = ""; + private String serializedString = null; + + public void add(String name, String value) { + params.put(name, value); + serializedString = null; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + serializedString = null; + } + + public String serialize() { + if (serializedString == null) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + String name = entry.getKey(); + try { + sb.append(separator); + sb.append(URLEncoder.encode(name, "UTF-8")); + sb.append('='); + sb.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + separator = "&"; + } catch (UnsupportedEncodingException ex) { + } + } + + if (apiVersion != null) { + sb.append(separator); + sb.append("api-version="); + sb.append(apiVersion); + separator = "&"; + } + serializedString = sb.toString(); + } + return serializedString; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/RefreshTokenBasedTokenProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/RefreshTokenBasedTokenProvider.java new file mode 100644 index 00000000000..949d5bf1d80 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/RefreshTokenBasedTokenProvider.java @@ -0,0 +1,57 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Provides tokens based on refresh token. + */ +public class RefreshTokenBasedTokenProvider extends AccessTokenProvider { + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + private final String clientId; + + private final String refreshToken; + + /** + * Constructs a token provider based on the refresh token provided. + * + * @param clientId the client ID (GUID) of the client web app obtained from Azure Active Directory configuration + * @param refreshToken the refresh token + */ + public RefreshTokenBasedTokenProvider(String clientId, String refreshToken) { + Preconditions.checkNotNull(clientId, "clientId"); + Preconditions.checkNotNull(refreshToken, "refreshToken"); + this.clientId = clientId; + this.refreshToken = refreshToken; + } + + + @Override + protected AzureADToken refreshToken() throws IOException { + LOG.debug("AADToken: refreshing refresh-token based token"); + return AzureADAuthenticator.getTokenUsingRefreshToken(clientId, refreshToken); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/UserPasswordTokenProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/UserPasswordTokenProvider.java new file mode 100644 index 00000000000..3dad32ec6f5 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/UserPasswordTokenProvider.java @@ -0,0 +1,56 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; + +import java.io.IOException; + +import com.google.common.base.Preconditions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides tokens based on username and password. + */ +public class UserPasswordTokenProvider extends AccessTokenProvider { + + private final String authEndpoint; + + private final String username; + + private final String password; + + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); + + public UserPasswordTokenProvider(final String authEndpoint, + final String username, final String password) { + Preconditions.checkNotNull(authEndpoint, "authEndpoint"); + Preconditions.checkNotNull(username, "username"); + Preconditions.checkNotNull(password, "password"); + + this.authEndpoint = authEndpoint; + this.username = username; + this.password = password; + } + + @Override + protected AzureADToken refreshToken() throws IOException { + LOG.debug("AADToken: refreshing user-password based token"); + return AzureADAuthenticator.getTokenUsingClientCreds(authEndpoint, username, password); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/package-info.java new file mode 100644 index 00000000000..bad1a85b31d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/oauth2/package-info.java @@ -0,0 +1,18 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.oauth2; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/package.html b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/package.html new file mode 100644 index 00000000000..5333cec2d58 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/package.html @@ -0,0 +1,31 @@ + + + + + + +

+A distributed implementation of {@link +org.apache.hadoop.fs.FileSystem} for reading and writing files on +Azure Storage. +This implementation stores files on Azure in their native form for +interoperability with other Azure tools. +

+ + + diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenIdentifier.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenIdentifier.java new file mode 100644 index 00000000000..390c2f40319 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenIdentifier.java @@ -0,0 +1,49 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.security; + +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.delegation.web.DelegationTokenIdentifier; + +/** + * Delegation token Identifier for ABFS delegation tokens. + */ +public class AbfsDelegationTokenIdentifier extends DelegationTokenIdentifier { + public static final Text TOKEN_KIND = new Text("ABFS delegation"); + + public AbfsDelegationTokenIdentifier(){ + super(TOKEN_KIND); + } + + public AbfsDelegationTokenIdentifier(Text kind) { + super(kind); + } + + public AbfsDelegationTokenIdentifier(Text kind, Text owner, Text renewer, + Text realUser) { + super(kind, owner, renewer, realUser); + } + + @Override + public Text getKind() { + return TOKEN_KIND; + } + +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenManager.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenManager.java new file mode 100644 index 00000000000..eb47f768a4b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsDelegationTokenManager.java @@ -0,0 +1,88 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.security; + +import java.io.IOException; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.extensions.CustomDelegationTokenManager; +import org.apache.hadoop.security.token.delegation.web.DelegationTokenIdentifier; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.util.ReflectionUtils; + +/** + * Class for delegation token Manager. + */ +public class AbfsDelegationTokenManager { + + private CustomDelegationTokenManager tokenManager; + private static final Logger LOG = + LoggerFactory.getLogger(AbfsDelegationTokenManager.class); + + public AbfsDelegationTokenManager(final Configuration conf) throws IOException { + + Preconditions.checkNotNull(conf, "conf"); + + Class customDelegationTokenMgrClass = + conf.getClass(ConfigurationKeys.FS_AZURE_DELEGATION_TOKEN_PROVIDER_TYPE, null, + CustomDelegationTokenManager.class); + + if (customDelegationTokenMgrClass == null) { + throw new IllegalArgumentException( + "The value for \"fs.azure.delegation.token.provider.type\" is not defined."); + } + + CustomDelegationTokenManager customTokenMgr = (CustomDelegationTokenManager) ReflectionUtils + .newInstance(customDelegationTokenMgrClass, conf); + if (customTokenMgr == null) { + throw new IllegalArgumentException(String.format("Failed to initialize %s.", customDelegationTokenMgrClass)); + } + + customTokenMgr.initialize(conf); + + tokenManager = customTokenMgr; + } + + public Token getDelegationToken( + String renewer) throws IOException { + + Token token = tokenManager.getDelegationToken(renewer); + + token.setKind(AbfsDelegationTokenIdentifier.TOKEN_KIND); + return token; + } + + public long renewDelegationToken(Token token) + throws IOException { + + return tokenManager.renewDelegationToken(token); + } + + public void cancelDelegationToken(Token token) + throws IOException { + + tokenManager.cancelDelegationToken(token); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsTokenRenewer.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsTokenRenewer.java new file mode 100644 index 00000000000..ab51838f26f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/AbfsTokenRenewer.java @@ -0,0 +1,96 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.security; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenRenewer; + +/** + * Token Renewer for renewing ABFS delegation tokens with remote service. + */ +public class AbfsTokenRenewer extends TokenRenewer { + public static final Logger LOG = + LoggerFactory.getLogger(AbfsTokenRenewer.class); + + /** + * Checks if this particular object handles the Kind of token passed. + * + * @param kind the kind of the token + * @return true if it handles passed token kind false otherwise. + */ + @Override + public boolean handleKind(Text kind) { + return AbfsDelegationTokenIdentifier.TOKEN_KIND.equals(kind); + } + + /** + * Checks if passed token is managed. + * + * @param token the token being checked + * @return true if it is managed. + * @throws IOException thrown when evaluating if token is managed. + */ + @Override + public boolean isManaged(Token token) throws IOException { + return true; + } + + /** + * Renew the delegation token. + * + * @param token token to renew. + * @param conf configuration object. + * @return extended expiry time of the token. + * @throws IOException thrown when trying get current user. + * @throws InterruptedException thrown when thread is interrupted + */ + @Override + public long renew(final Token token, Configuration conf) + throws IOException, InterruptedException { + LOG.debug("Renewing the delegation token"); + return getInstance(conf).renewDelegationToken(token); + } + + /** + * Cancel the delegation token. + * + * @param token token to cancel. + * @param conf configuration object. + * @throws IOException thrown when trying get current user. + * @throws InterruptedException thrown when thread is interrupted. + */ + @Override + public void cancel(final Token token, Configuration conf) + throws IOException, InterruptedException { + LOG.debug("Cancelling the delegation token"); + getInstance(conf).cancelDelegationToken(token); + } + + private AbfsDelegationTokenManager getInstance(Configuration conf) + throws IOException { + return new AbfsDelegationTokenManager(conf); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/package-info.java new file mode 100644 index 00000000000..7c3e37ae6eb --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/security/package-info.java @@ -0,0 +1,23 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +@InterfaceAudience.Private +@InterfaceStability.Unstable +package org.apache.hadoop.fs.azurebfs.security; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsAclHelper.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsAclHelper.java new file mode 100644 index 00000000000..c28da2c4b4f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsAclHelper.java @@ -0,0 +1,202 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidAclOperationException; +import org.apache.hadoop.fs.permission.FsAction; + +/** + * AbfsAclHelper provides convenience methods to implement modifyAclEntries / removeAclEntries / removeAcl / removeDefaultAcl + * from setAcl and getAcl. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public final class AbfsAclHelper { + + private AbfsAclHelper() { + // not called + } + + public static Map deserializeAclSpec(final String aclSpecString) { + final Map aclEntries = new HashMap<>(); + final String[] aclArray = aclSpecString.split(AbfsHttpConstants.COMMA); + for (String acl : aclArray) { + int idx = acl.lastIndexOf(AbfsHttpConstants.COLON); + aclEntries.put(acl.substring(0, idx), acl.substring(idx + 1)); + } + return aclEntries; + } + + public static String serializeAclSpec(final Map aclEntries) { + final StringBuilder sb = new StringBuilder(); + for (Map.Entry aclEntry : aclEntries.entrySet()) { + sb.append(aclEntry.getKey() + AbfsHttpConstants.COLON + aclEntry.getValue() + AbfsHttpConstants.COMMA); + } + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + + public static String processAclString(final String aclSpecString) { + final List aclEntries = Arrays.asList(aclSpecString.split(AbfsHttpConstants.COMMA)); + final StringBuilder sb = new StringBuilder(); + + boolean containsMask = false; + for (int i = aclEntries.size() - 1; i >= 0; i--) { + String ace = aclEntries.get(i); + if (ace.startsWith(AbfsHttpConstants.ACCESS_OTHER)|| ace.startsWith(AbfsHttpConstants.ACCESS_USER + AbfsHttpConstants.COLON)) { + // skip + } else if (ace.startsWith(AbfsHttpConstants.ACCESS_MASK)) { + containsMask = true; + // skip + } else if (ace.startsWith(AbfsHttpConstants.ACCESS_GROUP + AbfsHttpConstants.COLON) && !containsMask) { + // skip + } else { + sb.insert(0, ace + AbfsHttpConstants.COMMA); + } + } + + return sb.length() == 0 ? AbfsHttpConstants.EMPTY_STRING : sb.substring(0, sb.length() - 1); + } + + public static void removeAclEntriesInternal(Map aclEntries, Map toRemoveEntries) + throws AzureBlobFileSystemException { + boolean accessAclTouched = false; + boolean defaultAclTouched = false; + + final Set removeIndicationSet = new HashSet<>(); + + for (String entryKey : toRemoveEntries.keySet()) { + final boolean isDefaultAcl = isDefaultAce(entryKey); + if (removeNamedAceAndUpdateSet(entryKey, isDefaultAcl, removeIndicationSet, aclEntries)) { + if (isDefaultAcl) { + defaultAclTouched = true; + } else { + accessAclTouched = true; + } + } + } + if (accessAclTouched) { + if (removeIndicationSet.contains(AbfsHttpConstants.ACCESS_MASK)) { + aclEntries.remove(AbfsHttpConstants.ACCESS_MASK); + } + recalculateMask(aclEntries, false); + } + if (defaultAclTouched) { + if (removeIndicationSet.contains(AbfsHttpConstants.DEFAULT_MASK)) { + aclEntries.remove(AbfsHttpConstants.DEFAULT_MASK); + } + if (removeIndicationSet.contains(AbfsHttpConstants.DEFAULT_USER)) { + aclEntries.put(AbfsHttpConstants.DEFAULT_USER, aclEntries.get(AbfsHttpConstants.ACCESS_USER)); + } + if (removeIndicationSet.contains(AbfsHttpConstants.DEFAULT_GROUP)) { + aclEntries.put(AbfsHttpConstants.DEFAULT_GROUP, aclEntries.get(AbfsHttpConstants.ACCESS_GROUP)); + } + if (removeIndicationSet.contains(AbfsHttpConstants.DEFAULT_OTHER)) { + aclEntries.put(AbfsHttpConstants.DEFAULT_OTHER, aclEntries.get(AbfsHttpConstants.ACCESS_OTHER)); + } + recalculateMask(aclEntries, true); + } + } + + private static boolean removeNamedAceAndUpdateSet(String entry, boolean isDefaultAcl, Set removeIndicationSet, + Map aclEntries) + throws AzureBlobFileSystemException { + final int startIndex = isDefaultAcl ? 1 : 0; + final String[] entryParts = entry.split(AbfsHttpConstants.COLON); + final String tag = isDefaultAcl ? AbfsHttpConstants.DEFAULT_SCOPE + entryParts[startIndex] + AbfsHttpConstants.COLON + : entryParts[startIndex] + AbfsHttpConstants.COLON; + + if ((entry.equals(AbfsHttpConstants.ACCESS_USER) || entry.equals(AbfsHttpConstants.ACCESS_GROUP) + || entry.equals(AbfsHttpConstants.ACCESS_OTHER)) + && !isNamedAce(entry)) { + throw new InvalidAclOperationException("Cannot remove user, group or other entry from access ACL."); + } + + boolean touched = false; + if (!isNamedAce(entry)) { + removeIndicationSet.add(tag); // this must not be a access user, group or other + touched = true; + } else { + if (aclEntries.remove(entry) != null) { + touched = true; + } + } + return touched; + } + + private static void recalculateMask(Map aclEntries, boolean isDefaultMask) { + FsAction umask = FsAction.NONE; + if (!isExtendAcl(aclEntries, isDefaultMask)) { + return; + } + + for (Map.Entry aclEntry : aclEntries.entrySet()) { + if (isDefaultMask) { + if ((isDefaultAce(aclEntry.getKey()) && isNamedAce(aclEntry.getKey())) + || aclEntry.getKey().equals(AbfsHttpConstants.DEFAULT_GROUP)) { + umask = umask.or(FsAction.getFsAction(aclEntry.getValue())); + } + } else { + if ((!isDefaultAce(aclEntry.getKey()) && isNamedAce(aclEntry.getKey())) + || aclEntry.getKey().equals(AbfsHttpConstants.ACCESS_GROUP)) { + umask = umask.or(FsAction.getFsAction(aclEntry.getValue())); + } + } + } + + aclEntries.put(isDefaultMask ? AbfsHttpConstants.DEFAULT_MASK : AbfsHttpConstants.ACCESS_MASK, umask.SYMBOL); + } + + private static boolean isExtendAcl(Map aclEntries, boolean checkDefault) { + for (String entryKey : aclEntries.keySet()) { + if (checkDefault && !(entryKey.equals(AbfsHttpConstants.DEFAULT_USER) + || entryKey.equals(AbfsHttpConstants.DEFAULT_GROUP) + || entryKey.equals(AbfsHttpConstants.DEFAULT_OTHER) || !isDefaultAce(entryKey))) { + return true; + } + if (!checkDefault && !(entryKey.equals(AbfsHttpConstants.ACCESS_USER) + || entryKey.equals(AbfsHttpConstants.ACCESS_GROUP) + || entryKey.equals(AbfsHttpConstants.ACCESS_OTHER) || isDefaultAce(entryKey))) { + return true; + } + } + return false; + } + + private static boolean isDefaultAce(String entry) { + return entry.startsWith(AbfsHttpConstants.DEFAULT_SCOPE); + } + + private static boolean isNamedAce(String entry) { + return entry.charAt(entry.length() - 1) != AbfsHttpConstants.COLON.charAt(0); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java new file mode 100644 index 00000000000..258045a4738 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClient.java @@ -0,0 +1,581 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; +import org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidUriException; +import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.oauth2.AccessTokenProvider; + +import static org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants.*; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes.HTTPS_SCHEME; +import static org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations.*; +import static org.apache.hadoop.fs.azurebfs.constants.HttpQueryParams.*; + +/** + * AbfsClient. + */ +public class AbfsClient { + public static final Logger LOG = LoggerFactory.getLogger(AbfsClient.class); + private final URL baseUrl; + private final SharedKeyCredentials sharedKeyCredentials; + private final String xMsVersion = "2018-06-17"; + private final ExponentialRetryPolicy retryPolicy; + private final String filesystem; + private final AbfsConfiguration abfsConfiguration; + private final String userAgent; + + private final AccessTokenProvider tokenProvider; + + + public AbfsClient(final URL baseUrl, final SharedKeyCredentials sharedKeyCredentials, + final AbfsConfiguration abfsConfiguration, + final ExponentialRetryPolicy exponentialRetryPolicy, + final AccessTokenProvider tokenProvider) { + this.baseUrl = baseUrl; + this.sharedKeyCredentials = sharedKeyCredentials; + String baseUrlString = baseUrl.toString(); + this.filesystem = baseUrlString.substring(baseUrlString.lastIndexOf(FORWARD_SLASH) + 1); + this.abfsConfiguration = abfsConfiguration; + this.retryPolicy = exponentialRetryPolicy; + + String sslProviderName = null; + + if (this.baseUrl.toString().startsWith(HTTPS_SCHEME)) { + try { + SSLSocketFactoryEx.initializeDefaultFactory(this.abfsConfiguration.getPreferredSSLFactoryOption()); + sslProviderName = SSLSocketFactoryEx.getDefaultFactory().getProviderName(); + } catch (IOException e) { + // Suppress exception. Failure to init SSLSocketFactoryEx would have only performance impact. + } + } + + this.userAgent = initializeUserAgent(abfsConfiguration, sslProviderName); + this.tokenProvider = tokenProvider; + } + + public String getFileSystem() { + return filesystem; + } + + ExponentialRetryPolicy getRetryPolicy() { + return retryPolicy; + } + + SharedKeyCredentials getSharedKeyCredentials() { + return sharedKeyCredentials; + } + + List createDefaultHeaders() { + final List requestHeaders = new ArrayList(); + requestHeaders.add(new AbfsHttpHeader(X_MS_VERSION, xMsVersion)); + requestHeaders.add(new AbfsHttpHeader(ACCEPT, APPLICATION_JSON + + COMMA + SINGLE_WHITE_SPACE + APPLICATION_OCTET_STREAM)); + requestHeaders.add(new AbfsHttpHeader(ACCEPT_CHARSET, + UTF_8)); + requestHeaders.add(new AbfsHttpHeader(CONTENT_TYPE, EMPTY_STRING)); + requestHeaders.add(new AbfsHttpHeader(USER_AGENT, userAgent)); + return requestHeaders; + } + + AbfsUriQueryBuilder createDefaultUriQueryBuilder() { + final AbfsUriQueryBuilder abfsUriQueryBuilder = new AbfsUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_TIMEOUT, DEFAULT_TIMEOUT); + return abfsUriQueryBuilder; + } + + public AbfsRestOperation createFilesystem() throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = new AbfsUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + + final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.CreateFileSystem, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation setFilesystemProperties(final String properties) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + requestHeaders.add(new AbfsHttpHeader(X_MS_PROPERTIES, + properties)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + + final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.SetFileSystemProperties, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation listPath(final String relativePath, final boolean recursive, final int listMaxResults, + final String continuation) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_DIRECTORY, relativePath == null ? AbfsHttpConstants.EMPTY_STRING + : relativePath); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RECURSIVE, String.valueOf(recursive)); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_MAXRESULTS, String.valueOf(listMaxResults)); + + final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.ListPaths, + this, + HTTP_METHOD_GET, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation getFilesystemProperties() throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + + final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.GetFileSystemProperties, + this, + HTTP_METHOD_HEAD, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation deleteFilesystem() throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, FILESYSTEM); + + final URL url = createRequestUrl(abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.DeleteFileSystem, + this, + HTTP_METHOD_DELETE, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation createPath(final String path, final boolean isFile, final boolean overwrite, + final String permission, final String umask) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + if (!overwrite) { + requestHeaders.add(new AbfsHttpHeader(IF_NONE_MATCH, AbfsHttpConstants.STAR)); + } + + if (permission != null && !permission.isEmpty()) { + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS, permission)); + } + + if (umask != null && !umask.isEmpty()) { + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_UMASK, umask)); + } + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RESOURCE, isFile ? FILE : DIRECTORY); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.CreatePath, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation renamePath(final String source, final String destination, final String continuation) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final String encodedRenameSource = urlEncode(FORWARD_SLASH + this.getFileSystem() + source); + requestHeaders.add(new AbfsHttpHeader(X_MS_RENAME_SOURCE, encodedRenameSource)); + requestHeaders.add(new AbfsHttpHeader(IF_NONE_MATCH, STAR)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); + + final URL url = createRequestUrl(destination, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.RenamePath, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation append(final String path, final long position, final byte[] buffer, final int offset, + final int length) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, APPEND_ACTION); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_POSITION, Long.toString(position)); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.Append, + this, + HTTP_METHOD_PUT, + url, + requestHeaders, buffer, offset, length); + op.execute(); + return op; + } + + public AbfsRestOperation flush(final String path, final long position, boolean retainUncommittedData) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, FLUSH_ACTION); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_POSITION, Long.toString(position)); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RETAIN_UNCOMMITTED_DATA, String.valueOf(retainUncommittedData)); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.Flush, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation setPathProperties(final String path, final String properties) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + requestHeaders.add(new AbfsHttpHeader(X_MS_PROPERTIES, properties)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_ACTION, SET_PROPERTIES_ACTION); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.SetPathProperties, + this, + HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation getPathProperties(final String path) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.GetPathProperties, + this, + HTTP_METHOD_HEAD, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation read(final String path, final long position, final byte[] buffer, final int bufferOffset, + final int bufferLength, final String eTag) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + requestHeaders.add(new AbfsHttpHeader(RANGE, + String.format("bytes=%d-%d", position, position + bufferLength - 1))); + requestHeaders.add(new AbfsHttpHeader(IF_MATCH, eTag)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.ReadFile, + this, + HTTP_METHOD_GET, + url, + requestHeaders, + buffer, + bufferOffset, + bufferLength); + op.execute(); + + return op; + } + + public AbfsRestOperation deletePath(final String path, final boolean recursive, final String continuation) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_RECURSIVE, String.valueOf(recursive)); + abfsUriQueryBuilder.addQuery(QUERY_PARAM_CONTINUATION, continuation); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.DeletePath, + this, + HTTP_METHOD_DELETE, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation setOwner(final String path, final String owner, final String group) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + if (owner != null && !owner.isEmpty()) { + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_OWNER, owner)); + } + if (group != null && !group.isEmpty()) { + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_GROUP, group)); + } + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.SetOwner, + this, + AbfsHttpConstants.HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation setPermission(final String path, final String permission) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_PERMISSIONS, permission)); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.SetPermissions, + this, + AbfsHttpConstants.HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation setAcl(final String path, final String aclSpecString) throws AzureBlobFileSystemException { + return setAcl(path, aclSpecString, AbfsHttpConstants.EMPTY_STRING); + } + + public AbfsRestOperation setAcl(final String path, final String aclSpecString, final String eTag) + throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + // JDK7 does not support PATCH, so to workaround the issue we will use + // PUT and specify the real method in the X-Http-Method-Override header. + requestHeaders.add(new AbfsHttpHeader(X_HTTP_METHOD_OVERRIDE, + HTTP_METHOD_PATCH)); + + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.X_MS_ACL, aclSpecString)); + + if (eTag != null && !eTag.isEmpty()) { + requestHeaders.add(new AbfsHttpHeader(HttpHeaderConfigurations.IF_MATCH, eTag)); + } + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.SET_ACCESS_CONTROL); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.SetAcl, + this, + AbfsHttpConstants.HTTP_METHOD_PUT, + url, + requestHeaders); + op.execute(); + return op; + } + + public AbfsRestOperation getAclStatus(final String path) throws AzureBlobFileSystemException { + final List requestHeaders = createDefaultHeaders(); + + final AbfsUriQueryBuilder abfsUriQueryBuilder = createDefaultUriQueryBuilder(); + abfsUriQueryBuilder.addQuery(HttpQueryParams.QUERY_PARAM_ACTION, AbfsHttpConstants.GET_ACCESS_CONTROL); + + final URL url = createRequestUrl(path, abfsUriQueryBuilder.toString()); + final AbfsRestOperation op = new AbfsRestOperation( + AbfsRestOperationType.GetAcl, + this, + AbfsHttpConstants.HTTP_METHOD_HEAD, + url, + requestHeaders); + op.execute(); + return op; + } + + private URL createRequestUrl(final String query) throws AzureBlobFileSystemException { + return createRequestUrl(EMPTY_STRING, query); + } + + private URL createRequestUrl(final String path, final String query) + throws AzureBlobFileSystemException { + final String base = baseUrl.toString(); + String encodedPath = path; + try { + encodedPath = urlEncode(path); + } catch (AzureBlobFileSystemException ex) { + LOG.debug("Unexpected error.", ex); + throw new InvalidUriException(path); + } + + final StringBuilder sb = new StringBuilder(); + sb.append(base); + sb.append(encodedPath); + sb.append(query); + + final URL url; + try { + url = new URL(sb.toString()); + } catch (MalformedURLException ex) { + throw new InvalidUriException(sb.toString()); + } + return url; + } + + public static String urlEncode(final String value) throws AzureBlobFileSystemException { + String encodedString; + try { + encodedString = URLEncoder.encode(value, UTF_8) + .replace(PLUS, PLUS_ENCODE) + .replace(FORWARD_SLASH_ENCODE, FORWARD_SLASH); + } catch (UnsupportedEncodingException ex) { + throw new InvalidUriException(value); + } + + return encodedString; + } + + public synchronized String getAccessToken() throws IOException { + if (tokenProvider != null) { + return "Bearer " + tokenProvider.getToken().getAccessToken(); + } else { + return null; + } + } + + @VisibleForTesting + String initializeUserAgent(final AbfsConfiguration abfsConfiguration, + final String sslProviderName) { + StringBuilder sb = new StringBuilder(); + sb.append("(JavaJRE "); + sb.append(System.getProperty(JAVA_VERSION)); + sb.append("; "); + sb.append( + System.getProperty(OS_NAME).replaceAll(SINGLE_WHITE_SPACE, EMPTY_STRING)); + sb.append(" "); + sb.append(System.getProperty(OS_VERSION)); + if (sslProviderName != null && !sslProviderName.isEmpty()) { + sb.append("; "); + sb.append(sslProviderName); + } + sb.append(")"); + final String userAgentComment = sb.toString(); + String customUserAgentId = abfsConfiguration.getCustomUserAgentPrefix(); + if (customUserAgentId != null && !customUserAgentId.isEmpty()) { + return String.format(Locale.ROOT, CLIENT_VERSION + " %s %s", + userAgentComment, customUserAgentId); + } + return String.format(CLIENT_VERSION + " %s", userAgentComment); + } + + @VisibleForTesting + URL getBaseUrl() { + return baseUrl; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingAnalyzer.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingAnalyzer.java new file mode 100644 index 00000000000..f1e5aaae683 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingAnalyzer.java @@ -0,0 +1,272 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AbfsClientThrottlingAnalyzer { + private static final Logger LOG = LoggerFactory.getLogger( + AbfsClientThrottlingAnalyzer.class); + private static final int DEFAULT_ANALYSIS_PERIOD_MS = 10 * 1000; + private static final int MIN_ANALYSIS_PERIOD_MS = 1000; + private static final int MAX_ANALYSIS_PERIOD_MS = 30000; + private static final double MIN_ACCEPTABLE_ERROR_PERCENTAGE = .1; + private static final double MAX_EQUILIBRIUM_ERROR_PERCENTAGE = 1; + private static final double RAPID_SLEEP_DECREASE_FACTOR = .75; + private static final double RAPID_SLEEP_DECREASE_TRANSITION_PERIOD_MS = 150 + * 1000; + private static final double SLEEP_DECREASE_FACTOR = .975; + private static final double SLEEP_INCREASE_FACTOR = 1.05; + private int analysisPeriodMs; + + private volatile int sleepDuration = 0; + private long consecutiveNoErrorCount = 0; + private String name = null; + private Timer timer = null; + private AtomicReference blobMetrics = null; + + private AbfsClientThrottlingAnalyzer() { + // hide default constructor + } + + /** + * Creates an instance of the AbfsClientThrottlingAnalyzer class with + * the specified name. + * + * @param name a name used to identify this instance. + * @throws IllegalArgumentException if name is null or empty. + */ + AbfsClientThrottlingAnalyzer(String name) throws IllegalArgumentException { + this(name, DEFAULT_ANALYSIS_PERIOD_MS); + } + + /** + * Creates an instance of the AbfsClientThrottlingAnalyzer class with + * the specified name and period. + * + * @param name A name used to identify this instance. + * @param period The frequency, in milliseconds, at which metrics are + * analyzed. + * @throws IllegalArgumentException If name is null or empty. + * If period is less than 1000 or greater than 30000 milliseconds. + */ + AbfsClientThrottlingAnalyzer(String name, int period) + throws IllegalArgumentException { + Preconditions.checkArgument( + StringUtils.isNotEmpty(name), + "The argument 'name' cannot be null or empty."); + Preconditions.checkArgument( + period >= MIN_ANALYSIS_PERIOD_MS && period <= MAX_ANALYSIS_PERIOD_MS, + "The argument 'period' must be between 1000 and 30000."); + this.name = name; + this.analysisPeriodMs = period; + this.blobMetrics = new AtomicReference( + new AbfsOperationMetrics(System.currentTimeMillis())); + this.timer = new Timer( + String.format("abfs-timer-client-throttling-analyzer-%s", name), true); + this.timer.schedule(new TimerTaskImpl(), + analysisPeriodMs, + analysisPeriodMs); + } + + /** + * Updates metrics with results from the current storage operation. + * + * @param count The count of bytes transferred. + * @param isFailedOperation True if the operation failed; otherwise false. + */ + public void addBytesTransferred(long count, boolean isFailedOperation) { + AbfsOperationMetrics metrics = blobMetrics.get(); + if (isFailedOperation) { + metrics.bytesFailed.addAndGet(count); + metrics.operationsFailed.incrementAndGet(); + } else { + metrics.bytesSuccessful.addAndGet(count); + metrics.operationsSuccessful.incrementAndGet(); + } + } + + /** + * Suspends the current storage operation, as necessary, to reduce throughput. + */ + public void suspendIfNecessary() { + int duration = sleepDuration; + if (duration > 0) { + try { + Thread.sleep(duration); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + } + + @VisibleForTesting + int getSleepDuration() { + return sleepDuration; + } + + private int analyzeMetricsAndUpdateSleepDuration(AbfsOperationMetrics metrics, + int sleepDuration) { + final double percentageConversionFactor = 100; + double bytesFailed = metrics.bytesFailed.get(); + double bytesSuccessful = metrics.bytesSuccessful.get(); + double operationsFailed = metrics.operationsFailed.get(); + double operationsSuccessful = metrics.operationsSuccessful.get(); + double errorPercentage = (bytesFailed <= 0) + ? 0 + : (percentageConversionFactor + * bytesFailed + / (bytesFailed + bytesSuccessful)); + long periodMs = metrics.endTime - metrics.startTime; + + double newSleepDuration; + + if (errorPercentage < MIN_ACCEPTABLE_ERROR_PERCENTAGE) { + ++consecutiveNoErrorCount; + // Decrease sleepDuration in order to increase throughput. + double reductionFactor = + (consecutiveNoErrorCount * analysisPeriodMs + >= RAPID_SLEEP_DECREASE_TRANSITION_PERIOD_MS) + ? RAPID_SLEEP_DECREASE_FACTOR + : SLEEP_DECREASE_FACTOR; + + newSleepDuration = sleepDuration * reductionFactor; + } else if (errorPercentage < MAX_EQUILIBRIUM_ERROR_PERCENTAGE) { + // Do not modify sleepDuration in order to stabilize throughput. + newSleepDuration = sleepDuration; + } else { + // Increase sleepDuration in order to minimize error rate. + consecutiveNoErrorCount = 0; + + // Increase sleep duration in order to reduce throughput and error rate. + // First, calculate target throughput: bytesSuccessful / periodMs. + // Next, calculate time required to send *all* data (assuming next period + // is similar to previous) at the target throughput: (bytesSuccessful + // + bytesFailed) * periodMs / bytesSuccessful. Next, subtract periodMs to + // get the total additional delay needed. + double additionalDelayNeeded = 5 * analysisPeriodMs; + if (bytesSuccessful > 0) { + additionalDelayNeeded = (bytesSuccessful + bytesFailed) + * periodMs + / bytesSuccessful + - periodMs; + } + + // amortize the additional delay needed across the estimated number of + // requests during the next period + newSleepDuration = additionalDelayNeeded + / (operationsFailed + operationsSuccessful); + + final double maxSleepDuration = analysisPeriodMs; + final double minSleepDuration = sleepDuration * SLEEP_INCREASE_FACTOR; + + // Add 1 ms to avoid rounding down and to decrease proximity to the server + // side ingress/egress limit. Ensure that the new sleep duration is + // larger than the current one to more quickly reduce the number of + // errors. Don't allow the sleep duration to grow unbounded, after a + // certain point throttling won't help, for example, if there are far too + // many tasks/containers/nodes no amount of throttling will help. + newSleepDuration = Math.max(newSleepDuration, minSleepDuration) + 1; + newSleepDuration = Math.min(newSleepDuration, maxSleepDuration); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "%5.5s, %10d, %10d, %10d, %10d, %6.2f, %5d, %5d, %5d", + name, + (int) bytesFailed, + (int) bytesSuccessful, + (int) operationsFailed, + (int) operationsSuccessful, + errorPercentage, + periodMs, + (int) sleepDuration, + (int) newSleepDuration)); + } + + return (int) newSleepDuration; + } + + /** + * Timer callback implementation for periodically analyzing metrics. + */ + class TimerTaskImpl extends TimerTask { + private AtomicInteger doingWork = new AtomicInteger(0); + + /** + * Periodically analyzes a snapshot of the blob storage metrics and updates + * the sleepDuration in order to appropriately throttle storage operations. + */ + @Override + public void run() { + boolean doWork = false; + try { + doWork = doingWork.compareAndSet(0, 1); + + // prevent concurrent execution of this task + if (!doWork) { + return; + } + + long now = System.currentTimeMillis(); + if (now - blobMetrics.get().startTime >= analysisPeriodMs) { + AbfsOperationMetrics oldMetrics = blobMetrics.getAndSet( + new AbfsOperationMetrics(now)); + oldMetrics.endTime = now; + sleepDuration = analyzeMetricsAndUpdateSleepDuration(oldMetrics, + sleepDuration); + } + } finally { + if (doWork) { + doingWork.set(0); + } + } + } + } + + /** + * Stores Abfs operation metrics during each analysis period. + */ + static class AbfsOperationMetrics { + private AtomicLong bytesFailed; + private AtomicLong bytesSuccessful; + private AtomicLong operationsFailed; + private AtomicLong operationsSuccessful; + private long endTime; + private long startTime; + + AbfsOperationMetrics(long startTime) { + this.startTime = startTime; + this.bytesFailed = new AtomicLong(); + this.bytesSuccessful = new AtomicLong(); + this.operationsFailed = new AtomicLong(); + this.operationsSuccessful = new AtomicLong(); + } + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingIntercept.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingIntercept.java new file mode 100644 index 00000000000..1c6ce17a38c --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsClientThrottlingIntercept.java @@ -0,0 +1,135 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.net.HttpURLConnection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; + +/** + * Throttles Azure Blob File System read and write operations to achieve maximum + * throughput by minimizing errors. The errors occur when the account ingress + * or egress limits are exceeded and the server-side throttles requests. + * Server-side throttling causes the retry policy to be used, but the retry + * policy sleeps for long periods of time causing the total ingress or egress + * throughput to be as much as 35% lower than optimal. The retry policy is also + * after the fact, in that it applies after a request fails. On the other hand, + * the client-side throttling implemented here happens before requests are made + * and sleeps just enough to minimize errors, allowing optimal ingress and/or + * egress throughput. + */ +public final class AbfsClientThrottlingIntercept { + private static final Logger LOG = LoggerFactory.getLogger( + AbfsClientThrottlingIntercept.class); + private static final String RANGE_PREFIX = "bytes="; + private static AbfsClientThrottlingIntercept singleton = null; + private AbfsClientThrottlingAnalyzer readThrottler = null; + private AbfsClientThrottlingAnalyzer writeThrottler = null; + private static boolean isAutoThrottlingEnabled = false; + + // Hide default constructor + private AbfsClientThrottlingIntercept() { + readThrottler = new AbfsClientThrottlingAnalyzer("read"); + writeThrottler = new AbfsClientThrottlingAnalyzer("write"); + } + + public static synchronized void initializeSingleton(boolean enableAutoThrottling) { + if (!enableAutoThrottling) { + return; + } + if (singleton == null) { + singleton = new AbfsClientThrottlingIntercept(); + isAutoThrottlingEnabled = true; + LOG.debug("Client-side throttling is enabled for the ABFS file system."); + } + } + + static void updateMetrics(AbfsRestOperationType operationType, + AbfsHttpOperation abfsHttpOperation) { + if (!isAutoThrottlingEnabled || abfsHttpOperation == null) { + return; + } + + int status = abfsHttpOperation.getStatusCode(); + long contentLength = 0; + // If the socket is terminated prior to receiving a response, the HTTP + // status may be 0 or -1. A status less than 200 or greater than or equal + // to 500 is considered an error. + boolean isFailedOperation = (status < HttpURLConnection.HTTP_OK + || status >= HttpURLConnection.HTTP_INTERNAL_ERROR); + + switch (operationType) { + case Append: + contentLength = abfsHttpOperation.getBytesSent(); + if (contentLength > 0) { + singleton.writeThrottler.addBytesTransferred(contentLength, + isFailedOperation); + } + break; + case ReadFile: + String range = abfsHttpOperation.getConnection().getRequestProperty(HttpHeaderConfigurations.RANGE); + contentLength = getContentLengthIfKnown(range); + if (contentLength > 0) { + singleton.readThrottler.addBytesTransferred(contentLength, + isFailedOperation); + } + break; + default: + break; + } + } + + /** + * Called before the request is sent. Client-side throttling + * uses this to suspend the request, if necessary, to minimize errors and + * maximize throughput. + */ + static void sendingRequest(AbfsRestOperationType operationType) { + if (!isAutoThrottlingEnabled) { + return; + } + + switch (operationType) { + case ReadFile: + singleton.readThrottler.suspendIfNecessary(); + break; + case Append: + singleton.writeThrottler.suspendIfNecessary(); + break; + default: + break; + } + } + + private static long getContentLengthIfKnown(String range) { + long contentLength = 0; + // Format is "bytes=%d-%d" + if (range != null && range.startsWith(RANGE_PREFIX)) { + String[] offsets = range.substring(RANGE_PREFIX.length()).split("-"); + if (offsets.length == 2) { + contentLength = Long.parseLong(offsets[1]) - Long.parseLong(offsets[0]) + + 1; + } + } + return contentLength; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpHeader.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpHeader.java new file mode 100644 index 00000000000..0067b755460 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpHeader.java @@ -0,0 +1,40 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +/** + * The Http Request / Response Headers for Rest AbfsClient. + */ +public class AbfsHttpHeader { + private final String name; + private final String value; + + public AbfsHttpHeader(final String name, final String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpOperation.java new file mode 100644 index 00000000000..de38b347248 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsHttpOperation.java @@ -0,0 +1,446 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.UUID; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.ObjectMapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; +import org.apache.hadoop.fs.azurebfs.contracts.services.ListResultSchema; + +/** + * Represents an HTTP operation. + */ +public class AbfsHttpOperation { + private static final Logger LOG = LoggerFactory.getLogger(AbfsHttpOperation.class); + + private static final int CONNECT_TIMEOUT = 30 * 1000; + private static final int READ_TIMEOUT = 30 * 1000; + + private static final int CLEAN_UP_BUFFER_SIZE = 64 * 1024; + + private static final int ONE_THOUSAND = 1000; + private static final int ONE_MILLION = ONE_THOUSAND * ONE_THOUSAND; + + private final String method; + private final URL url; + + private HttpURLConnection connection; + private int statusCode; + private String statusDescription; + private String storageErrorCode = ""; + private String storageErrorMessage = ""; + private String clientRequestId = ""; + private String requestId = ""; + private ListResultSchema listResultSchema = null; + + // metrics + private int bytesSent; + private long bytesReceived; + + // optional trace enabled metrics + private final boolean isTraceEnabled; + private long connectionTimeMs; + private long sendRequestTimeMs; + private long recvResponseTimeMs; + + protected HttpURLConnection getConnection() { + return connection; + } + + public String getMethod() { + return method; + } + + public URL getUrl() { + return url; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatusDescription() { + return statusDescription; + } + + public String getStorageErrorCode() { + return storageErrorCode; + } + + public String getStorageErrorMessage() { + return storageErrorMessage; + } + + public String getClientRequestId() { + return clientRequestId; + } + + public String getRequestId() { + return requestId; + } + + public int getBytesSent() { + return bytesSent; + } + + public long getBytesReceived() { + return bytesReceived; + } + + public ListResultSchema getListResultSchema() { + return listResultSchema; + } + + public String getResponseHeader(String httpHeader) { + return connection.getHeaderField(httpHeader); + } + + // Returns a trace message for the request + @Override + public String toString() { + final String urlStr = url.toString(); + final StringBuilder sb = new StringBuilder(); + sb.append(statusCode); + sb.append(","); + sb.append(storageErrorCode); + sb.append(",cid="); + sb.append(clientRequestId); + sb.append(",rid="); + sb.append(requestId); + if (isTraceEnabled) { + sb.append(",connMs="); + sb.append(connectionTimeMs); + sb.append(",sendMs="); + sb.append(sendRequestTimeMs); + sb.append(",recvMs="); + sb.append(recvResponseTimeMs); + } + sb.append(",sent="); + sb.append(bytesSent); + sb.append(",recv="); + sb.append(bytesReceived); + sb.append(","); + sb.append(method); + sb.append(","); + sb.append(urlStr); + return sb.toString(); + } + + /** + * Initializes a new HTTP request and opens the connection. + * + * @param url The full URL including query string parameters. + * @param method The HTTP method (PUT, PATCH, POST, GET, HEAD, or DELETE). + * @param requestHeaders The HTTP request headers.READ_TIMEOUT + * + * @throws IOException if an error occurs. + */ + public AbfsHttpOperation(final URL url, final String method, final List requestHeaders) + throws IOException { + this.isTraceEnabled = LOG.isTraceEnabled(); + this.url = url; + this.method = method; + this.clientRequestId = UUID.randomUUID().toString(); + + this.connection = openConnection(); + if (this.connection instanceof HttpsURLConnection) { + HttpsURLConnection secureConn = (HttpsURLConnection) this.connection; + SSLSocketFactory sslSocketFactory = SSLSocketFactoryEx.getDefaultFactory(); + if (sslSocketFactory != null) { + secureConn.setSSLSocketFactory(sslSocketFactory); + } + } + + this.connection.setConnectTimeout(CONNECT_TIMEOUT); + this.connection.setReadTimeout(READ_TIMEOUT); + + this.connection.setRequestMethod(method); + + for (AbfsHttpHeader header : requestHeaders) { + this.connection.setRequestProperty(header.getName(), header.getValue()); + } + + this.connection.setRequestProperty(HttpHeaderConfigurations.X_MS_CLIENT_REQUEST_ID, clientRequestId); + } + + /** + * Sends the HTTP request. Note that HttpUrlConnection requires that an + * empty buffer be sent in order to set the "Content-Length: 0" header, which + * is required by our endpoint. + * + * @param buffer the request entity body. + * @param offset an offset into the buffer where the data beings. + * @param length the length of the data in the buffer. + * + * @throws IOException if an error occurs. + */ + public void sendRequest(byte[] buffer, int offset, int length) throws IOException { + this.connection.setDoOutput(true); + this.connection.setFixedLengthStreamingMode(length); + if (buffer == null) { + // An empty buffer is sent to set the "Content-Length: 0" header, which + // is required by our endpoint. + buffer = new byte[]{}; + offset = 0; + length = 0; + } + + // send the request body + + long startTime = 0; + if (this.isTraceEnabled) { + startTime = System.nanoTime(); + } + try (OutputStream outputStream = this.connection.getOutputStream()) { + // update bytes sent before they are sent so we may observe + // attempted sends as well as successful sends via the + // accompanying statusCode + this.bytesSent = length; + outputStream.write(buffer, offset, length); + } finally { + if (this.isTraceEnabled) { + this.sendRequestTimeMs = elapsedTimeMs(startTime); + } + } + } + + /** + * Gets and processes the HTTP response. + * + * @param buffer a buffer to hold the response entity body + * @param offset an offset in the buffer where the data will being. + * @param length the number of bytes to be written to the buffer. + * + * @throws IOException if an error occurs. + */ + public void processResponse(final byte[] buffer, final int offset, final int length) throws IOException { + + // get the response + long startTime = 0; + if (this.isTraceEnabled) { + startTime = System.nanoTime(); + } + + this.statusCode = this.connection.getResponseCode(); + + if (this.isTraceEnabled) { + this.recvResponseTimeMs = elapsedTimeMs(startTime); + } + + this.statusDescription = this.connection.getResponseMessage(); + + this.requestId = this.connection.getHeaderField(HttpHeaderConfigurations.X_MS_REQUEST_ID); + if (this.requestId == null) { + this.requestId = AbfsHttpConstants.EMPTY_STRING; + } + + if (AbfsHttpConstants.HTTP_METHOD_HEAD.equals(this.method)) { + // If it is HEAD, and it is ERROR + return; + } + + if (this.isTraceEnabled) { + startTime = System.nanoTime(); + } + + if (statusCode >= HttpURLConnection.HTTP_BAD_REQUEST) { + processStorageErrorResponse(); + if (this.isTraceEnabled) { + this.recvResponseTimeMs += elapsedTimeMs(startTime); + } + this.bytesReceived = this.connection.getHeaderFieldLong(HttpHeaderConfigurations.CONTENT_LENGTH, 0); + } else { + // consume the input stream to release resources + int totalBytesRead = 0; + + try (InputStream stream = this.connection.getInputStream()) { + if (isNullInputStream(stream)) { + return; + } + boolean endOfStream = false; + + // this is a list operation and need to retrieve the data + // need a better solution + if (AbfsHttpConstants.HTTP_METHOD_GET.equals(this.method) && buffer == null) { + parseListFilesResponse(stream); + } else { + if (buffer != null) { + while (totalBytesRead < length) { + int bytesRead = stream.read(buffer, offset + totalBytesRead, length - totalBytesRead); + if (bytesRead == -1) { + endOfStream = true; + break; + } + totalBytesRead += bytesRead; + } + } + if (!endOfStream && stream.read() != -1) { + // read and discard + int bytesRead = 0; + byte[] b = new byte[CLEAN_UP_BUFFER_SIZE]; + while ((bytesRead = stream.read(b)) >= 0) { + totalBytesRead += bytesRead; + } + } + } + } catch (IOException ex) { + LOG.error("UnexpectedError: ", ex); + throw ex; + } finally { + if (this.isTraceEnabled) { + this.recvResponseTimeMs += elapsedTimeMs(startTime); + } + this.bytesReceived = totalBytesRead; + } + } + } + + + /** + * Open the HTTP connection. + * + * @throws IOException if an error occurs. + */ + private HttpURLConnection openConnection() throws IOException { + if (!isTraceEnabled) { + return (HttpURLConnection) url.openConnection(); + } + long start = System.nanoTime(); + try { + return (HttpURLConnection) url.openConnection(); + } finally { + connectionTimeMs = elapsedTimeMs(start); + } + } + + /** + * When the request fails, this function is used to parse the responseAbfsHttpClient.LOG.debug("ExpectedError: ", ex); + * and extract the storageErrorCode and storageErrorMessage. Any errors + * encountered while attempting to process the error response are logged, + * but otherwise ignored. + * + * For storage errors, the response body *usually* has the following format: + * + * { + * "error": + * { + * "code": "string", + * "message": "string" + * } + * } + * + */ + private void processStorageErrorResponse() { + try (InputStream stream = connection.getErrorStream()) { + if (stream == null) { + return; + } + JsonFactory jf = new JsonFactory(); + try (JsonParser jp = jf.createJsonParser(stream)) { + String fieldName, fieldValue; + jp.nextToken(); // START_OBJECT - { + jp.nextToken(); // FIELD_NAME - "error": + jp.nextToken(); // START_OBJECT - { + jp.nextToken(); + while (jp.hasCurrentToken()) { + if (jp.getCurrentToken() == JsonToken.FIELD_NAME) { + fieldName = jp.getCurrentName(); + jp.nextToken(); + fieldValue = jp.getText(); + switch (fieldName) { + case "code": + storageErrorCode = fieldValue; + break; + case "message": + storageErrorMessage = fieldValue; + break; + default: + break; + } + } + jp.nextToken(); + } + } + } catch (IOException ex) { + // Ignore errors that occur while attempting to parse the storage + // error, since the response may have been handled by the HTTP driver + // or for other reasons have an unexpected + LOG.debug("ExpectedError: ", ex); + } + } + + /** + * Returns the elapsed time in milliseconds. + */ + private long elapsedTimeMs(final long startTime) { + return (System.nanoTime() - startTime) / ONE_MILLION; + } + + /** + * Parse the list file response + * + * @param stream InputStream contains the list results. + * @throws IOException + */ + private void parseListFilesResponse(final InputStream stream) throws IOException { + if (stream == null) { + return; + } + + if (listResultSchema != null) { + // already parse the response + return; + } + + try { + final ObjectMapper objectMapper = new ObjectMapper(); + this.listResultSchema = objectMapper.readValue(stream, ListResultSchema.class); + } catch (IOException ex) { + LOG.error("Unable to deserialize list results", ex); + throw ex; + } + } + + /** + * Check null stream, this is to pass findbugs's redundant check for NULL + * @param stream InputStream + */ + private boolean isNullInputStream(InputStream stream) { + return stream == null ? true : false; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java new file mode 100644 index 00000000000..960579dfaa3 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsInputStream.java @@ -0,0 +1,381 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.EOFException; +import java.io.IOException; + +import com.google.common.base.Preconditions; + +import org.apache.hadoop.fs.FSExceptionMessages; +import org.apache.hadoop.fs.FSInputStream; +import org.apache.hadoop.fs.FileSystem.Statistics; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; + +/** + * The AbfsInputStream for AbfsClient. + */ +public class AbfsInputStream extends FSInputStream { + private final AbfsClient client; + private final Statistics statistics; + private final String path; + private final long contentLength; + private final int bufferSize; // default buffer size + private final int readAheadQueueDepth; // initialized in constructor + private final String eTag; // eTag of the path when InputStream are created + private final boolean tolerateOobAppends; // whether tolerate Oob Appends + private final boolean readAheadEnabled; // whether enable readAhead; + + private byte[] buffer = null; // will be initialized on first use + + private long fCursor = 0; // cursor of buffer within file - offset of next byte to read from remote server + private long fCursorAfterLastRead = -1; + private int bCursor = 0; // cursor of read within buffer - offset of next byte to be returned from buffer + private int limit = 0; // offset of next byte to be read into buffer from service (i.e., upper marker+1 + // of valid bytes in buffer) + private boolean closed = false; + + public AbfsInputStream( + final AbfsClient client, + final Statistics statistics, + final String path, + final long contentLength, + final int bufferSize, + final int readAheadQueueDepth, + final String eTag) { + this.client = client; + this.statistics = statistics; + this.path = path; + this.contentLength = contentLength; + this.bufferSize = bufferSize; + this.readAheadQueueDepth = (readAheadQueueDepth >= 0) ? readAheadQueueDepth : Runtime.getRuntime().availableProcessors(); + this.eTag = eTag; + this.tolerateOobAppends = false; + this.readAheadEnabled = true; + } + + public String getPath() { + return path; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int numberOfBytesRead = read(b, 0, 1); + if (numberOfBytesRead < 0) { + return -1; + } else { + return (b[0] & 0xFF); + } + } + + @Override + public synchronized int read(final byte[] b, final int off, final int len) throws IOException { + int currentOff = off; + int currentLen = len; + int lastReadBytes; + int totalReadBytes = 0; + do { + lastReadBytes = readOneBlock(b, currentOff, currentLen); + if (lastReadBytes > 0) { + currentOff += lastReadBytes; + currentLen -= lastReadBytes; + totalReadBytes += lastReadBytes; + } + if (currentLen <= 0 || currentLen > b.length - currentOff) { + break; + } + } while (lastReadBytes > 0); + return totalReadBytes > 0 ? totalReadBytes : lastReadBytes; + } + + private int readOneBlock(final byte[] b, final int off, final int len) throws IOException { + if (closed) { + throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + } + + Preconditions.checkNotNull(b); + + if (len == 0) { + return 0; + } + + if (this.available() == 0) { + return -1; + } + + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + + //If buffer is empty, then fill the buffer. + if (bCursor == limit) { + //If EOF, then return -1 + if (fCursor >= contentLength) { + return -1; + } + + long bytesRead = 0; + //reset buffer to initial state - i.e., throw away existing data + bCursor = 0; + limit = 0; + if (buffer == null) { + buffer = new byte[bufferSize]; + } + + // Enable readAhead when reading sequentially + if (-1 == fCursorAfterLastRead || fCursorAfterLastRead == fCursor || b.length >= bufferSize) { + bytesRead = readInternal(fCursor, buffer, 0, bufferSize, false); + } else { + bytesRead = readInternal(fCursor, buffer, 0, b.length, true); + } + + if (bytesRead == -1) { + return -1; + } + + limit += bytesRead; + fCursor += bytesRead; + fCursorAfterLastRead = fCursor; + } + + //If there is anything in the buffer, then return lesser of (requested bytes) and (bytes in buffer) + //(bytes returned may be less than requested) + int bytesRemaining = limit - bCursor; + int bytesToRead = Math.min(len, bytesRemaining); + System.arraycopy(buffer, bCursor, b, off, bytesToRead); + bCursor += bytesToRead; + if (statistics != null) { + statistics.incrementBytesRead(bytesToRead); + } + return bytesToRead; + } + + + private int readInternal(final long position, final byte[] b, final int offset, final int length, + final boolean bypassReadAhead) throws IOException { + if (readAheadEnabled && !bypassReadAhead) { + // try reading from read-ahead + if (offset != 0) { + throw new IllegalArgumentException("readahead buffers cannot have non-zero buffer offsets"); + } + int receivedBytes; + + // queue read-aheads + int numReadAheads = this.readAheadQueueDepth; + long nextSize; + long nextOffset = position; + while (numReadAheads > 0 && nextOffset < contentLength) { + nextSize = Math.min((long) bufferSize, contentLength - nextOffset); + ReadBufferManager.getBufferManager().queueReadAhead(this, nextOffset, (int) nextSize); + nextOffset = nextOffset + nextSize; + numReadAheads--; + } + + // try reading from buffers first + receivedBytes = ReadBufferManager.getBufferManager().getBlock(this, position, length, b); + if (receivedBytes > 0) { + return receivedBytes; + } + + // got nothing from read-ahead, do our own read now + receivedBytes = readRemote(position, b, offset, length); + return receivedBytes; + } else { + return readRemote(position, b, offset, length); + } + } + + int readRemote(long position, byte[] b, int offset, int length) throws IOException { + if (position < 0) { + throw new IllegalArgumentException("attempting to read from negative offset"); + } + if (position >= contentLength) { + return -1; // Hadoop prefers -1 to EOFException + } + if (b == null) { + throw new IllegalArgumentException("null byte array passed in to read() method"); + } + if (offset >= b.length) { + throw new IllegalArgumentException("offset greater than length of array"); + } + if (length < 0) { + throw new IllegalArgumentException("requested read length is less than zero"); + } + if (length > (b.length - offset)) { + throw new IllegalArgumentException("requested read length is more than will fit after requested offset in buffer"); + } + final AbfsRestOperation op; + try { + op = client.read(path, position, b, offset, length, tolerateOobAppends ? "*" : eTag); + } catch (AzureBlobFileSystemException ex) { + throw new IOException(ex); + } + long bytesRead = op.getResult().getBytesReceived(); + if (bytesRead > Integer.MAX_VALUE) { + throw new IOException("Unexpected Content-Length"); + } + return (int) bytesRead; + } + + /** + * Seek to given position in stream. + * @param n position to seek to + * @throws IOException if there is an error + * @throws EOFException if attempting to seek past end of file + */ + @Override + public synchronized void seek(long n) throws IOException { + if (closed) { + throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + } + if (n < 0) { + throw new EOFException(FSExceptionMessages.NEGATIVE_SEEK); + } + if (n > contentLength) { + throw new EOFException(FSExceptionMessages.CANNOT_SEEK_PAST_EOF); + } + + if (n>=fCursor-limit && n<=fCursor) { // within buffer + bCursor = (int) (n-(fCursor-limit)); + return; + } + + // next read will read from here + fCursor = n; + + //invalidate buffer + limit = 0; + bCursor = 0; + } + + @Override + public synchronized long skip(long n) throws IOException { + if (closed) { + throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + } + long currentPos = getPos(); + if (currentPos == contentLength) { + if (n > 0) { + throw new EOFException(FSExceptionMessages.CANNOT_SEEK_PAST_EOF); + } + } + long newPos = currentPos + n; + if (newPos < 0) { + newPos = 0; + n = newPos - currentPos; + } + if (newPos > contentLength) { + newPos = contentLength; + n = newPos - currentPos; + } + seek(newPos); + return n; + } + + /** + * Return the size of the remaining available bytes + * if the size is less than or equal to {@link Integer#MAX_VALUE}, + * otherwise, return {@link Integer#MAX_VALUE}. + * + * This is to match the behavior of DFSInputStream.available(), + * which some clients may rely on (HBase write-ahead log reading in + * particular). + */ + @Override + public synchronized int available() throws IOException { + if (closed) { + throw new IOException( + FSExceptionMessages.STREAM_IS_CLOSED); + } + final long remaining = this.contentLength - this.getPos(); + return remaining <= Integer.MAX_VALUE + ? (int) remaining : Integer.MAX_VALUE; + } + + /** + * Returns the length of the file that this stream refers to. Note that the length returned is the length + * as of the time the Stream was opened. Specifically, if there have been subsequent appends to the file, + * they wont be reflected in the returned length. + * + * @return length of the file. + * @throws IOException if the stream is closed + */ + public long length() throws IOException { + if (closed) { + throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + } + return contentLength; + } + + /** + * Return the current offset from the start of the file + * @throws IOException throws {@link IOException} if there is an error + */ + @Override + public synchronized long getPos() throws IOException { + if (closed) { + throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + } + return fCursor - limit + bCursor; + } + + /** + * Seeks a different copy of the data. Returns true if + * found a new source, false otherwise. + * @throws IOException throws {@link IOException} if there is an error + */ + @Override + public boolean seekToNewSource(long l) throws IOException { + return false; + } + + @Override + public synchronized void close() throws IOException { + closed = true; + buffer = null; // de-reference the buffer so it can be GC'ed sooner + } + + /** + * Not supported by this stream. Throws {@link UnsupportedOperationException} + * @param readlimit ignored + */ + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException("mark()/reset() not supported on this stream"); + } + + /** + * Not supported by this stream. Throws {@link UnsupportedOperationException} + */ + @Override + public synchronized void reset() throws IOException { + throw new UnsupportedOperationException("mark()/reset() not supported on this stream"); + } + + /** + * gets whether mark and reset are supported by {@code ADLFileInputStream}. Always returns false. + * + * @return always {@code false} + */ + @Override + public boolean markSupported() { + return false; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsOutputStream.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsOutputStream.java new file mode 100644 index 00000000000..7e43090a957 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsOutputStream.java @@ -0,0 +1,378 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.util.Locale; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; + +import org.apache.hadoop.fs.FSExceptionMessages; +import org.apache.hadoop.fs.StreamCapabilities; +import org.apache.hadoop.fs.Syncable; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; + +/** + * The BlobFsOutputStream for Rest AbfsClient. + */ +public class AbfsOutputStream extends OutputStream implements Syncable, StreamCapabilities { + private final AbfsClient client; + private final String path; + private long position; + private boolean closed; + private boolean supportFlush; + private volatile IOException lastError; + + private long lastFlushOffset; + private long lastTotalAppendOffset = 0; + + private final int bufferSize; + private byte[] buffer; + private int bufferIndex; + private final int maxConcurrentRequestCount; + + private ConcurrentLinkedDeque writeOperations; + private final ThreadPoolExecutor threadExecutor; + private final ExecutorCompletionService completionService; + + public AbfsOutputStream( + final AbfsClient client, + final String path, + final long position, + final int bufferSize, + final boolean supportFlush) { + this.client = client; + this.path = path; + this.position = position; + this.closed = false; + this.supportFlush = supportFlush; + this.lastError = null; + this.lastFlushOffset = 0; + this.bufferSize = bufferSize; + this.buffer = new byte[bufferSize]; + this.bufferIndex = 0; + this.writeOperations = new ConcurrentLinkedDeque<>(); + + this.maxConcurrentRequestCount = 4 * Runtime.getRuntime().availableProcessors(); + + this.threadExecutor + = new ThreadPoolExecutor(maxConcurrentRequestCount, + maxConcurrentRequestCount, + 10L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>()); + this.completionService = new ExecutorCompletionService<>(this.threadExecutor); + } + + /** + * Query the stream for a specific capability. + * + * @param capability string to query the stream support for. + * @return true for hsync and hflush. + */ + @Override + public boolean hasCapability(String capability) { + switch (capability.toLowerCase(Locale.ENGLISH)) { + case StreamCapabilities.HSYNC: + case StreamCapabilities.HFLUSH: + return supportFlush; + default: + return false; + } + } + + /** + * Writes the specified byte to this output stream. The general contract for + * write is that one byte is written to the output stream. The byte to be + * written is the eight low-order bits of the argument b. The 24 high-order + * bits of b are ignored. + * + * @param byteVal the byteValue to write. + * @throws IOException if an I/O error occurs. In particular, an IOException may be + * thrown if the output stream has been closed. + */ + @Override + public void write(final int byteVal) throws IOException { + write(new byte[]{(byte) (byteVal & 0xFF)}); + } + + /** + * Writes length bytes from the specified byte array starting at off to + * this output stream. + * + * @param data the byte array to write. + * @param off the start off in the data. + * @param length the number of bytes to write. + * @throws IOException if an I/O error occurs. In particular, an IOException may be + * thrown if the output stream has been closed. + */ + @Override + public synchronized void write(final byte[] data, final int off, final int length) + throws IOException { + maybeThrowLastError(); + + Preconditions.checkArgument(data != null, "null data"); + + if (off < 0 || length < 0 || length > data.length - off) { + throw new IndexOutOfBoundsException(); + } + + int currentOffset = off; + int writableBytes = bufferSize - bufferIndex; + int numberOfBytesToWrite = length; + + while (numberOfBytesToWrite > 0) { + if (writableBytes <= numberOfBytesToWrite) { + System.arraycopy(data, currentOffset, buffer, bufferIndex, writableBytes); + bufferIndex += writableBytes; + writeCurrentBufferToService(); + currentOffset += writableBytes; + numberOfBytesToWrite = numberOfBytesToWrite - writableBytes; + } else { + System.arraycopy(data, currentOffset, buffer, bufferIndex, numberOfBytesToWrite); + bufferIndex += numberOfBytesToWrite; + numberOfBytesToWrite = 0; + } + + writableBytes = bufferSize - bufferIndex; + } + } + + /** + * Throw the last error recorded if not null. + * After the stream is closed, this is always set to + * an exception, so acts as a guard against method invocation once + * closed. + * @throws IOException if lastError is set + */ + private void maybeThrowLastError() throws IOException { + if (lastError != null) { + throw lastError; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes to be + * written out. If any data remains in the payload it is committed to the + * service. Data is queued for writing and forced out to the service + * before the call returns. + */ + @Override + public void flush() throws IOException { + if (supportFlush) { + flushInternalAsync(); + } + } + + /** Similar to posix fsync, flush out the data in client's user buffer + * all the way to the disk device (but the disk may have it in its cache). + * @throws IOException if error occurs + */ + @Override + public void hsync() throws IOException { + if (supportFlush) { + flushInternal(); + } + } + + /** Flush out the data in client's user buffer. After the return of + * this call, new readers will see the data. + * @throws IOException if any error occurs + */ + @Override + public void hflush() throws IOException { + if (supportFlush) { + flushInternal(); + } + } + + /** + * Force all data in the output stream to be written to Azure storage. + * Wait to return until this is complete. Close the access to the stream and + * shutdown the upload thread pool. + * If the blob was created, its lease will be released. + * Any error encountered caught in threads and stored will be rethrown here + * after cleanup. + */ + @Override + public synchronized void close() throws IOException { + if (closed) { + return; + } + + try { + flushInternal(); + threadExecutor.shutdown(); + } finally { + lastError = new IOException(FSExceptionMessages.STREAM_IS_CLOSED); + buffer = null; + bufferIndex = 0; + closed = true; + writeOperations.clear(); + if (!threadExecutor.isShutdown()) { + threadExecutor.shutdownNow(); + } + } + } + + private synchronized void flushInternal() throws IOException { + maybeThrowLastError(); + writeCurrentBufferToService(); + flushWrittenBytesToService(); + } + + private synchronized void flushInternalAsync() throws IOException { + maybeThrowLastError(); + writeCurrentBufferToService(); + flushWrittenBytesToServiceAsync(); + } + + private synchronized void writeCurrentBufferToService() throws IOException { + if (bufferIndex == 0) { + return; + } + + final byte[] bytes = buffer; + final int bytesLength = bufferIndex; + + buffer = new byte[bufferSize]; + bufferIndex = 0; + final long offset = position; + position += bytesLength; + + if (threadExecutor.getQueue().size() >= maxConcurrentRequestCount * 2) { + waitForTaskToComplete(); + } + + final Future job = completionService.submit(new Callable() { + @Override + public Void call() throws Exception { + client.append(path, offset, bytes, 0, + bytesLength); + return null; + } + }); + + writeOperations.add(new WriteOperation(job, offset, bytesLength)); + + // Try to shrink the queue + shrinkWriteOperationQueue(); + } + + private synchronized void flushWrittenBytesToService() throws IOException { + for (WriteOperation writeOperation : writeOperations) { + try { + writeOperation.task.get(); + } catch (Exception ex) { + if (ex.getCause() instanceof AzureBlobFileSystemException) { + ex = (AzureBlobFileSystemException) ex.getCause(); + } + lastError = new IOException(ex); + throw lastError; + } + } + flushWrittenBytesToServiceInternal(position, false); + } + + private synchronized void flushWrittenBytesToServiceAsync() throws IOException { + shrinkWriteOperationQueue(); + + if (this.lastTotalAppendOffset > this.lastFlushOffset) { + this.flushWrittenBytesToServiceInternal(this.lastTotalAppendOffset, true); + } + } + + private synchronized void flushWrittenBytesToServiceInternal(final long offset, + final boolean retainUncommitedData) throws IOException { + try { + client.flush(path, offset, retainUncommitedData); + } catch (AzureBlobFileSystemException ex) { + throw new IOException(ex); + } + this.lastFlushOffset = offset; + } + + /** + * Try to remove the completed write operations from the beginning of write + * operation FIFO queue. + */ + private synchronized void shrinkWriteOperationQueue() throws IOException { + try { + while (writeOperations.peek() != null && writeOperations.peek().task.isDone()) { + writeOperations.peek().task.get(); + lastTotalAppendOffset += writeOperations.peek().length; + writeOperations.remove(); + } + } catch (Exception e) { + if (e.getCause() instanceof AzureBlobFileSystemException) { + lastError = (AzureBlobFileSystemException) e.getCause(); + } else { + lastError = new IOException(e); + } + throw lastError; + } + } + + private void waitForTaskToComplete() throws IOException { + boolean completed; + for (completed = false; completionService.poll() != null; completed = true) { + // keep polling until there is no data + } + + if (!completed) { + try { + completionService.take(); + } catch (InterruptedException e) { + lastError = (IOException) new InterruptedIOException(e.toString()).initCause(e); + throw lastError; + } + } + } + + private static class WriteOperation { + private final Future task; + private final long startOffset; + private final long length; + + WriteOperation(final Future task, final long startOffset, final long length) { + Preconditions.checkNotNull(task, "task"); + Preconditions.checkArgument(startOffset >= 0, "startOffset"); + Preconditions.checkArgument(length >= 0, "length"); + + this.task = task; + this.startOffset = startOffset; + this.length = length; + } + } + + @VisibleForTesting + public synchronized void waitForPendingUploads() throws IOException { + waitForTaskToComplete(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsPermission.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsPermission.java new file mode 100644 index 00000000000..9c44610ff59 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsPermission.java @@ -0,0 +1,114 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.permission.FsPermission; + +/** + * The AbfsPermission for AbfsClient. + */ +public class AbfsPermission extends FsPermission { + private static final int STICKY_BIT_OCTAL_VALUE = 01000; + private final boolean aclBit; + + public AbfsPermission(Short aShort, boolean aclBitStatus) { + super(aShort); + this.aclBit = aclBitStatus; + } + + public AbfsPermission(FsAction u, FsAction g, FsAction o) { + super(u, g, o, false); + this.aclBit = false; + } + + /** + * Returns true if there is also an ACL (access control list). + * + * @return boolean true if there is also an ACL (access control list). + * @deprecated Get acl bit from the {@link org.apache.hadoop.fs.FileStatus} + * object. + */ + public boolean getAclBit() { + return aclBit; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FsPermission) { + FsPermission that = (FsPermission) obj; + return this.getUserAction() == that.getUserAction() + && this.getGroupAction() == that.getGroupAction() + && this.getOtherAction() == that.getOtherAction() + && this.getStickyBit() == that.getStickyBit(); + } + return false; + } + + /** + * Create a AbfsPermission from a abfs symbolic permission string + * @param abfsSymbolicPermission e.g. "rw-rw-rw-+" / "rw-rw-rw-" + * @return a permission object for the provided string representation + */ + public static AbfsPermission valueOf(final String abfsSymbolicPermission) { + if (abfsSymbolicPermission == null) { + return null; + } + + final boolean isExtendedAcl = abfsSymbolicPermission.charAt(abfsSymbolicPermission.length() - 1) == '+'; + + final String abfsRawSymbolicPermission = isExtendedAcl ? abfsSymbolicPermission.substring(0, abfsSymbolicPermission.length() - 1) + : abfsSymbolicPermission; + + int n = 0; + for (int i = 0; i < abfsRawSymbolicPermission.length(); i++) { + n = n << 1; + char c = abfsRawSymbolicPermission.charAt(i); + n += (c == '-' || c == 'T' || c == 'S') ? 0: 1; + } + + // Add sticky bit value if set + if (abfsRawSymbolicPermission.charAt(abfsRawSymbolicPermission.length() - 1) == 't' + || abfsRawSymbolicPermission.charAt(abfsRawSymbolicPermission.length() - 1) == 'T') { + n += STICKY_BIT_OCTAL_VALUE; + } + + return new AbfsPermission((short) n, isExtendedAcl); + } + + /** + * Check whether abfs symbolic permission string is a extended Acl + * @param abfsSymbolicPermission e.g. "rw-rw-rw-+" / "rw-rw-rw-" + * @return true if the permission string indicates the existence of an + * extended ACL; otherwise false. + */ + public static boolean isExtendedAcl(final String abfsSymbolicPermission) { + if (abfsSymbolicPermission == null) { + return false; + } + + return abfsSymbolicPermission.charAt(abfsSymbolicPermission.length() - 1) == '+'; + } + + @Override + public int hashCode() { + return toShort(); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java new file mode 100644 index 00000000000..3f5717ee7e1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperation.java @@ -0,0 +1,193 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidAbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; + +/** + * The AbfsRestOperation for Rest AbfsClient. + */ +public class AbfsRestOperation { + // The type of the REST operation (Append, ReadFile, etc) + private final AbfsRestOperationType operationType; + // Blob FS client, which has the credentials, retry policy, and logs. + private final AbfsClient client; + // the HTTP method (PUT, PATCH, POST, GET, HEAD, or DELETE) + private final String method; + // full URL including query parameters + private final URL url; + // all the custom HTTP request headers provided by the caller + private final List requestHeaders; + + // This is a simple operation class, where all the upload methods have a + // request body and all the download methods have a response body. + private final boolean hasRequestBody; + + private static final Logger LOG = LoggerFactory.getLogger(AbfsClient.class); + + // For uploads, this is the request entity body. For downloads, + // this will hold the response entity body. + private byte[] buffer; + private int bufferOffset; + private int bufferLength; + + private AbfsHttpOperation result; + + public AbfsHttpOperation getResult() { + return result; + } + + /** + * Initializes a new REST operation. + * + * @param client The Blob FS client. + * @param method The HTTP method (PUT, PATCH, POST, GET, HEAD, or DELETE). + * @param url The full URL including query string parameters. + * @param requestHeaders The HTTP request headers. + */ + AbfsRestOperation(final AbfsRestOperationType operationType, + final AbfsClient client, + final String method, + final URL url, + final List requestHeaders) { + this.operationType = operationType; + this.client = client; + this.method = method; + this.url = url; + this.requestHeaders = requestHeaders; + this.hasRequestBody = (AbfsHttpConstants.HTTP_METHOD_PUT.equals(method) + || AbfsHttpConstants.HTTP_METHOD_PATCH.equals(method)); + } + + /** + * Initializes a new REST operation. + * + * @param operationType The type of the REST operation (Append, ReadFile, etc). + * @param client The Blob FS client. + * @param method The HTTP method (PUT, PATCH, POST, GET, HEAD, or DELETE). + * @param url The full URL including query string parameters. + * @param requestHeaders The HTTP request headers. + * @param buffer For uploads, this is the request entity body. For downloads, + * this will hold the response entity body. + * @param bufferOffset An offset into the buffer where the data beings. + * @param bufferLength The length of the data in the buffer. + */ + AbfsRestOperation(AbfsRestOperationType operationType, + AbfsClient client, + String method, + URL url, + List requestHeaders, + byte[] buffer, + int bufferOffset, + int bufferLength) { + this(operationType, client, method, url, requestHeaders); + this.buffer = buffer; + this.bufferOffset = bufferOffset; + this.bufferLength = bufferLength; + } + + /** + * Executes the REST operation with retry, by issuing one or more + * HTTP operations. + */ + void execute() throws AzureBlobFileSystemException { + int retryCount = 0; + while (!executeHttpOperation(retryCount++)) { + try { + Thread.sleep(client.getRetryPolicy().getRetryInterval(retryCount)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + if (result.getStatusCode() >= HttpURLConnection.HTTP_BAD_REQUEST) { + throw new AbfsRestOperationException(result.getStatusCode(), result.getStorageErrorCode(), + result.getStorageErrorMessage(), null, result); + } + } + + /** + * Executes a single HTTP operation to complete the REST operation. If it + * fails, there may be a retry. The retryCount is incremented with each + * attempt. + */ + private boolean executeHttpOperation(final int retryCount) throws AzureBlobFileSystemException { + AbfsHttpOperation httpOperation = null; + try { + // initialize the HTTP request and open the connection + httpOperation = new AbfsHttpOperation(url, method, requestHeaders); + + // sign the HTTP request + if (client.getAccessToken() == null) { + // sign the HTTP request + client.getSharedKeyCredentials().signRequest( + httpOperation.getConnection(), + hasRequestBody ? bufferLength : 0); + } else { + httpOperation.getConnection().setRequestProperty(HttpHeaderConfigurations.AUTHORIZATION, + client.getAccessToken()); + } + + AbfsClientThrottlingIntercept.sendingRequest(operationType); + + if (hasRequestBody) { + // HttpUrlConnection requires + httpOperation.sendRequest(buffer, bufferOffset, bufferLength); + } + + httpOperation.processResponse(buffer, bufferOffset, bufferLength); + } catch (IOException ex) { + if (LOG.isDebugEnabled()) { + if (httpOperation != null) { + LOG.debug("HttpRequestFailure: " + httpOperation.toString(), ex); + } else { + LOG.debug("HttpRequestFailure: " + method + "," + url, ex); + } + } + if (!client.getRetryPolicy().shouldRetry(retryCount, -1)) { + throw new InvalidAbfsRestOperationException(ex); + } + return false; + } finally { + AbfsClientThrottlingIntercept.updateMetrics(operationType, httpOperation); + } + + LOG.debug("HttpRequest: " + httpOperation.toString()); + + if (client.getRetryPolicy().shouldRetry(retryCount, httpOperation.getStatusCode())) { + return false; + } + + result = httpOperation; + + return true; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperationType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperationType.java new file mode 100644 index 00000000000..eeea81750e6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsRestOperationType.java @@ -0,0 +1,42 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +/** + * The REST operation type (Read, Append, Other ). + */ +public enum AbfsRestOperationType { + CreateFileSystem, + GetFileSystemProperties, + SetFileSystemProperties, + ListPaths, + DeleteFileSystem, + CreatePath, + RenamePath, + GetAcl, + GetPathProperties, + SetAcl, + SetOwner, + SetPathProperties, + SetPermissions, + Append, + Flush, + ReadFile, + DeletePath +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsUriQueryBuilder.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsUriQueryBuilder.java new file mode 100644 index 00000000000..a200b406a55 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AbfsUriQueryBuilder.java @@ -0,0 +1,64 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AzureBlobFileSystemException; + +/** + * The UrlQueryBuilder for Rest AbfsClient. + */ +public class AbfsUriQueryBuilder { + private Map parameters; + + public AbfsUriQueryBuilder() { + this.parameters = new HashMap<>(); + } + + public void addQuery(final String name, final String value) { + if (value != null && !value.isEmpty()) { + this.parameters.put(name, value); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean first = true; + + for (Map.Entry entry : parameters.entrySet()) { + if (first) { + sb.append(AbfsHttpConstants.QUESTION_MARK); + first = false; + } else { + sb.append(AbfsHttpConstants.AND_MARK); + } + try { + sb.append(entry.getKey()).append(AbfsHttpConstants.EQUAL).append(AbfsClient.urlEncode(entry.getValue())); + } + catch (AzureBlobFileSystemException ex) { + throw new IllegalArgumentException("Query string param is not encode-able: " + entry.getKey() + "=" + entry.getValue()); + } + } + return sb.toString(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AuthType.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AuthType.java new file mode 100644 index 00000000000..c95b92cbe61 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/AuthType.java @@ -0,0 +1,27 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +/** + * Auth Type Enum. + */ +public enum AuthType { + SharedKey, + OAuth, + Custom +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ExponentialRetryPolicy.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ExponentialRetryPolicy.java new file mode 100644 index 00000000000..5eb7a6639a6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ExponentialRetryPolicy.java @@ -0,0 +1,144 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.util.Random; +import java.net.HttpURLConnection; + +/** + * Retry policy used by AbfsClient. + * */ +public class ExponentialRetryPolicy { + /** + * Represents the default number of retry attempts. + */ + private static final int DEFAULT_CLIENT_RETRY_COUNT = 30; + + /** + * Represents the default amount of time used when calculating a random delta in the exponential + * delay between retries. + */ + private static final int DEFAULT_CLIENT_BACKOFF = 1000 * 3; + + /** + * Represents the default maximum amount of time used when calculating the exponential + * delay between retries. + */ + private static final int DEFAULT_MAX_BACKOFF = 1000 * 30; + + /** + * Represents the default minimum amount of time used when calculating the exponential + * delay between retries. + */ + private static final int DEFAULT_MIN_BACKOFF = 1000 * 3; + + /** + * The minimum random ratio used for delay interval calculation. + */ + private static final double MIN_RANDOM_RATIO = 0.8; + + /** + * The maximum random ratio used for delay interval calculation. + */ + private static final double MAX_RANDOM_RATIO = 1.2; + + /** + * Holds the random number generator used to calculate randomized backoff intervals + */ + private final Random randRef = new Random(); + + /** + * The value that will be used to calculate a random delta in the exponential delay interval + */ + private final int deltaBackoff; + + /** + * The maximum backoff time. + */ + private final int maxBackoff; + + /** + * The minimum backoff time. + */ + private final int minBackoff; + + /** + * The maximum number of retry attempts. + */ + private final int retryCount; + + /** + * Initializes a new instance of the {@link ExponentialRetryPolicy} class. + */ + public ExponentialRetryPolicy() { + this(DEFAULT_CLIENT_RETRY_COUNT, DEFAULT_MIN_BACKOFF, DEFAULT_MAX_BACKOFF, DEFAULT_CLIENT_BACKOFF); + } + + /** + * Initializes a new instance of the {@link ExponentialRetryPolicy} class. + * + * @param retryCount The maximum number of retry attempts. + * @param minBackoff The minimum backoff time. + * @param maxBackoff The maximum backoff time. + * @param deltaBackoff The value that will be used to calculate a random delta in the exponential delay + * between retries. + */ + public ExponentialRetryPolicy(final int retryCount, final int minBackoff, final int maxBackoff, final int deltaBackoff) { + this.retryCount = retryCount; + this.minBackoff = minBackoff; + this.maxBackoff = maxBackoff; + this.deltaBackoff = deltaBackoff; + } + + /** + * Returns if a request should be retried based on the retry count, current response, + * and the current strategy. + * + * @param retryCount The current retry attempt count. + * @param statusCode The status code of the response, or -1 for socket error. + * @return true if the request should be retried; false otherwise. + */ + public boolean shouldRetry(final int retryCount, final int statusCode) { + return retryCount < this.retryCount + && (statusCode == -1 + || statusCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT + || (statusCode >= HttpURLConnection.HTTP_INTERNAL_ERROR + && statusCode != HttpURLConnection.HTTP_NOT_IMPLEMENTED + && statusCode != HttpURLConnection.HTTP_VERSION)); + } + + /** + * Returns backoff interval between 80% and 120% of the desired backoff, + * multiply by 2^n-1 for exponential. + * + * @param retryCount The current retry attempt count. + * @return backoff Interval time + */ + public long getRetryInterval(final int retryCount) { + final long boundedRandDelta = (int) (this.deltaBackoff * MIN_RANDOM_RATIO) + + this.randRef.nextInt((int) (this.deltaBackoff * MAX_RANDOM_RATIO) + - (int) (this.deltaBackoff * MIN_RANDOM_RATIO)); + + final double incrementDelta = (Math.pow(2, retryCount - 1)) * boundedRandDelta; + + final long retryInterval = (int) Math.round(Math.min(this.minBackoff + incrementDelta, maxBackoff)); + + return retryInterval; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/KeyProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/KeyProvider.java new file mode 100644 index 00000000000..09491c520bd --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/KeyProvider.java @@ -0,0 +1,43 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.KeyProviderException; + +/** + * The interface that every Azure file system key provider must implement. + */ +public interface KeyProvider { + /** + * Key providers must implement this method. Given a list of configuration + * parameters for the specified Azure storage account, retrieve the plaintext + * storage account key. + * + * @param accountName + * the storage account name + * @param conf + * Hadoop configuration parameters + * @return the plaintext storage account key + * @throws KeyProviderException if an error occurs while attempting to get + * the storage account key. + */ + String getStorageAccountKey(String accountName, Configuration conf) + throws KeyProviderException; +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBuffer.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBuffer.java new file mode 100644 index 00000000000..00e4f008ad0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBuffer.java @@ -0,0 +1,139 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.util.concurrent.CountDownLatch; + +import org.apache.hadoop.fs.azurebfs.contracts.services.ReadBufferStatus; + +class ReadBuffer { + + private AbfsInputStream stream; + private long offset; // offset within the file for the buffer + private int length; // actual length, set after the buffer is filles + private int requestedLength; // requested length of the read + private byte[] buffer; // the buffer itself + private int bufferindex = -1; // index in the buffers array in Buffer manager + private ReadBufferStatus status; // status of the buffer + private CountDownLatch latch = null; // signaled when the buffer is done reading, so any client + // waiting on this buffer gets unblocked + + // fields to help with eviction logic + private long timeStamp = 0; // tick at which buffer became available to read + private boolean isFirstByteConsumed = false; + private boolean isLastByteConsumed = false; + private boolean isAnyByteConsumed = false; + + public AbfsInputStream getStream() { + return stream; + } + + public void setStream(AbfsInputStream stream) { + this.stream = stream; + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + public int getRequestedLength() { + return requestedLength; + } + + public void setRequestedLength(int requestedLength) { + this.requestedLength = requestedLength; + } + + public byte[] getBuffer() { + return buffer; + } + + public void setBuffer(byte[] buffer) { + this.buffer = buffer; + } + + public int getBufferindex() { + return bufferindex; + } + + public void setBufferindex(int bufferindex) { + this.bufferindex = bufferindex; + } + + public ReadBufferStatus getStatus() { + return status; + } + + public void setStatus(ReadBufferStatus status) { + this.status = status; + } + + public CountDownLatch getLatch() { + return latch; + } + + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } + + public long getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + + public boolean isFirstByteConsumed() { + return isFirstByteConsumed; + } + + public void setFirstByteConsumed(boolean isFirstByteConsumed) { + this.isFirstByteConsumed = isFirstByteConsumed; + } + + public boolean isLastByteConsumed() { + return isLastByteConsumed; + } + + public void setLastByteConsumed(boolean isLastByteConsumed) { + this.isLastByteConsumed = isLastByteConsumed; + } + + public boolean isAnyByteConsumed() { + return isAnyByteConsumed; + } + + public void setAnyByteConsumed(boolean isAnyByteConsumed) { + this.isAnyByteConsumed = isAnyByteConsumed; + } + +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferManager.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferManager.java new file mode 100644 index 00000000000..5b71cf05225 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferManager.java @@ -0,0 +1,395 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +import org.apache.hadoop.fs.azurebfs.contracts.services.ReadBufferStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Stack; +import java.util.concurrent.CountDownLatch; + +/** + * The Read Buffer Manager for Rest AbfsClient. + */ +final class ReadBufferManager { + private static final Logger LOGGER = LoggerFactory.getLogger(ReadBufferManager.class); + + private static final int NUM_BUFFERS = 16; + private static final int BLOCK_SIZE = 4 * 1024 * 1024; + private static final int NUM_THREADS = 8; + private static final int THRESHOLD_AGE_MILLISECONDS = 3000; // have to see if 3 seconds is a good threshold + + private Thread[] threads = new Thread[NUM_THREADS]; + private byte[][] buffers; // array of byte[] buffers, to hold the data that is read + private Stack freeList = new Stack<>(); // indices in buffers[] array that are available + + private Queue readAheadQueue = new LinkedList<>(); // queue of requests that are not picked up by any worker thread yet + private LinkedList inProgressList = new LinkedList<>(); // requests being processed by worker threads + private LinkedList completedReadList = new LinkedList<>(); // buffers available for reading + private static final ReadBufferManager BUFFER_MANAGER; // singleton, initialized in static initialization block + + static { + BUFFER_MANAGER = new ReadBufferManager(); + BUFFER_MANAGER.init(); + } + + static ReadBufferManager getBufferManager() { + return BUFFER_MANAGER; + } + + private void init() { + buffers = new byte[NUM_BUFFERS][]; + for (int i = 0; i < NUM_BUFFERS; i++) { + buffers[i] = new byte[BLOCK_SIZE]; // same buffers are reused. The byte array never goes back to GC + freeList.add(i); + } + for (int i = 0; i < NUM_THREADS; i++) { + Thread t = new Thread(new ReadBufferWorker(i)); + t.setDaemon(true); + threads[i] = t; + t.setName("ABFS-prefetch-" + i); + t.start(); + } + ReadBufferWorker.UNLEASH_WORKERS.countDown(); + } + + // hide instance constructor + private ReadBufferManager() { + } + + + /* + * + * AbfsInputStream-facing methods + * + */ + + + /** + * {@link AbfsInputStream} calls this method to queue read-aheads. + * + * @param stream The {@link AbfsInputStream} for which to do the read-ahead + * @param requestedOffset The offset in the file which shoukd be read + * @param requestedLength The length to read + */ + void queueReadAhead(final AbfsInputStream stream, final long requestedOffset, final int requestedLength) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Start Queueing readAhead for {} offset {} length {}", + stream.getPath(), requestedOffset, requestedLength); + } + ReadBuffer buffer; + synchronized (this) { + if (isAlreadyQueued(stream, requestedOffset)) { + return; // already queued, do not queue again + } + if (freeList.isEmpty() && !tryEvict()) { + return; // no buffers available, cannot queue anything + } + + buffer = new ReadBuffer(); + buffer.setStream(stream); + buffer.setOffset(requestedOffset); + buffer.setLength(0); + buffer.setRequestedLength(requestedLength); + buffer.setStatus(ReadBufferStatus.NOT_AVAILABLE); + buffer.setLatch(new CountDownLatch(1)); + + Integer bufferIndex = freeList.pop(); // will return a value, since we have checked size > 0 already + + buffer.setBuffer(buffers[bufferIndex]); + buffer.setBufferindex(bufferIndex); + readAheadQueue.add(buffer); + notifyAll(); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Done q-ing readAhead for file {} offset {} buffer idx {}", + stream.getPath(), requestedOffset, buffer.getBufferindex()); + } + } + + + /** + * {@link AbfsInputStream} calls this method read any bytes already available in a buffer (thereby saving a + * remote read). This returns the bytes if the data already exists in buffer. If there is a buffer that is reading + * the requested offset, then this method blocks until that read completes. If the data is queued in a read-ahead + * but not picked up by a worker thread yet, then it cancels that read-ahead and reports cache miss. This is because + * depending on worker thread availability, the read-ahead may take a while - the calling thread can do it's own + * read to get the data faster (copmared to the read waiting in queue for an indeterminate amount of time). + * + * @param stream the file to read bytes for + * @param position the offset in the file to do a read for + * @param length the length to read + * @param buffer the buffer to read data into. Note that the buffer will be written into from offset 0. + * @return the number of bytes read + */ + int getBlock(final AbfsInputStream stream, final long position, final int length, final byte[] buffer) { + // not synchronized, so have to be careful with locking + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("getBlock for file {} position {} thread {}", + stream.getPath(), position, Thread.currentThread().getName()); + } + + waitForProcess(stream, position); + + int bytesRead = 0; + synchronized (this) { + bytesRead = getBlockFromCompletedQueue(stream, position, length, buffer); + } + if (bytesRead > 0) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Done read from Cache for {} position {} length {}", + stream.getPath(), position, bytesRead); + } + return bytesRead; + } + + // otherwise, just say we got nothing - calling thread can do its own read + return 0; + } + + /* + * + * Internal methods + * + */ + + private void waitForProcess(final AbfsInputStream stream, final long position) { + ReadBuffer readBuf; + synchronized (this) { + clearFromReadAheadQueue(stream, position); + readBuf = getFromList(inProgressList, stream, position); + } + if (readBuf != null) { // if in in-progress queue, then block for it + try { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("got a relevant read buffer for file {} offset {} buffer idx {}", + stream.getPath(), readBuf.getOffset(), readBuf.getBufferindex()); + } + readBuf.getLatch().await(); // blocking wait on the caller stream's thread + // Note on correctness: readBuf gets out of inProgressList only in 1 place: after worker thread + // is done processing it (in doneReading). There, the latch is set after removing the buffer from + // inProgressList. So this latch is safe to be outside the synchronized block. + // Putting it in synchronized would result in a deadlock, since this thread would be holding the lock + // while waiting, so no one will be able to change any state. If this becomes more complex in the future, + // then the latch cane be removed and replaced with wait/notify whenever inProgressList is touched. + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("latch done for file {} buffer idx {} length {}", + stream.getPath(), readBuf.getBufferindex(), readBuf.getLength()); + } + } + } + + /** + * If any buffer in the completedlist can be reclaimed then reclaim it and return the buffer to free list. + * The objective is to find just one buffer - there is no advantage to evicting more than one. + * + * @return whether the eviction succeeeded - i.e., were we able to free up one buffer + */ + private synchronized boolean tryEvict() { + ReadBuffer nodeToEvict = null; + if (completedReadList.size() <= 0) { + return false; // there are no evict-able buffers + } + + // first, try buffers where all bytes have been consumed (approximated as first and last bytes consumed) + for (ReadBuffer buf : completedReadList) { + if (buf.isFirstByteConsumed() && buf.isLastByteConsumed()) { + nodeToEvict = buf; + break; + } + } + if (nodeToEvict != null) { + return evict(nodeToEvict); + } + + // next, try buffers where any bytes have been consumed (may be a bad idea? have to experiment and see) + for (ReadBuffer buf : completedReadList) { + if (buf.isAnyByteConsumed()) { + nodeToEvict = buf; + break; + } + } + + if (nodeToEvict != null) { + return evict(nodeToEvict); + } + + // next, try any old nodes that have not been consumed + long earliestBirthday = Long.MAX_VALUE; + for (ReadBuffer buf : completedReadList) { + if (buf.getTimeStamp() < earliestBirthday) { + nodeToEvict = buf; + earliestBirthday = buf.getTimeStamp(); + } + } + if ((currentTimeMillis() - earliestBirthday > THRESHOLD_AGE_MILLISECONDS) && (nodeToEvict != null)) { + return evict(nodeToEvict); + } + + // nothing can be evicted + return false; + } + + private boolean evict(final ReadBuffer buf) { + freeList.push(buf.getBufferindex()); + completedReadList.remove(buf); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Evicting buffer idx {}; was used for file {} offset {} length {}", + buf.getBufferindex(), buf.getStream().getPath(), buf.getOffset(), buf.getLength()); + } + return true; + } + + private boolean isAlreadyQueued(final AbfsInputStream stream, final long requestedOffset) { + // returns true if any part of the buffer is already queued + return (isInList(readAheadQueue, stream, requestedOffset) + || isInList(inProgressList, stream, requestedOffset) + || isInList(completedReadList, stream, requestedOffset)); + } + + private boolean isInList(final Collection list, final AbfsInputStream stream, final long requestedOffset) { + return (getFromList(list, stream, requestedOffset) != null); + } + + private ReadBuffer getFromList(final Collection list, final AbfsInputStream stream, final long requestedOffset) { + for (ReadBuffer buffer : list) { + if (buffer.getStream() == stream) { + if (buffer.getStatus() == ReadBufferStatus.AVAILABLE + && requestedOffset >= buffer.getOffset() + && requestedOffset < buffer.getOffset() + buffer.getLength()) { + return buffer; + } else if (requestedOffset >= buffer.getOffset() + && requestedOffset < buffer.getOffset() + buffer.getRequestedLength()) { + return buffer; + } + } + } + return null; + } + + private void clearFromReadAheadQueue(final AbfsInputStream stream, final long requestedOffset) { + ReadBuffer buffer = getFromList(readAheadQueue, stream, requestedOffset); + if (buffer != null) { + readAheadQueue.remove(buffer); + notifyAll(); // lock is held in calling method + freeList.push(buffer.getBufferindex()); + } + } + + private int getBlockFromCompletedQueue(final AbfsInputStream stream, final long position, final int length, + final byte[] buffer) { + ReadBuffer buf = getFromList(completedReadList, stream, position); + if (buf == null || position >= buf.getOffset() + buf.getLength()) { + return 0; + } + int cursor = (int) (position - buf.getOffset()); + int availableLengthInBuffer = buf.getLength() - cursor; + int lengthToCopy = Math.min(length, availableLengthInBuffer); + System.arraycopy(buf.getBuffer(), cursor, buffer, 0, lengthToCopy); + if (cursor == 0) { + buf.setFirstByteConsumed(true); + } + if (cursor + lengthToCopy == buf.getLength()) { + buf.setLastByteConsumed(true); + } + buf.setAnyByteConsumed(true); + return lengthToCopy; + } + + /* + * + * ReadBufferWorker-thread-facing methods + * + */ + + /** + * ReadBufferWorker thread calls this to get the next buffer that it should work on. + * + * @return {@link ReadBuffer} + * @throws InterruptedException if thread is interrupted + */ + ReadBuffer getNextBlockToRead() throws InterruptedException { + ReadBuffer buffer = null; + synchronized (this) { + //buffer = readAheadQueue.take(); // blocking method + while (readAheadQueue.size() == 0) { + wait(); + } + buffer = readAheadQueue.remove(); + notifyAll(); + if (buffer == null) { + return null; // should never happen + } + buffer.setStatus(ReadBufferStatus.READING_IN_PROGRESS); + inProgressList.add(buffer); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("ReadBufferWorker picked file {} for offset {}", + buffer.getStream().getPath(), buffer.getOffset()); + } + return buffer; + } + + /** + * ReadBufferWorker thread calls this method to post completion. + * + * @param buffer the buffer whose read was completed + * @param result the {@link ReadBufferStatus} after the read operation in the worker thread + * @param bytesActuallyRead the number of bytes that the worker thread was actually able to read + */ + void doneReading(final ReadBuffer buffer, final ReadBufferStatus result, final int bytesActuallyRead) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("ReadBufferWorker completed file {} for offset {} bytes {}", + buffer.getStream().getPath(), buffer.getOffset(), bytesActuallyRead); + } + synchronized (this) { + inProgressList.remove(buffer); + if (result == ReadBufferStatus.AVAILABLE && bytesActuallyRead > 0) { + buffer.setStatus(ReadBufferStatus.AVAILABLE); + buffer.setTimeStamp(currentTimeMillis()); + buffer.setLength(bytesActuallyRead); + completedReadList.add(buffer); + } else { + freeList.push(buffer.getBufferindex()); + // buffer should go out of scope after the end of the calling method in ReadBufferWorker, and eligible for GC + } + } + //outside the synchronized, since anyone receiving a wake-up from the latch must see safe-published results + buffer.getLatch().countDown(); // wake up waiting threads (if any) + } + + /** + * Similar to System.currentTimeMillis, except implemented with System.nanoTime(). + * System.currentTimeMillis can go backwards when system clock is changed (e.g., with NTP time synchronization), + * making it unsuitable for measuring time intervals. nanotime is strictly monotonically increasing per CPU core. + * Note: it is not monotonic across Sockets, and even within a CPU, its only the + * more recent parts which share a clock across all cores. + * + * @return current time in milliseconds + */ + private long currentTimeMillis() { + return System.nanoTime() / 1000 / 1000; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferWorker.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferWorker.java new file mode 100644 index 00000000000..af69de0f089 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ReadBufferWorker.java @@ -0,0 +1,72 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.util.concurrent.CountDownLatch; + +import org.apache.hadoop.fs.azurebfs.contracts.services.ReadBufferStatus; + +class ReadBufferWorker implements Runnable { + + protected static final CountDownLatch UNLEASH_WORKERS = new CountDownLatch(1); + private int id; + + ReadBufferWorker(final int id) { + this.id = id; + } + + /** + * return the ID of ReadBufferWorker. + */ + public int getId() { + return this.id; + } + + /** + * Waits until a buffer becomes available in ReadAheadQueue. + * Once a buffer becomes available, reads the file specified in it and then posts results back to buffer manager. + * Rinse and repeat. Forever. + */ + public void run() { + try { + UNLEASH_WORKERS.await(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + ReadBufferManager bufferManager = ReadBufferManager.getBufferManager(); + ReadBuffer buffer; + while (true) { + try { + buffer = bufferManager.getNextBlockToRead(); // blocks, until a buffer is available for this thread + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } + if (buffer != null) { + try { + // do the actual read, from the file. + int bytesRead = buffer.getStream().readRemote(buffer.getOffset(), buffer.getBuffer(), 0, buffer.getRequestedLength()); + bufferManager.doneReading(buffer, ReadBufferStatus.AVAILABLE, bytesRead); // post result back to ReadBufferManager + } catch (Exception ex) { + bufferManager.doneReading(buffer, ReadBufferStatus.READ_FAILED, 0); + } + } + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SharedKeyCredentials.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SharedKeyCredentials.java new file mode 100644 index 00000000000..9ab9e504506 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SharedKeyCredentials.java @@ -0,0 +1,510 @@ +/** + * 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 + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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.azurebfs.services; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.hadoop.fs.azurebfs.constants.AbfsHttpConstants; +import org.apache.hadoop.fs.azurebfs.constants.HttpHeaderConfigurations; +import org.apache.hadoop.fs.azurebfs.utils.Base64; + +/** + * Represents the shared key credentials used to access an Azure Storage + * account. + */ +public class SharedKeyCredentials { + private static final int EXPECTED_BLOB_QUEUE_CANONICALIZED_STRING_LENGTH = 300; + private static final Pattern CRLF = Pattern.compile("\r\n", Pattern.LITERAL); + private static final String HMAC_SHA256 = "HmacSHA256"; + /** + * Stores a reference to the RFC1123 date/time pattern. + */ + private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + + + private String accountName; + private byte[] accountKey; + private Mac hmacSha256; + + public SharedKeyCredentials(final String accountName, + final String accountKey) { + if (accountName == null || accountName.isEmpty()) { + throw new IllegalArgumentException("Invalid account name."); + } + if (accountKey == null || accountKey.isEmpty()) { + throw new IllegalArgumentException("Invalid account key."); + } + this.accountName = accountName; + this.accountKey = Base64.decode(accountKey); + initializeMac(); + } + + public void signRequest(HttpURLConnection connection, final long contentLength) throws UnsupportedEncodingException { + + connection.setRequestProperty(HttpHeaderConfigurations.X_MS_DATE, getGMTTime()); + + final String stringToSign = canonicalize(connection, accountName, contentLength); + + final String computedBase64Signature = computeHmac256(stringToSign); + + connection.setRequestProperty(HttpHeaderConfigurations.AUTHORIZATION, + String.format("%s %s:%s", "SharedKey", accountName, computedBase64Signature)); + } + + private String computeHmac256(final String stringToSign) { + byte[] utf8Bytes; + try { + utf8Bytes = stringToSign.getBytes(AbfsHttpConstants.UTF_8); + } catch (final UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + byte[] hmac; + synchronized (this) { + hmac = hmacSha256.doFinal(utf8Bytes); + } + return Base64.encode(hmac); + } + + /** + * Add x-ms- prefixed headers in a fixed order. + * + * @param conn the HttpURLConnection for the operation + * @param canonicalizedString the canonicalized string to add the canonicalized headerst to. + */ + private static void addCanonicalizedHeaders(final HttpURLConnection conn, final StringBuilder canonicalizedString) { + // Look for header names that start with + // HeaderNames.PrefixForStorageHeader + // Then sort them in case-insensitive manner. + + final Map> headers = conn.getRequestProperties(); + final ArrayList httpStorageHeaderNameArray = new ArrayList(); + + for (final String key : headers.keySet()) { + if (key.toLowerCase(Locale.ROOT).startsWith(AbfsHttpConstants.HTTP_HEADER_PREFIX)) { + httpStorageHeaderNameArray.add(key.toLowerCase(Locale.ROOT)); + } + } + + Collections.sort(httpStorageHeaderNameArray); + + // Now go through each header's values in the sorted order and append + // them to the canonicalized string. + for (final String key : httpStorageHeaderNameArray) { + final StringBuilder canonicalizedElement = new StringBuilder(key); + String delimiter = ":"; + final ArrayList values = getHeaderValues(headers, key); + + boolean appendCanonicalizedElement = false; + // Go through values, unfold them, and then append them to the + // canonicalized element string. + for (final String value : values) { + if (value != null) { + appendCanonicalizedElement = true; + } + + // Unfolding is simply removal of CRLF. + final String unfoldedValue = CRLF.matcher(value) + .replaceAll(Matcher.quoteReplacement("")); + + // Append it to the canonicalized element string. + canonicalizedElement.append(delimiter); + canonicalizedElement.append(unfoldedValue); + delimiter = ","; + } + + // Now, add this canonicalized element to the canonicalized header + // string. + if (appendCanonicalizedElement) { + appendCanonicalizedElement(canonicalizedString, canonicalizedElement.toString()); + } + } + } + + /** + * Initialize the HmacSha256 associated with the account key. + */ + private void initializeMac() { + // Initializes the HMAC-SHA256 Mac and SecretKey. + try { + hmacSha256 = Mac.getInstance(HMAC_SHA256); + hmacSha256.init(new SecretKeySpec(accountKey, HMAC_SHA256)); + } catch (final Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Append a string to a string builder with a newline constant. + * + * @param builder the StringBuilder object + * @param element the string to append. + */ + private static void appendCanonicalizedElement(final StringBuilder builder, final String element) { + builder.append("\n"); + builder.append(element); + } + + /** + * Constructs a canonicalized string from the request's headers that will be used to construct the signature string + * for signing a Blob or Queue service request under the Shared Key Full authentication scheme. + * + * @param address the request URI + * @param accountName the account name associated with the request + * @param method the verb to be used for the HTTP request. + * @param contentType the content type of the HTTP request. + * @param contentLength the length of the content written to the outputstream in bytes, -1 if unknown + * @param date the date/time specification for the HTTP request + * @param conn the HttpURLConnection for the operation. + * @return A canonicalized string. + */ + private static String canonicalizeHttpRequest(final URL address, + final String accountName, final String method, final String contentType, + final long contentLength, final String date, final HttpURLConnection conn) + throws UnsupportedEncodingException { + + // The first element should be the Method of the request. + // I.e. GET, POST, PUT, or HEAD. + final StringBuilder canonicalizedString = new StringBuilder(EXPECTED_BLOB_QUEUE_CANONICALIZED_STRING_LENGTH); + canonicalizedString.append(conn.getRequestMethod()); + + // The next elements are + // If any element is missing it may be empty. + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.CONTENT_ENCODING, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.CONTENT_LANGUAGE, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + contentLength <= 0 ? "" : String.valueOf(contentLength)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.CONTENT_MD5, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, contentType != null ? contentType : AbfsHttpConstants.EMPTY_STRING); + + final String dateString = getHeaderValue(conn, HttpHeaderConfigurations.X_MS_DATE, AbfsHttpConstants.EMPTY_STRING); + // If x-ms-date header exists, Date should be empty string + appendCanonicalizedElement(canonicalizedString, dateString.equals(AbfsHttpConstants.EMPTY_STRING) ? date + : ""); + + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.IF_MODIFIED_SINCE, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.IF_MATCH, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.IF_NONE_MATCH, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.IF_UNMODIFIED_SINCE, AbfsHttpConstants.EMPTY_STRING)); + appendCanonicalizedElement(canonicalizedString, + getHeaderValue(conn, HttpHeaderConfigurations.RANGE, AbfsHttpConstants.EMPTY_STRING)); + + addCanonicalizedHeaders(conn, canonicalizedString); + + appendCanonicalizedElement(canonicalizedString, getCanonicalizedResource(address, accountName)); + + return canonicalizedString.toString(); + } + + /** + * Gets the canonicalized resource string for a Blob or Queue service request under the Shared Key Lite + * authentication scheme. + * + * @param address the resource URI. + * @param accountName the account name for the request. + * @return the canonicalized resource string. + */ + private static String getCanonicalizedResource(final URL address, + final String accountName) throws UnsupportedEncodingException { + // Resource path + final StringBuilder resourcepath = new StringBuilder(AbfsHttpConstants.FORWARD_SLASH); + resourcepath.append(accountName); + + // Note that AbsolutePath starts with a '/'. + resourcepath.append(address.getPath()); + final StringBuilder canonicalizedResource = new StringBuilder(resourcepath.toString()); + + // query parameters + if (address.getQuery() == null || !address.getQuery().contains(AbfsHttpConstants.EQUAL)) { + //no query params. + return canonicalizedResource.toString(); + } + + final Map queryVariables = parseQueryString(address.getQuery()); + + final Map lowercasedKeyNameValue = new HashMap<>(); + + for (final Entry entry : queryVariables.entrySet()) { + // sort the value and organize it as comma separated values + final List sortedValues = Arrays.asList(entry.getValue()); + Collections.sort(sortedValues); + + final StringBuilder stringValue = new StringBuilder(); + + for (final String value : sortedValues) { + if (stringValue.length() > 0) { + stringValue.append(AbfsHttpConstants.COMMA); + } + + stringValue.append(value); + } + + // key turns out to be null for ?a&b&c&d + lowercasedKeyNameValue.put((entry.getKey()) == null ? null + : entry.getKey().toLowerCase(Locale.ROOT), stringValue.toString()); + } + + final ArrayList sortedKeys = new ArrayList(lowercasedKeyNameValue.keySet()); + + Collections.sort(sortedKeys); + + for (final String key : sortedKeys) { + final StringBuilder queryParamString = new StringBuilder(); + + queryParamString.append(key); + queryParamString.append(":"); + queryParamString.append(lowercasedKeyNameValue.get(key)); + + appendCanonicalizedElement(canonicalizedResource, queryParamString.toString()); + } + + return canonicalizedResource.toString(); + } + + /** + * Gets all the values for the given header in the one to many map, + * performs a trimStart() on each return value. + * + * @param headers a one to many map of key / values representing the header values for the connection. + * @param headerName the name of the header to lookup + * @return an ArrayList of all trimmed values corresponding to the requested headerName. This may be empty + * if the header is not found. + */ + private static ArrayList getHeaderValues( + final Map> headers, + final String headerName) { + + final ArrayList arrayOfValues = new ArrayList(); + List values = null; + + for (final Entry> entry : headers.entrySet()) { + if (entry.getKey().toLowerCase(Locale.ROOT).equals(headerName)) { + values = entry.getValue(); + break; + } + } + if (values != null) { + for (final String value : values) { + // canonicalization formula requires the string to be left + // trimmed. + arrayOfValues.add(trimStart(value)); + } + } + return arrayOfValues; + } + + /** + * Parses a query string into a one to many hashmap. + * + * @param parseString the string to parse + * @return a HashMap of the key values. + */ + private static HashMap parseQueryString(String parseString) throws UnsupportedEncodingException { + final HashMap retVals = new HashMap<>(); + if (parseString == null || parseString.isEmpty()) { + return retVals; + } + + // 1. Remove ? if present + final int queryDex = parseString.indexOf(AbfsHttpConstants.QUESTION_MARK); + if (queryDex >= 0 && parseString.length() > 0) { + parseString = parseString.substring(queryDex + 1); + } + + // 2. split name value pairs by splitting on the 'c&' character + final String[] valuePairs = parseString.contains(AbfsHttpConstants.AND_MARK) + ? parseString.split(AbfsHttpConstants.AND_MARK) + : parseString.split(AbfsHttpConstants.SEMICOLON); + + // 3. for each field value pair parse into appropriate map entries + for (int m = 0; m < valuePairs.length; m++) { + final int equalDex = valuePairs[m].indexOf(AbfsHttpConstants.EQUAL); + + if (equalDex < 0 || equalDex == valuePairs[m].length() - 1) { + continue; + } + + String key = valuePairs[m].substring(0, equalDex); + String value = valuePairs[m].substring(equalDex + 1); + + key = safeDecode(key); + value = safeDecode(value); + + // 3.1 add to map + String[] values = retVals.get(key); + + if (values == null) { + values = new String[]{value}; + if (!value.equals("")) { + retVals.put(key, values); + } + } + } + + return retVals; + } + + /** + * Performs safe decoding of the specified string, taking care to preserve each + character, rather + * than replacing it with a space character. + * + * @param stringToDecode A String that represents the string to decode. + * @return A String that represents the decoded string. + *

+ * If a storage service error occurred. + */ + private static String safeDecode(final String stringToDecode) throws UnsupportedEncodingException { + if (stringToDecode == null) { + return null; + } + + if (stringToDecode.length() == 0) { + return ""; + } + + if (stringToDecode.contains(AbfsHttpConstants.PLUS)) { + final StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToDecode.length(); m++) { + if (stringToDecode.charAt(m) == '+') { + if (m > startDex) { + outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, m), + AbfsHttpConstants.UTF_8)); + } + + outBuilder.append(AbfsHttpConstants.PLUS); + startDex = m + 1; + } + } + + if (startDex != stringToDecode.length()) { + outBuilder.append(URLDecoder.decode(stringToDecode.substring(startDex, stringToDecode.length()), + AbfsHttpConstants.UTF_8)); + } + + return outBuilder.toString(); + } else { + return URLDecoder.decode(stringToDecode, AbfsHttpConstants.UTF_8); + } + } + + private static String trimStart(final String value) { + int spaceDex = 0; + while (spaceDex < value.length() && value.charAt(spaceDex) == ' ') { + spaceDex++; + } + + return value.substring(spaceDex); + } + + private static String getHeaderValue(final HttpURLConnection conn, final String headerName, final String defaultValue) { + final String headerValue = conn.getRequestProperty(headerName); + return headerValue == null ? defaultValue : headerValue; + } + + + /** + * Constructs a canonicalized string for signing a request. + * + * @param conn the HttpURLConnection to canonicalize + * @param accountName the account name associated with the request + * @param contentLength the length of the content written to the outputstream in bytes, + * -1 if unknown + * @return a canonicalized string. + */ + private String canonicalize(final HttpURLConnection conn, + final String accountName, + final Long contentLength) throws UnsupportedEncodingException { + + if (contentLength < -1) { + throw new IllegalArgumentException( + "The Content-Length header must be greater than or equal to -1."); + } + + String contentType = getHeaderValue(conn, HttpHeaderConfigurations.CONTENT_TYPE, ""); + + return canonicalizeHttpRequest(conn.getURL(), accountName, + conn.getRequestMethod(), contentType, contentLength, null, conn); + } + + /** + * Thread local for storing GMT date format. + */ + private static ThreadLocal rfc1123GmtDateTimeFormatter + = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + final DateFormat formatter = new SimpleDateFormat(RFC1123_PATTERN, Locale.ROOT); + formatter.setTimeZone(GMT_ZONE); + return formatter; + } + }; + + public static final TimeZone GMT_ZONE = TimeZone.getTimeZone(AbfsHttpConstants.GMT_TIMEZONE); + + + /** + * Returns the current GMT date/time String using the RFC1123 pattern. + * + * @return A String that represents the current GMT date/time using the RFC1123 pattern. + */ + static String getGMTTime() { + return getGMTTime(new Date()); + } + + /** + * Returns the GTM date/time String for the specified value using the RFC1123 pattern. + * + * @param date + * A Date object that represents the date to convert to GMT date/time in the RFC1123 + * pattern. + * + * @return A String that represents the GMT date/time for the specified value using the RFC1123 + * pattern. + */ + static String getGMTTime(final Date date) { + return rfc1123GmtDateTimeFormatter.get().format(date); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ShellDecryptionKeyProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ShellDecryptionKeyProvider.java new file mode 100644 index 00000000000..bdac922fb3a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/ShellDecryptionKeyProvider.java @@ -0,0 +1,71 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.KeyProviderException; +import org.apache.hadoop.util.Shell; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shell decryption key provider which invokes an external script that will + * perform the key decryption. + */ +public class ShellDecryptionKeyProvider extends SimpleKeyProvider { + private static final Logger LOG = LoggerFactory.getLogger(ShellDecryptionKeyProvider.class); + + @Override + public String getStorageAccountKey(String accountName, Configuration rawConfig) + throws KeyProviderException { + String envelope = super.getStorageAccountKey(accountName, rawConfig); + + AbfsConfiguration abfsConfig; + try { + abfsConfig = new AbfsConfiguration(rawConfig, accountName); + } catch(IllegalAccessException | IOException e) { + throw new KeyProviderException("Unable to get key from credential providers.", e); + } + + final String command = abfsConfig.get(ConfigurationKeys.AZURE_KEY_ACCOUNT_SHELLKEYPROVIDER_SCRIPT); + if (command == null) { + throw new KeyProviderException( + "Script path is not specified via fs.azure.shellkeyprovider.script"); + } + + String[] cmd = command.split(" "); + String[] cmdWithEnvelope = Arrays.copyOf(cmd, cmd.length + 1); + cmdWithEnvelope[cmdWithEnvelope.length - 1] = envelope; + + String decryptedKey = null; + try { + decryptedKey = Shell.execCommand(cmdWithEnvelope); + } catch (IOException ex) { + throw new KeyProviderException(ex); + } + + // trim any whitespace + return decryptedKey.trim(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SimpleKeyProvider.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SimpleKeyProvider.java new file mode 100644 index 00000000000..727e1b3fd3f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/SimpleKeyProvider.java @@ -0,0 +1,54 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.IOException; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.KeyProviderException; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Key provider that simply returns the storage account key from the + * configuration as plaintext. + */ +public class SimpleKeyProvider implements KeyProvider { + private static final Logger LOG = LoggerFactory.getLogger(SimpleKeyProvider.class); + + @Override + public String getStorageAccountKey(String accountName, Configuration rawConfig) + throws KeyProviderException { + String key = null; + + try { + AbfsConfiguration abfsConfig = new AbfsConfiguration(rawConfig, accountName); + key = abfsConfig.getPasswordString(ConfigurationKeys.FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME); + } catch(IllegalAccessException | InvalidConfigurationValueException e) { + throw new KeyProviderException("Failure to initialize configuration", e); + } catch(IOException ioe) { + LOG.warn("Unable to get key from credential providers. {}", ioe); + } + + return key; + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/package-info.java new file mode 100644 index 00000000000..97c1d71251f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/services/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.services; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/Base64.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/Base64.java new file mode 100644 index 00000000000..c1910060420 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/Base64.java @@ -0,0 +1,329 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +/** + * Base64 + */ +public final class Base64 { + /** + * The Base 64 Characters. + */ + private static final String BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + /** + * Decoded values, -1 is invalid character, -2 is = pad character. + */ + private static final byte[] DECODE_64 = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0-15 + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* + * 16- 31 + */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* + * 32- 47 + */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, /* + * 48- 63 + */ + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64-79 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* + * 80- 95 + */ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* + * 96- 111 + */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 /* + * 112- 127 + */ + }; + + /** + * Decodes a given Base64 string into its corresponding byte array. + * + * @param data + * the Base64 string, as a String object, to decode + * + * @return the corresponding decoded byte array + * @throws IllegalArgumentException + * If the string is not a valid base64 encoded string + */ + public static byte[] decode(final String data) { + if (data == null) { + throw new IllegalArgumentException("The data parameter is not a valid base64-encoded string."); + } + + int byteArrayLength = 3 * data.length() / 4; + + if (data.endsWith("==")) { + byteArrayLength -= 2; + } + else if (data.endsWith("=")) { + byteArrayLength -= 1; + } + + final byte[] retArray = new byte[byteArrayLength]; + int byteDex = 0; + int charDex = 0; + + for (; charDex < data.length(); charDex += 4) { + // get 4 chars, convert to 3 bytes + final int char1 = DECODE_64[(byte) data.charAt(charDex)]; + final int char2 = DECODE_64[(byte) data.charAt(charDex + 1)]; + final int char3 = DECODE_64[(byte) data.charAt(charDex + 2)]; + final int char4 = DECODE_64[(byte) data.charAt(charDex + 3)]; + + if (char1 < 0 || char2 < 0 || char3 == -1 || char4 == -1) { + // invalid character(-1), or bad padding (-2) + throw new IllegalArgumentException("The data parameter is not a valid base64-encoded string."); + } + + int tVal = char1 << 18; + tVal += char2 << 12; + tVal += (char3 & 0xff) << 6; + tVal += char4 & 0xff; + + if (char3 == -2) { + // two "==" pad chars, check bits 12-24 + tVal &= 0x00FFF000; + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + } + else if (char4 == -2) { + // one pad char "=" , check bits 6-24. + tVal &= 0x00FFFFC0; + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + retArray[byteDex++] = (byte) (tVal >> 8 & 0xFF); + + } + else { + // No pads take all 3 bytes, bits 0-24 + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + retArray[byteDex++] = (byte) (tVal >> 8 & 0xFF); + retArray[byteDex++] = (byte) (tVal & 0xFF); + } + } + return retArray; + } + + /** + * Decodes a given Base64 string into its corresponding byte array. + * + * @param data + * the Base64 string, as a String object, to decode + * + * @return the corresponding decoded byte array + * @throws IllegalArgumentException + * If the string is not a valid base64 encoded string + */ + public static Byte[] decodeAsByteObjectArray(final String data) { + int byteArrayLength = 3 * data.length() / 4; + + if (data.endsWith("==")) { + byteArrayLength -= 2; + } + else if (data.endsWith("=")) { + byteArrayLength -= 1; + } + + final Byte[] retArray = new Byte[byteArrayLength]; + int byteDex = 0; + int charDex = 0; + + for (; charDex < data.length(); charDex += 4) { + // get 4 chars, convert to 3 bytes + final int char1 = DECODE_64[(byte) data.charAt(charDex)]; + final int char2 = DECODE_64[(byte) data.charAt(charDex + 1)]; + final int char3 = DECODE_64[(byte) data.charAt(charDex + 2)]; + final int char4 = DECODE_64[(byte) data.charAt(charDex + 3)]; + + if (char1 < 0 || char2 < 0 || char3 == -1 || char4 == -1) { + // invalid character(-1), or bad padding (-2) + throw new IllegalArgumentException("The data parameter is not a valid base64-encoded string."); + } + + int tVal = char1 << 18; + tVal += char2 << 12; + tVal += (char3 & 0xff) << 6; + tVal += char4 & 0xff; + + if (char3 == -2) { + // two "==" pad chars, check bits 12-24 + tVal &= 0x00FFF000; + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + } + else if (char4 == -2) { + // one pad char "=" , check bits 6-24. + tVal &= 0x00FFFFC0; + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + retArray[byteDex++] = (byte) (tVal >> 8 & 0xFF); + + } + else { + // No pads take all 3 bytes, bits 0-24 + retArray[byteDex++] = (byte) (tVal >> 16 & 0xFF); + retArray[byteDex++] = (byte) (tVal >> 8 & 0xFF); + retArray[byteDex++] = (byte) (tVal & 0xFF); + } + } + return retArray; + } + + /** + * Encodes a byte array as a Base64 string. + * + * @param data + * the byte array to encode + * @return the Base64-encoded string, as a String object + */ + public static String encode(final byte[] data) { + final StringBuilder builder = new StringBuilder(); + final int dataRemainder = data.length % 3; + + int j = 0; + int n = 0; + for (; j < data.length; j += 3) { + + if (j < data.length - dataRemainder) { + n = ((data[j] & 0xFF) << 16) + ((data[j + 1] & 0xFF) << 8) + (data[j + 2] & 0xFF); + } + else { + if (dataRemainder == 1) { + n = (data[j] & 0xFF) << 16; + } + else if (dataRemainder == 2) { + n = ((data[j] & 0xFF) << 16) + ((data[j + 1] & 0xFF) << 8); + } + } + + // Left here for readability + // byte char1 = (byte) ((n >>> 18) & 0x3F); + // byte char2 = (byte) ((n >>> 12) & 0x3F); + // byte char3 = (byte) ((n >>> 6) & 0x3F); + // byte char4 = (byte) (n & 0x3F); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 18) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 12) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 6) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) (n & 0x3F))); + } + + final int bLength = builder.length(); + + // append '=' to pad + if (data.length % 3 == 1) { + builder.replace(bLength - 2, bLength, "=="); + } + else if (data.length % 3 == 2) { + builder.replace(bLength - 1, bLength, "="); + } + + return builder.toString(); + } + + /** + * Encodes a byte array as a Base64 string. + * + * @param data + * the byte array to encode + * @return the Base64-encoded string, as a String object + */ + public static String encode(final Byte[] data) { + final StringBuilder builder = new StringBuilder(); + final int dataRemainder = data.length % 3; + + int j = 0; + int n = 0; + for (; j < data.length; j += 3) { + + if (j < data.length - dataRemainder) { + n = ((data[j] & 0xFF) << 16) + ((data[j + 1] & 0xFF) << 8) + (data[j + 2] & 0xFF); + } + else { + if (dataRemainder == 1) { + n = (data[j] & 0xFF) << 16; + } + else if (dataRemainder == 2) { + n = ((data[j] & 0xFF) << 16) + ((data[j + 1] & 0xFF) << 8); + } + } + + // Left here for readability + // byte char1 = (byte) ((n >>> 18) & 0x3F); + // byte char2 = (byte) ((n >>> 12) & 0x3F); + // byte char3 = (byte) ((n >>> 6) & 0x3F); + // byte char4 = (byte) (n & 0x3F); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 18) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 12) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) ((n >>> 6) & 0x3F))); + builder.append(BASE_64_CHARS.charAt((byte) (n & 0x3F))); + } + + final int bLength = builder.length(); + + // append '=' to pad + if (data.length % 3 == 1) { + builder.replace(bLength - 2, bLength, "=="); + } + else if (data.length % 3 == 2) { + builder.replace(bLength - 1, bLength, "="); + } + + return builder.toString(); + } + + /** + * Determines whether the given string contains only Base64 characters. + * + * @param data + * the string, as a String object, to validate + * @return true if data is a valid Base64 string, otherwise false + */ + public static boolean validateIsBase64String(final String data) { + + if (data == null || data.length() % 4 != 0) { + return false; + } + + for (int m = 0; m < data.length(); m++) { + final byte charByte = (byte) data.charAt(m); + + // pad char detected + if (DECODE_64[charByte] == -2) { + if (m < data.length() - 2) { + return false; + } + else if (m == data.length() - 2 && DECODE_64[(byte) data.charAt(m + 1)] != -2) { + return false; + } + } + + if (charByte < 0 || DECODE_64[charByte] == -1) { + return false; + } + } + + return true; + } + + /** + * Private Default Ctor. + */ + private Base64() { + // No op + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/SSLSocketFactoryEx.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/SSLSocketFactoryEx.java new file mode 100644 index 00000000000..00e7786fa4a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/SSLSocketFactoryEx.java @@ -0,0 +1,240 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wildfly.openssl.OpenSSLProvider; + + +/** + * Extension to use native OpenSSL library instead of JSSE for better + * performance. + * + */ +public final class SSLSocketFactoryEx extends SSLSocketFactory { + + /** + * Default indicates Ordered, preferred OpenSSL, if failed to load then fall + * back to Default_JSSE + */ + public enum SSLChannelMode { + OpenSSL, + Default, + Default_JSSE + } + + private static SSLSocketFactoryEx instance = null; + private static final Logger LOG = LoggerFactory.getLogger( + SSLSocketFactoryEx.class); + private String providerName; + private SSLContext ctx; + private String[] ciphers; + private SSLChannelMode channelMode; + + /** + * Initialize a singleton SSL socket factory. + * + * @param preferredMode applicable only if the instance is not initialized. + * @throws IOException if an error occurs. + */ + public static synchronized void initializeDefaultFactory( + SSLChannelMode preferredMode) throws IOException { + if (instance == null) { + instance = new SSLSocketFactoryEx(preferredMode); + } + } + + /** + * Singletone instance of the SSLSocketFactory. + * + * SSLSocketFactory must be initialized with appropriate SSLChannelMode + * using initializeDefaultFactory method. + * + * @return instance of the SSLSocketFactory, instance must be initialized by + * initializeDefaultFactory. + */ + public static SSLSocketFactoryEx getDefaultFactory() { + return instance; + } + + static { + OpenSSLProvider.register(); + } + + private SSLSocketFactoryEx(SSLChannelMode preferredChannelMode) + throws IOException { + try { + initializeSSLContext(preferredChannelMode); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } catch (KeyManagementException e) { + throw new IOException(e); + } + + // Get list of supported cipher suits from the SSL factory. + SSLSocketFactory factory = ctx.getSocketFactory(); + String[] defaultCiphers = factory.getSupportedCipherSuites(); + String version = System.getProperty("java.version"); + + ciphers = (channelMode == SSLChannelMode.Default_JSSE + && version.startsWith("1.8")) + ? alterCipherList(defaultCiphers) : defaultCiphers; + + providerName = ctx.getProvider().getName() + "-" + + ctx.getProvider().getVersion(); + } + + private void initializeSSLContext(SSLChannelMode preferredChannelMode) + throws NoSuchAlgorithmException, KeyManagementException { + switch (preferredChannelMode) { + case Default: + try { + ctx = SSLContext.getInstance("openssl.TLS"); + ctx.init(null, null, null); + channelMode = SSLChannelMode.OpenSSL; + } catch (NoSuchAlgorithmException e) { + LOG.warn("Failed to load OpenSSL. Falling back to the JSSE default."); + ctx = SSLContext.getDefault(); + channelMode = SSLChannelMode.Default_JSSE; + } + break; + case OpenSSL: + ctx = SSLContext.getInstance("openssl.TLS"); + ctx.init(null, null, null); + channelMode = SSLChannelMode.OpenSSL; + break; + case Default_JSSE: + ctx = SSLContext.getDefault(); + channelMode = SSLChannelMode.Default_JSSE; + break; + default: + throw new AssertionError("Unknown channel mode: " + + preferredChannelMode); + } + } + + public String getProviderName() { + return providerName; + } + + @Override + public String[] getDefaultCipherSuites() { + return ciphers.clone(); + } + + @Override + public String[] getSupportedCipherSuites() { + return ciphers.clone(); + } + + public Socket createSocket() throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory.createSocket(); + configureSocket(ss); + return ss; + } + + @Override + public Socket createSocket(Socket s, String host, int port, + boolean autoClose) throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory.createSocket(s, host, port, autoClose); + + configureSocket(ss); + return ss; + } + + @Override + public Socket createSocket(InetAddress address, int port, + InetAddress localAddress, int localPort) + throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory + .createSocket(address, port, localAddress, localPort); + + configureSocket(ss); + return ss; + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, + int localPort) throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory + .createSocket(host, port, localHost, localPort); + + configureSocket(ss); + + return ss; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory.createSocket(host, port); + + configureSocket(ss); + + return ss; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + SSLSocketFactory factory = ctx.getSocketFactory(); + SSLSocket ss = (SSLSocket) factory.createSocket(host, port); + + configureSocket(ss); + + return ss; + } + + private void configureSocket(SSLSocket ss) throws SocketException { + ss.setEnabledCipherSuites(ciphers); + } + + private String[] alterCipherList(String[] defaultCiphers) { + + ArrayList preferredSuits = new ArrayList<>(); + + // Remove GCM mode based ciphers from the supported list. + for (int i = 0; i < defaultCiphers.length; i++) { + if (defaultCiphers[i].contains("_GCM_")) { + LOG.debug("Removed Cipher - " + defaultCiphers[i]); + } else { + preferredSuits.add(defaultCiphers[i]); + } + } + + ciphers = preferredSuits.toArray(new String[0]); + return ciphers; + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/UriUtils.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/UriUtils.java new file mode 100644 index 00000000000..1bbc1b39e16 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/UriUtils.java @@ -0,0 +1,78 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import java.util.regex.Pattern; + +/** + * Utility class to help with Abfs url transformation to blob urls. + */ +public final class UriUtils { + private static final String ABFS_URI_REGEX = "[^.]+\\.dfs\\.(preprod\\.){0,1}core\\.windows\\.net"; + private static final Pattern ABFS_URI_PATTERN = Pattern.compile(ABFS_URI_REGEX); + + /** + * Checks whether a string includes abfs url. + * @param string the string to check. + * @return true if string has abfs url. + */ + public static boolean containsAbfsUrl(final String string) { + if (string == null || string.isEmpty()) { + return false; + } + + return ABFS_URI_PATTERN.matcher(string).matches(); + } + + /** + * Extracts the account name from the host name. + * @param hostName the fully-qualified domain name of the storage service + * endpoint (e.g. {account}.dfs.core.windows.net. + * @return the storage service account name. + */ + public static String extractAccountNameFromHostName(final String hostName) { + if (hostName == null || hostName.isEmpty()) { + return null; + } + + if (!containsAbfsUrl(hostName)) { + return null; + } + + String[] splitByDot = hostName.split("\\."); + if (splitByDot.length == 0) { + return null; + } + + return splitByDot[0]; + } + + /** + * Generate unique test path for multiple user tests. + * + * @return root test path + */ + public static String generateUniqueTestPath() { + String testUniqueForkId = System.getProperty("test.unique.fork.id"); + return testUniqueForkId == null ? "/test" : "/" + testUniqueForkId + "/test"; + } + + private UriUtils() { + } +} diff --git a/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java new file mode 100644 index 00000000000..d8cc940da1b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/main/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.utils; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier b/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier index 7ec8216deb0..90169185863 100644 --- a/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier +++ b/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenIdentifier @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. +org.apache.hadoop.fs.azurebfs.security.AbfsDelegationTokenIdentifier org.apache.hadoop.fs.azure.security.WasbDelegationTokenIdentifier \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer b/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer index f9c590aad8d..d889534c73c 100644 --- a/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer +++ b/hadoop-tools/hadoop-azure/src/main/resources/META-INF/services/org.apache.hadoop.security.token.TokenRenewer @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. +org.apache.hadoop.fs.azurebfs.security.AbfsTokenRenewer org.apache.hadoop.fs.azure.security.WasbTokenRenewer \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md b/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md new file mode 100644 index 00000000000..db55e67b766 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md @@ -0,0 +1,82 @@ + + +# Hadoop Azure Support: ABFS — Azure Data Lake Storage Gen2 + + + +## Introduction + +The `hadoop-azure` module provides support for the Azure Data Lake Storage Gen2 +storage layer through the "abfs" connector + +To make it part of Apache Hadoop's default classpath, simply make sure that +`HADOOP_OPTIONAL_TOOLS` in `hadoop-env.sh` has `hadoop-azure` in the list. + +## Features + +* Read and write data stored in an Azure Blob Storage account. +* *Fully Consistent* view of the storage across all clients. +* Can read data written through the wasb: connector. +* Present a hierarchical file system view by implementing the standard Hadoop + [`FileSystem`](../api/org/apache/hadoop/fs/FileSystem.html) interface. +* Supports configuration of multiple Azure Blob Storage accounts. +* Can act as a source or destination of data in Hadoop MapReduce, Apache Hive, Apache Spark +* Tested at scale on both Linux and Windows. +* Can be used as a replacement for HDFS on Hadoop clusters deployed in Azure infrastructure. + + + +## Limitations + +* File last access time is not tracked. + + +## Technical notes + +### Security + +### Consistency and Concurrency + +*TODO*: complete/review + +The abfs client has a fully consistent view of the store, which has complete Create Read Update and Delete consistency for data and metadata. +(Compare and contrast with S3 which only offers Create consistency; S3Guard adds CRUD to metadata, but not the underlying data). + +### Performance + +*TODO*: check these. + +* File Rename: `O(1)`. +* Directory Rename: `O(files)`. +* Directory Delete: `O(files)`. + +## Configuring ABFS + +Any configuration can be specified generally (or as the default when accessing all accounts) or can be tied to s a specific account. +For example, an OAuth identity can be configured for use regardless of which account is accessed with the property +"fs.azure.account.oauth2.client.id" +or you can configure an identity to be used only for a specific storage account with +"fs.azure.account.oauth2.client.id.\.dfs.core.windows.net". + +Note that it doesn't make sense to do this with some properties, like shared keys that are inherently account-specific. + +## Testing ABFS + +See the relevant section in [Testing Azure](testing_azure.html). + +## References + +* [A closer look at Azure Data Lake Storage Gen2](https://azure.microsoft.com/en-gb/blog/a-closer-look-at-azure-data-lake-storage-gen2/); +MSDN Article from June 28, 2018. diff --git a/hadoop-tools/hadoop-azure/src/site/markdown/testing_azure.md b/hadoop-tools/hadoop-azure/src/site/markdown/testing_azure.md index b58e68be5f3..c2afe74e22e 100644 --- a/hadoop-tools/hadoop-azure/src/site/markdown/testing_azure.md +++ b/hadoop-tools/hadoop-azure/src/site/markdown/testing_azure.md @@ -90,7 +90,7 @@ For example: - fs.azure.test.account.name + fs.azure.wasb.account.name {ACCOUNTNAME}.blob.core.windows.net @@ -126,7 +126,7 @@ Overall, to run all the tests using `mvn test`, a sample `azure-auth-keys.xml` - fs.azure.test.account.name + fs.azure.wasb.account.name {ACCOUNTNAME}.blob.core.windows.net @@ -574,3 +574,174 @@ mvn test -Dtest=CleanupTestContainers This will delete the containers; the output log of the test run will provide the details and summary of the operation. + + +## Testing the Azure ABFS Client + +Azure Data Lake Storage Gen 2 (ADLS Gen 2) is a set of capabilities dedicated to +big data analytics, built on top of Azure Blob Storage. The ABFS and ABFSS +schemes target the ADLS Gen 2 REST API, and the WASB and WASBS schemes target +the Azure Blob Storage REST API. ADLS Gen 2 offers better performance and +scalability. ADLS Gen 2 also offers authentication and authorization compatible +with the Hadoop Distributed File System permissions model when hierarchical +namespace is enabled for the storage account. Furthermore, the metadata and data +produced by ADLS Gen 2 REST API can be consumed by Blob REST API, and vice versa. + +In order to test ABFS, please add the following configuration to your +`src/test/resources/azure-auth-keys.xml` file. Note that the ABFS tests include +compatibility tests which require WASB credentials, in addition to the ABFS +credentials. + +```xml + + + + + fs.azure.abfs.account.name + {ACCOUNT_NAME}.dfs.core.windows.net + + + + fs.azure.account.key.{ACCOUNT_NAME}.dfs.core.windows.net + {ACCOUNT_ACCESS_KEY} + + + + fs.azure.wasb.account.name + {ACCOUNT_NAME}.blob.core.windows.net + + + + fs.azure.account.key.{ACCOUNT_NAME}.blob.core.windows.net + {ACCOUNT_ACCESS_KEY} + + + + fs.contract.test.fs.abfs + abfs://{CONTAINER_NAME}@{ACCOUNT_NAME}.dfs.core.windows.net + A file system URI to be used by the contract tests. + + + + fs.contract.test.fs.wasb + wasb://{CONTAINER_NAME}@{ACCOUNT_NAME}.blob.core.windows.net + A file system URI to be used by the contract tests. + + +``` + +To run OAuth and ACL test cases you must use a storage account with the +hierarchical namespace enabled, and set the following configuration settings: + +```xml + + + + fs.azure.account.auth.type.{YOUR_ABFS_ACCOUNT_NAME} + {AUTH TYPE} + The authorization type can be SharedKey, OAuth, or Custom. The + default is SharedKey. + + + + + + + + + + + + + + + + + + + + + + + +``` + +If running tests against an endpoint that uses the URL format +http[s]://[ip]:[port]/[account]/[filesystem] instead of +http[s]://[account][domain-suffix]/[filesystem], please use the following: + +```xml + + fs.azure.abfs.endpoint + {IP}:{PORT} + +``` diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java index 5b36c8793ca..b65ce78fcbb 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/AzureBlobStorageTestAccount.java @@ -48,6 +48,7 @@ import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.DEFAULT_STORAGE_EMULATOR_ACCOUNT_NAME; import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.KEY_USE_LOCAL_SAS_KEY_MODE; import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.KEY_USE_SECURE_MODE; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.verifyWasbAccountNameInConfig; /** * Helper class to create WASB file systems backed by either a mock in-memory @@ -58,11 +59,14 @@ public final class AzureBlobStorageTestAccount implements AutoCloseable, private static final Logger LOG = LoggerFactory.getLogger( AzureBlobStorageTestAccount.class); - private static final String ACCOUNT_KEY_PROPERTY_NAME = "fs.azure.account.key."; private static final String SAS_PROPERTY_NAME = "fs.azure.sas."; private static final String TEST_CONFIGURATION_FILE_NAME = "azure-test.xml"; - private static final String TEST_ACCOUNT_NAME_PROPERTY_NAME = "fs.azure.test.account.name"; + public static final String ACCOUNT_KEY_PROPERTY_NAME = "fs.azure.account.key."; + public static final String TEST_ACCOUNT_NAME_PROPERTY_NAME = "fs.azure.account.name"; + public static final String WASB_TEST_ACCOUNT_NAME_WITH_DOMAIN = "fs.azure.wasb.account.name"; public static final String MOCK_ACCOUNT_NAME = "mockAccount.blob.core.windows.net"; + public static final String WASB_ACCOUNT_NAME_DOMAIN_SUFFIX = ".blob.core.windows.net"; + public static final String WASB_ACCOUNT_NAME_DOMAIN_SUFFIX_REGEX = "\\.blob(\\.preprod)?\\.core\\.windows\\.net"; public static final String MOCK_CONTAINER_NAME = "mockContainer"; public static final String WASB_AUTHORITY_DELIMITER = "@"; public static final String WASB_SCHEME = "wasb"; @@ -379,7 +383,7 @@ public static AzureBlobStorageTestAccount createOutOfBandStore( containerName); container.create(); - String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + String accountName = verifyWasbAccountNameInConfig(conf); // Ensure that custom throttling is disabled and tolerate concurrent // out-of-band appends. @@ -525,7 +529,7 @@ public static CloudStorageAccount createTestAccount() static CloudStorageAccount createTestAccount(Configuration conf) throws URISyntaxException, KeyProviderException { - String testAccountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + String testAccountName = verifyWasbAccountNameInConfig(conf); if (testAccountName == null) { LOG.warn("Skipping live Azure test because of missing test account"); return null; @@ -570,16 +574,16 @@ public static AzureBlobStorageTestAccount create( String containerName = useContainerSuffixAsContainerName ? containerNameSuffix : String.format( - "wasbtests-%s-%tQ%s", + "wasbtests-%s-%s%s", System.getProperty("user.name"), - new Date(), + UUID.randomUUID().toString(), containerNameSuffix); container = account.createCloudBlobClient().getContainerReference( containerName); if (createOptions.contains(CreateOptions.CreateContainer)) { container.createIfNotExists(); } - String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + String accountName = verifyWasbAccountNameInConfig(conf); if (createOptions.contains(CreateOptions.UseSas)) { String sas = generateSAS(container, createOptions.contains(CreateOptions.Readonly)); @@ -741,7 +745,7 @@ public static AzureBlobStorageTestAccount createAnonymous( CloudBlobClient blobClient = account.createCloudBlobClient(); // Capture the account URL and the account name. - String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + String accountName = verifyWasbAccountNameInConfig(conf); configureSecureModeTestSettings(conf); @@ -814,7 +818,7 @@ public static AzureBlobStorageTestAccount createRoot(final String blobName, CloudBlobClient blobClient = account.createCloudBlobClient(); // Capture the account URL and the account name. - String accountName = conf.get(TEST_ACCOUNT_NAME_PROPERTY_NAME); + String accountName = verifyWasbAccountNameInConfig(conf); configureSecureModeTestSettings(conf); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestFileSystemOperationExceptionMessage.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestFileSystemOperationExceptionMessage.java index 6d5e72e57b7..af570bdbea1 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestFileSystemOperationExceptionMessage.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestFileSystemOperationExceptionMessage.java @@ -29,6 +29,7 @@ import org.junit.Test; import static org.apache.hadoop.fs.azure.AzureNativeFileSystemStore.NO_ACCESS_TO_CONTAINER_MSG; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.verifyWasbAccountNameInConfig; /** * Test for error messages coming from SDK. @@ -46,7 +47,7 @@ public void testAnonymouseCredentialExceptionMessage() throws Throwable { AzureBlobStorageTestAccount.createTestAccount(conf); AzureTestUtils.assume("No test account", account != null); - String testStorageAccount = conf.get("fs.azure.test.account.name"); + String testStorageAccount = verifyWasbAccountNameInConfig(conf); conf = new Configuration(); conf.set("fs.AbstractFileSystem.wasb.impl", "org.apache.hadoop.fs.azure.Wasb"); diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestNativeFileSystemStatistics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestNativeFileSystemStatistics.java new file mode 100644 index 00000000000..447f65f2bd1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestNativeFileSystemStatistics.java @@ -0,0 +1,99 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azure; + +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +import static org.junit.Assume.assumeNotNull; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.cleanupTestAccount; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.readStringFromFile; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.writeStringToFile; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +/** + * Because FileSystem.Statistics is per FileSystem, so statistics can not be ran in + * parallel, hence in this test file, force them to run in sequential. + */ +public class ITestNativeFileSystemStatistics extends AbstractWasbTestWithTimeout{ + + @Test + public void test_001_NativeAzureFileSystemMocked() throws Exception { + AzureBlobStorageTestAccount testAccount = AzureBlobStorageTestAccount.createMock(); + assumeNotNull(testAccount); + testStatisticsWithAccount(testAccount); + } + + @Test + public void test_002_NativeAzureFileSystemPageBlobLive() throws Exception { + Configuration conf = new Configuration(); + // Configure the page blob directories key so every file created is a page blob. + conf.set(AzureNativeFileSystemStore.KEY_PAGE_BLOB_DIRECTORIES, "/"); + + // Configure the atomic rename directories key so every folder will have + // atomic rename applied. + conf.set(AzureNativeFileSystemStore.KEY_ATOMIC_RENAME_DIRECTORIES, "/"); + AzureBlobStorageTestAccount testAccount = AzureBlobStorageTestAccount.create(conf); + assumeNotNull(testAccount); + testStatisticsWithAccount(testAccount); + } + + @Test + public void test_003_NativeAzureFileSystem() throws Exception { + AzureBlobStorageTestAccount testAccount = AzureBlobStorageTestAccount.create(); + assumeNotNull(testAccount); + testStatisticsWithAccount(testAccount); + } + + private void testStatisticsWithAccount(AzureBlobStorageTestAccount testAccount) throws Exception { + assumeNotNull(testAccount); + NativeAzureFileSystem fs = testAccount.getFileSystem(); + testStatistics(fs); + cleanupTestAccount(testAccount); + } + + /** + * When tests are ran in parallel, this tests will fail because + * FileSystem.Statistics is per FileSystem class. + */ + @SuppressWarnings("deprecation") + private void testStatistics(NativeAzureFileSystem fs) throws Exception { + FileSystem.clearStatistics(); + FileSystem.Statistics stats = FileSystem.getStatistics("wasb", + NativeAzureFileSystem.class); + assertEquals(0, stats.getBytesRead()); + assertEquals(0, stats.getBytesWritten()); + Path newFile = new Path("testStats"); + writeStringToFile(fs, newFile, "12345678"); + assertEquals(8, stats.getBytesWritten()); + assertEquals(0, stats.getBytesRead()); + String readBack = readStringFromFile(fs, newFile); + assertEquals("12345678", readBack); + assertEquals(8, stats.getBytesRead()); + assertEquals(8, stats.getBytesWritten()); + assertTrue(fs.delete(newFile, true)); + assertEquals(8, stats.getBytesRead()); + assertEquals(8, stats.getBytesWritten()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestWasbUriAndConfiguration.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestWasbUriAndConfiguration.java index bee02206d60..7783684e9d2 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestWasbUriAndConfiguration.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/ITestWasbUriAndConfiguration.java @@ -499,32 +499,6 @@ public void testAbstractFileSystemImplementationForWasbsScheme() throws Exceptio } } - @Test - public void testNoAbstractFileSystemImplementationSpecifiedForWasbsScheme() throws Exception { - try { - testAccount = AzureBlobStorageTestAccount.createMock(); - Configuration conf = testAccount.getFileSystem().getConf(); - String authority = testAccount.getFileSystem().getUri().getAuthority(); - URI defaultUri = new URI("wasbs", authority, null, null, null); - conf.set(FS_DEFAULT_NAME_KEY, defaultUri.toString()); - - FileSystem fs = FileSystem.get(conf); - assertTrue(fs instanceof NativeAzureFileSystem); - assertEquals("wasbs", fs.getScheme()); - - // should throw if 'fs.AbstractFileSystem.wasbs.impl'' is not specified - try{ - FileContext.getFileContext(conf).getDefaultFileSystem(); - fail("Should've thrown."); - }catch(UnsupportedFileSystemException e){ - } - - } finally { - testAccount.cleanup(); - FileSystem.closeAll(); - } - } - @Test public void testCredentialProviderPathExclusions() throws Exception { String providerPath = diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/NativeAzureFileSystemBaseTest.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/NativeAzureFileSystemBaseTest.java index 726b5049b4c..19d370ebc99 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/NativeAzureFileSystemBaseTest.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/NativeAzureFileSystemBaseTest.java @@ -18,14 +18,10 @@ package org.apache.hadoop.fs.azure; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -51,6 +47,9 @@ import com.microsoft.azure.storage.StorageException; import com.microsoft.azure.storage.blob.CloudBlob; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.readStringFromFile; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.writeStringToFile; +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.writeStringToStream; import static org.apache.hadoop.test.GenericTestUtils.*; /* @@ -329,12 +328,12 @@ public void testCopyFromLocalFileSystem() throws Exception { FileSystem localFs = FileSystem.get(new Configuration()); localFs.delete(localFilePath, true); try { - writeString(localFs, localFilePath, "Testing"); + writeStringToFile(localFs, localFilePath, "Testing"); Path dstPath = methodPath(); assertTrue(FileUtil.copy(localFs, localFilePath, fs, dstPath, false, fs.getConf())); assertPathExists("coied from local", dstPath); - assertEquals("Testing", readString(fs, dstPath)); + assertEquals("Testing", readStringFromFile(fs, dstPath)); fs.delete(dstPath, true); } finally { localFs.delete(localFilePath, true); @@ -363,26 +362,6 @@ public void testListDirectory() throws Exception { assertTrue(fs.delete(rootFolder, true)); } - @Test - public void testStatistics() throws Exception { - FileSystem.clearStatistics(); - FileSystem.Statistics stats = FileSystem.getStatistics("wasb", - NativeAzureFileSystem.class); - assertEquals(0, stats.getBytesRead()); - assertEquals(0, stats.getBytesWritten()); - Path newFile = new Path("testStats"); - writeString(newFile, "12345678"); - assertEquals(8, stats.getBytesWritten()); - assertEquals(0, stats.getBytesRead()); - String readBack = readString(newFile); - assertEquals("12345678", readBack); - assertEquals(8, stats.getBytesRead()); - assertEquals(8, stats.getBytesWritten()); - assertTrue(fs.delete(newFile, true)); - assertEquals(8, stats.getBytesRead()); - assertEquals(8, stats.getBytesWritten()); - } - @Test public void testUriEncoding() throws Exception { fs.create(new Path("p/t%5Fe")).close(); @@ -767,7 +746,7 @@ public void testRedoRenameFolder() throws IOException { Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); - writeString(out, renameDescription); + writeStringToStream(out, renameDescription); // Redo the rename operation based on the contents of the -RenamePending.json file. // Trigger the redo by checking for existence of the original folder. It must appear @@ -831,7 +810,7 @@ public void testRedoRenameFolderInFolderListing() throws IOException { Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); - writeString(out, pending.makeRenamePendingFileContents()); + writeStringToStream(out, pending.makeRenamePendingFileContents()); // Redo the rename operation based on the contents of the // -RenamePending.json file. Trigger the redo by checking for existence of @@ -886,7 +865,7 @@ public void testRedoRenameFolderRenameInProgress() throws IOException { Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); - writeString(out, pending.makeRenamePendingFileContents()); + writeStringToStream(out, pending.makeRenamePendingFileContents()); // Rename inner folder to simulate the scenario where rename has started and // only one directory has been renamed but not the files under it @@ -1000,7 +979,7 @@ public void testRenameRedoFolderAlreadyDone() throws IOException { Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); - writeString(out, pending.makeRenamePendingFileContents()); + writeStringToStream(out, pending.makeRenamePendingFileContents()); try { pending.redo(); @@ -1228,7 +1207,7 @@ public void makeRenamePending(FileFolder dst) throws IOException { Path renamePendingFile = new Path(renamePendingStr); FSDataOutputStream out = fs.create(renamePendingFile, true); assertTrue(out != null); - writeString(out, renameDescription); + writeStringToStream(out, renameDescription); } // set whether a child is present or not @@ -1488,7 +1467,7 @@ private void testModifiedTime(Path testPath) throws Exception { Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); long currentUtcTime = utc.getTime().getTime(); FileStatus fileStatus = fs.getFileStatus(testPath); - final long errorMargin = 10 * 1000; // Give it +/-10 seconds + final long errorMargin = 60 * 1000; // Give it +/-60 seconds assertTrue("Modification time " + new Date(fileStatus.getModificationTime()) + " is not close to now: " + utc.getTime(), @@ -1504,45 +1483,12 @@ private void createEmptyFile(Path testFile, FsPermission permission) } private String readString(Path testFile) throws IOException { - return readString(fs, testFile); + return readStringFromFile(fs, testFile); } - private String readString(FileSystem fs, Path testFile) throws IOException { - FSDataInputStream inputStream = fs.open(testFile); - String ret = readString(inputStream); - inputStream.close(); - return ret; - } - - private String readString(FSDataInputStream inputStream) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader( - inputStream)); - final int BUFFER_SIZE = 1024; - char[] buffer = new char[BUFFER_SIZE]; - int count = reader.read(buffer, 0, BUFFER_SIZE); - if (count > BUFFER_SIZE) { - throw new IOException("Exceeded buffer size"); - } - inputStream.close(); - return new String(buffer, 0, count); - } private void writeString(Path path, String value) throws IOException { - writeString(fs, path, value); - } - - private void writeString(FileSystem fs, Path path, String value) - throws IOException { - FSDataOutputStream outputStream = fs.create(path, true); - writeString(outputStream, value); - } - - private void writeString(FSDataOutputStream outputStream, String value) - throws IOException { - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( - outputStream)); - writer.write(value); - writer.close(); + writeStringToFile(fs, path, value); } @Test diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestConstants.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestConstants.java index 0b72f069410..82907c57475 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestConstants.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestConstants.java @@ -143,12 +143,10 @@ public interface AzureTestConstants { - String ACCOUNT_KEY_PROPERTY_NAME - = "fs.azure.account.key."; + String ACCOUNT_KEY_PROPERTY_NAME = "fs.azure.account.key."; + String ACCOUNT_NAME_PROPERTY_NAME = "fs.azure.account.name"; String SAS_PROPERTY_NAME = "fs.azure.sas."; String TEST_CONFIGURATION_FILE_NAME = "azure-test.xml"; - String TEST_ACCOUNT_NAME_PROPERTY_NAME - = "fs.azure.test.account.name"; String MOCK_ACCOUNT_NAME = "mockAccount.blob.core.windows.net"; String MOCK_CONTAINER_NAME = "mockContainer"; diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestUtils.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestUtils.java index 8d2a104eb47..c46320a4835 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestUtils.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/integration/AzureTestUtils.java @@ -18,7 +18,11 @@ package org.apache.hadoop.fs.azure.integration; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.net.URI; import java.util.List; @@ -30,12 +34,18 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileContext; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.azure.AzureBlobStorageTestAccount; import org.apache.hadoop.fs.azure.NativeAzureFileSystem; +import static org.junit.Assume.assumeTrue; + +import static org.apache.hadoop.fs.azure.AzureBlobStorageTestAccount.WASB_ACCOUNT_NAME_DOMAIN_SUFFIX_REGEX; +import static org.apache.hadoop.fs.azure.AzureBlobStorageTestAccount.WASB_TEST_ACCOUNT_NAME_WITH_DOMAIN; import static org.apache.hadoop.fs.azure.integration.AzureTestConstants.*; import static org.apache.hadoop.test.MetricsAsserts.getLongCounter; import static org.apache.hadoop.test.MetricsAsserts.getLongGauge; @@ -476,4 +486,63 @@ public static void assumeScaleTestsEnabled(Configuration conf) { + KEY_SCALE_TESTS_ENABLED, enabled); } + + /** + * Check the account name for WASB tests is set correctly and return. + */ + public static String verifyWasbAccountNameInConfig(Configuration conf) { + String accountName = conf.get(ACCOUNT_NAME_PROPERTY_NAME); + if (accountName == null) { + accountName = conf.get(WASB_TEST_ACCOUNT_NAME_WITH_DOMAIN); + } + assumeTrue("Account for WASB is missing or it is not in correct format", + accountName != null && !accountName.endsWith(WASB_ACCOUNT_NAME_DOMAIN_SUFFIX_REGEX)); + return accountName; + } + + /** + * Write string into a file. + */ + public static void writeStringToFile(FileSystem fs, Path path, String value) + throws IOException { + FSDataOutputStream outputStream = fs.create(path, true); + writeStringToStream(outputStream, value); + } + + /** + * Write string into a file. + */ + public static void writeStringToStream(FSDataOutputStream outputStream, String value) + throws IOException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( + outputStream)); + writer.write(value); + writer.close(); + } + + /** + * Read string from a file. + */ + public static String readStringFromFile(FileSystem fs, Path testFile) throws IOException { + FSDataInputStream inputStream = fs.open(testFile); + String ret = readStringFromStream(inputStream); + inputStream.close(); + return ret; + } + + /** + * Read string from stream. + */ + public static String readStringFromStream(FSDataInputStream inputStream) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader( + inputStream)); + final int BUFFER_SIZE = 1024; + char[] buffer = new char[BUFFER_SIZE]; + int count = reader.read(buffer, 0, BUFFER_SIZE); + if (count > BUFFER_SIZE) { + throw new IOException("Exceeded buffer size"); + } + inputStream.close(); + return new String(buffer, 0, count); + } } diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/metrics/TestRollingWindowAverage.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/metrics/TestRollingWindowAverage.java index 9b1fb8e60e1..cd8b6927a04 100644 --- a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/metrics/TestRollingWindowAverage.java +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azure/metrics/TestRollingWindowAverage.java @@ -18,8 +18,8 @@ package org.apache.hadoop.fs.azure.metrics; -import static org.junit.Assert.*; -import org.junit.*; +import static org.junit.Assert.assertEquals; +import org.junit.Test; public class TestRollingWindowAverage { /** diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsIntegrationTest.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsIntegrationTest.java new file mode 100644 index 00000000000..52185cdc9af --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsIntegrationTest.java @@ -0,0 +1,340 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.net.URI; +import java.util.Hashtable; +import java.util.UUID; +import java.util.concurrent.Callable; + +import com.google.common.base.Preconditions; +import org.junit.After; +import org.junit.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.apache.hadoop.fs.azure.AzureNativeFileSystemStore; +import org.apache.hadoop.fs.azure.NativeAzureFileSystem; +import org.apache.hadoop.fs.azure.metrics.AzureFileSystemInstrumentation; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.utils.UriUtils; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.apache.hadoop.io.IOUtils; + +import static org.apache.hadoop.fs.azure.AzureBlobStorageTestAccount.WASB_ACCOUNT_NAME_DOMAIN_SUFFIX; +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.*; +import static org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode.FILE_SYSTEM_NOT_FOUND; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.*; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; +import static org.junit.Assume.assumeTrue; + +/** + * Base for AzureBlobFileSystem Integration tests. + * + * Important: This is for integration tests only. + */ +public abstract class AbstractAbfsIntegrationTest extends + AbstractAbfsTestWithTimeout { + + private static final Logger LOG = + LoggerFactory.getLogger(AbstractAbfsIntegrationTest.class); + + private boolean isIPAddress; + private NativeAzureFileSystem wasb; + private AzureBlobFileSystem abfs; + private String abfsScheme; + + private Configuration rawConfig; + private AbfsConfiguration abfsConfig; + private String fileSystemName; + private String accountName; + private String testUrl; + private AuthType authType; + + protected AbstractAbfsIntegrationTest() throws Exception { + fileSystemName = TEST_CONTAINER_PREFIX + UUID.randomUUID().toString(); + rawConfig = new Configuration(); + rawConfig.addResource(TEST_CONFIGURATION_FILE_NAME); + + this.accountName = rawConfig.get(FS_AZURE_ACCOUNT_NAME); + if (accountName == null) { + // check if accountName is set using different config key + accountName = rawConfig.get(FS_AZURE_ABFS_ACCOUNT_NAME); + } + assumeTrue("Not set: " + FS_AZURE_ABFS_ACCOUNT_NAME, + accountName != null && !accountName.isEmpty()); + + abfsConfig = new AbfsConfiguration(rawConfig, accountName); + + authType = abfsConfig.getEnum(FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME, AuthType.SharedKey); + abfsScheme = authType == AuthType.SharedKey ? FileSystemUriSchemes.ABFS_SCHEME + : FileSystemUriSchemes.ABFS_SECURE_SCHEME; + + if (authType == AuthType.SharedKey) { + assumeTrue("Not set: " + FS_AZURE_ACCOUNT_KEY, + abfsConfig.get(FS_AZURE_ACCOUNT_KEY) != null); + // Update credentials + } else { + assumeTrue("Not set: " + FS_AZURE_ACCOUNT_TOKEN_PROVIDER_TYPE_PROPERTY_NAME, + abfsConfig.get(FS_AZURE_ACCOUNT_TOKEN_PROVIDER_TYPE_PROPERTY_NAME) != null); + } + + final String abfsUrl = this.getFileSystemName() + "@" + this.getAccountName(); + URI defaultUri = null; + + try { + defaultUri = new URI(abfsScheme, abfsUrl, null, null, null); + } catch (Exception ex) { + throw new AssertionError(ex); + } + + this.testUrl = defaultUri.toString(); + abfsConfig.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, defaultUri.toString()); + abfsConfig.setBoolean(AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION, true); + // For testing purposes, an IP address and port may be provided to override + // the host specified in the FileSystem URI. Also note that the format of + // the Azure Storage Service URI changes from + // http[s]://[account][domain-suffix]/[filesystem] to + // http[s]://[ip]:[port]/[account]/[filesystem]. + String endPoint = abfsConfig.get(AZURE_ABFS_ENDPOINT); + if (endPoint != null && endPoint.contains(":") && endPoint.split(":").length == 2) { + this.isIPAddress = true; + } else { + this.isIPAddress = false; + } + } + + + @Before + public void setup() throws Exception { + //Create filesystem first to make sure getWasbFileSystem() can return an existing filesystem. + createFileSystem(); + + if (!isIPAddress && authType == AuthType.SharedKey) { + final URI wasbUri = new URI(abfsUrlToWasbUrl(getTestUrl())); + final AzureNativeFileSystemStore azureNativeFileSystemStore = + new AzureNativeFileSystemStore(); + + // update configuration with wasb credentials + String accountNameWithoutDomain = accountName.split("\\.")[0]; + String wasbAccountName = accountNameWithoutDomain + WASB_ACCOUNT_NAME_DOMAIN_SUFFIX; + String keyProperty = FS_AZURE_ACCOUNT_KEY + "." + wasbAccountName; + if (rawConfig.get(keyProperty) == null) { + rawConfig.set(keyProperty, getAccountKey()); + } + + azureNativeFileSystemStore.initialize( + wasbUri, + rawConfig, + new AzureFileSystemInstrumentation(rawConfig)); + + wasb = new NativeAzureFileSystem(azureNativeFileSystemStore); + wasb.initialize(wasbUri, rawConfig); + } + } + + @After + public void teardown() throws Exception { + try { + IOUtils.closeStream(wasb); + wasb = null; + + if (abfs == null) { + return; + } + + final AzureBlobFileSystemStore abfsStore = abfs.getAbfsStore(); + abfsStore.deleteFilesystem(); + + AbfsRestOperationException ex = intercept( + AbfsRestOperationException.class, + new Callable>() { + @Override + public Hashtable call() throws Exception { + return abfsStore.getFilesystemProperties(); + } + }); + if (FILE_SYSTEM_NOT_FOUND.getStatusCode() != ex.getStatusCode()) { + LOG.warn("Deleted test filesystem may still exist: {}", abfs, ex); + } + } catch (Exception e) { + LOG.warn("During cleanup: {}", e, e); + } finally { + IOUtils.closeStream(abfs); + abfs = null; + } + } + + public AzureBlobFileSystem getFileSystem() throws IOException { + return abfs; + } + + public AzureBlobFileSystem getFileSystem(Configuration configuration) throws Exception{ + final AzureBlobFileSystem fs = (AzureBlobFileSystem) FileSystem.get(configuration); + return fs; + } + + public AzureBlobFileSystem getFileSystem(String abfsUri) throws Exception { + abfsConfig.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, abfsUri); + final AzureBlobFileSystem fs = (AzureBlobFileSystem) FileSystem.get(rawConfig); + return fs; + } + + /** + * Creates the filesystem; updates the {@link #abfs} field. + * @return the created filesystem. + * @throws IOException failure during create/init. + */ + public AzureBlobFileSystem createFileSystem() throws IOException { + Preconditions.checkState(abfs == null, + "existing ABFS instance exists: %s", abfs); + abfs = (AzureBlobFileSystem) FileSystem.newInstance(rawConfig); + return abfs; + } + + + protected NativeAzureFileSystem getWasbFileSystem() { + return wasb; + } + + protected String getHostName() { + // READ FROM ENDPOINT, THIS IS CALLED ONLY WHEN TESTING AGAINST DEV-FABRIC + String endPoint = abfsConfig.get(AZURE_ABFS_ENDPOINT); + return endPoint.split(":")[0]; + } + + protected void setTestUrl(String testUrl) { + this.testUrl = testUrl; + } + + protected String getTestUrl() { + return testUrl; + } + + protected void setFileSystemName(String fileSystemName) { + this.fileSystemName = fileSystemName; + } + protected String getFileSystemName() { + return fileSystemName; + } + + protected String getAccountName() { + return this.accountName; + } + + protected String getAccountKey() { + return abfsConfig.get(FS_AZURE_ACCOUNT_KEY); + } + + public AbfsConfiguration getConfiguration() { + return abfsConfig; + } + + public Configuration getRawConfiguration() { + return abfsConfig.getRawConfiguration(); + } + + protected boolean isIPAddress() { + return isIPAddress; + } + + protected AuthType getAuthType() { + return this.authType; + } + + /** + * Write a buffer to a file. + * @param path path + * @param buffer buffer + * @throws IOException failure + */ + protected void write(Path path, byte[] buffer) throws IOException { + ContractTestUtils.writeDataset(getFileSystem(), path, buffer, buffer.length, + CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_DEFAULT, false); + } + + /** + * Touch a file in the test store. Will overwrite any existing file. + * @param path path + * @throws IOException failure. + */ + protected void touch(Path path) throws IOException { + ContractTestUtils.touch(getFileSystem(), path); + } + + protected static String wasbUrlToAbfsUrl(final String wasbUrl) { + return convertTestUrls( + wasbUrl, FileSystemUriSchemes.WASB_SCHEME, FileSystemUriSchemes.WASB_SECURE_SCHEME, FileSystemUriSchemes.WASB_DNS_PREFIX, + FileSystemUriSchemes.ABFS_SCHEME, FileSystemUriSchemes.ABFS_SECURE_SCHEME, FileSystemUriSchemes.ABFS_DNS_PREFIX); + } + + protected static String abfsUrlToWasbUrl(final String abfsUrl) { + return convertTestUrls( + abfsUrl, FileSystemUriSchemes.ABFS_SCHEME, FileSystemUriSchemes.ABFS_SECURE_SCHEME, FileSystemUriSchemes.ABFS_DNS_PREFIX, + FileSystemUriSchemes.WASB_SCHEME, FileSystemUriSchemes.WASB_SECURE_SCHEME, FileSystemUriSchemes.WASB_DNS_PREFIX); + } + + private static String convertTestUrls( + final String url, + final String fromNonSecureScheme, + final String fromSecureScheme, + final String fromDnsPrefix, + final String toNonSecureScheme, + final String toSecureScheme, + final String toDnsPrefix) { + String data = null; + if (url.startsWith(fromNonSecureScheme + "://")) { + data = url.replace(fromNonSecureScheme + "://", toNonSecureScheme + "://"); + } else if (url.startsWith(fromSecureScheme + "://")) { + data = url.replace(fromSecureScheme + "://", toSecureScheme + "://"); + } + + + if (data != null) { + data = data.replace("." + fromDnsPrefix + ".", + "." + toDnsPrefix + "."); + } + return data; + } + + public Path getTestPath() { + Path path = new Path(UriUtils.generateUniqueTestPath()); + return path; + } + + /** + * Create a path under the test path provided by + * {@link #getTestPath()}. + * @param filepath path string in + * @return a path qualified by the test filesystem + * @throws IOException IO problems + */ + protected Path path(String filepath) throws IOException { + return getFileSystem().makeQualified( + new Path(getTestPath(), filepath)); + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsScaleTest.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsScaleTest.java new file mode 100644 index 00000000000..14c9bff7bf8 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsScaleTest.java @@ -0,0 +1,59 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azure.integration.AzureTestConstants; + +import static org.apache.hadoop.fs.azure.integration.AzureTestUtils.assumeScaleTestsEnabled; + +/** + * Integration tests at bigger scale; configurable as to + * size, off by default. + */ +public class AbstractAbfsScaleTest extends AbstractAbfsIntegrationTest { + + protected static final Logger LOG = + LoggerFactory.getLogger(AbstractAbfsScaleTest.class); + + public AbstractAbfsScaleTest() throws Exception { + super(); + } + + @Override + protected int getTestTimeoutMillis() { + return AzureTestConstants.SCALE_TEST_TIMEOUT_MILLIS; + } + + @Override + public void setup() throws Exception { + super.setup(); + LOG.debug("Scale test operation count = {}", getOperationCount()); + Configuration rawConfiguration = getRawConfiguration(); + assumeScaleTestsEnabled(rawConfiguration); + } + + protected long getOperationCount() { + return getConfiguration().getLong(AzureTestConstants.KEY_OPERATION_COUNT, + AzureTestConstants.DEFAULT_OPERATION_COUNT); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsTestWithTimeout.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsTestWithTimeout.java new file mode 100644 index 00000000000..fee90abeabc --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/AbstractAbfsTestWithTimeout.java @@ -0,0 +1,70 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.junit.rules.Timeout; + +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.TEST_TIMEOUT; + +/** + * Base class for any ABFS test with timeouts & named threads. + * This class does not attempt to bind to Azure. + */ +public class AbstractAbfsTestWithTimeout extends Assert { + /** + * The name of the current method. + */ + @Rule + public TestName methodName = new TestName(); + /** + * Set the timeout for every test. + * This is driven by the value returned by {@link #getTestTimeoutMillis()}. + */ + @Rule + public Timeout testTimeout = new Timeout(getTestTimeoutMillis()); + + /** + * Name the junit thread for the class. This will overridden + * before the individual test methods are run. + */ + @BeforeClass + public static void nameTestThread() { + Thread.currentThread().setName("JUnit"); + } + + /** + * Name the thread to the current test method. + */ + @Before + public void nameThread() { + Thread.currentThread().setName("JUnit-" + methodName.getMethodName()); + } + + /** + * Override point: the test timeout in milliseconds. + * @return a timeout in milliseconds + */ + protected int getTestTimeoutMillis() { + return TEST_TIMEOUT; + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsClient.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsClient.java new file mode 100644 index 00000000000..f024f257b7d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsClient.java @@ -0,0 +1,49 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.services.AbfsClient; +import org.apache.hadoop.fs.azurebfs.services.AbfsRestOperation; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test continuation token which has equal sign. + */ +public final class ITestAbfsClient extends AbstractAbfsIntegrationTest { + private static final int LIST_MAX_RESULTS = 5000; + + public ITestAbfsClient() throws Exception { + super(); + } + + @Test + public void testContinuationTokenHavingEqualSign() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + AbfsClient abfsClient = fs.getAbfsClient(); + + try { + AbfsRestOperation op = abfsClient.listPath("/", true, LIST_MAX_RESULTS, "==========="); + Assert.assertTrue(false); + } catch (AbfsRestOperationException ex) { + Assert.assertEquals("InvalidQueryParameterValue", ex.getErrorCode().getErrorCode()); + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadWriteAndSeek.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadWriteAndSeek.java new file mode 100644 index 00000000000..a270a00e913 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAbfsReadWriteAndSeek.java @@ -0,0 +1,89 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.MAX_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.MIN_BUFFER_SIZE; + +/** + * Test read, write and seek. + * Uses package-private methods in AbfsConfiguration, which is why it is in + * this package. + */ +@RunWith(Parameterized.class) +public class ITestAbfsReadWriteAndSeek extends AbstractAbfsScaleTest { + private static final Path TEST_PATH = new Path("/testfile"); + + @Parameterized.Parameters(name = "Size={0}") + public static Iterable sizes() { + return Arrays.asList(new Object[][]{{MIN_BUFFER_SIZE}, + {DEFAULT_READ_BUFFER_SIZE}, + {MAX_BUFFER_SIZE}}); + } + + private final int size; + + public ITestAbfsReadWriteAndSeek(final int size) throws Exception { + this.size = size; + } + + @Test + public void testReadAndWriteWithDifferentBufferSizesAndSeek() throws Exception { + testReadWriteAndSeek(size); + } + + private void testReadWriteAndSeek(int bufferSize) throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final AbfsConfiguration abfsConfiguration = fs.getAbfsStore().getAbfsConfiguration(); + + abfsConfiguration.setWriteBufferSize(bufferSize); + abfsConfiguration.setReadBufferSize(bufferSize); + + + final byte[] b = new byte[2 * bufferSize]; + new Random().nextBytes(b); + try (FSDataOutputStream stream = fs.create(TEST_PATH)) { + stream.write(b); + } + + final byte[] readBuffer = new byte[2 * bufferSize]; + int result; + try (FSDataInputStream inputStream = fs.open(TEST_PATH)) { + inputStream.seek(bufferSize); + result = inputStream.read(readBuffer, bufferSize, bufferSize); + assertNotEquals(-1, result); + inputStream.seek(0); + result = inputStream.read(readBuffer, 0, bufferSize); + } + assertNotEquals("data read in final read()", -1, result); + assertArrayEquals(readBuffer, b); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemAppend.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemAppend.java new file mode 100644 index 00000000000..cbe19396d12 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemAppend.java @@ -0,0 +1,79 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.FileNotFoundException; +import java.util.Random; + +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; + +/** + * Test append operations. + */ +public class ITestAzureBlobFileSystemAppend extends + AbstractAbfsIntegrationTest { + private static final Path TEST_FILE_PATH = new Path("testfile"); + private static final Path TEST_FOLDER_PATH = new Path("testFolder"); + + public ITestAzureBlobFileSystemAppend() throws Exception { + super(); + } + + @Test(expected = FileNotFoundException.class) + public void testAppendDirShouldFail() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path filePath = TEST_FILE_PATH; + fs.mkdirs(filePath); + fs.append(filePath, 0); + } + + @Test + public void testAppendWithLength0() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + try(FSDataOutputStream stream = fs.create(TEST_FILE_PATH)) { + final byte[] b = new byte[1024]; + new Random().nextBytes(b); + stream.write(b, 1000, 0); + assertEquals(0, stream.getPos()); + } + } + + + @Test(expected = FileNotFoundException.class) + public void testAppendFileAfterDelete() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path filePath = TEST_FILE_PATH; + ContractTestUtils.touch(fs, filePath); + fs.delete(filePath, false); + + fs.append(filePath); + } + + @Test(expected = FileNotFoundException.class) + public void testAppendDirectory() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path folderPath = TEST_FOLDER_PATH; + fs.mkdirs(folderPath); + fs.append(folderPath); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemBackCompat.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemBackCompat.java new file mode 100644 index 00000000000..d8940f7d753 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemBackCompat.java @@ -0,0 +1,88 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.blob.CloudBlobClient; +import com.microsoft.azure.storage.blob.CloudBlobContainer; +import com.microsoft.azure.storage.blob.CloudBlockBlob; + +import org.junit.Assume; +import org.junit.Test; + +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +/** + * Test AzureBlobFileSystem back compatibility with WASB. + */ +public class ITestAzureBlobFileSystemBackCompat extends + AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemBackCompat() throws Exception { + super(); + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + } + + @Test + public void testBlobBackCompat() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(fs.getIsNamespaceEnabeld()); + String storageConnectionString = getBlobConnectionString(); + CloudStorageAccount storageAccount = CloudStorageAccount.parse(storageConnectionString); + CloudBlobClient blobClient = storageAccount.createCloudBlobClient(); + CloudBlobContainer container = blobClient.getContainerReference(this.getFileSystemName()); + container.createIfNotExists(); + + CloudBlockBlob blockBlob = container.getBlockBlobReference("test/10/10/10"); + blockBlob.uploadText(""); + + blockBlob = container.getBlockBlobReference("test/10/123/3/2/1/3"); + blockBlob.uploadText(""); + + FileStatus[] fileStatuses = fs.listStatus(new Path("/test/10/")); + assertEquals(2, fileStatuses.length); + assertEquals("10", fileStatuses[0].getPath().getName()); + assertTrue(fileStatuses[0].isDirectory()); + assertEquals(0, fileStatuses[0].getLen()); + assertEquals("123", fileStatuses[1].getPath().getName()); + assertTrue(fileStatuses[1].isDirectory()); + assertEquals(0, fileStatuses[1].getLen()); + } + + private String getBlobConnectionString() { + String connectionString; + if (isIPAddress()) { + connectionString = "DefaultEndpointsProtocol=http;BlobEndpoint=http://" + + this.getHostName() + ":8880/" + this.getAccountName().split("\\.") [0] + + ";AccountName=" + this.getAccountName().split("\\.")[0] + + ";AccountKey=" + this.getAccountKey(); + } + else { + connectionString = "DefaultEndpointsProtocol=http;BlobEndpoint=http://" + + this.getAccountName().replaceFirst("\\.dfs\\.", ".blob.") + + ";AccountName=" + this.getAccountName().split("\\.")[0] + + ";AccountKey=" + this.getAccountKey(); + } + + return connectionString; + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCopy.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCopy.java new file mode 100644 index 00000000000..917ee9ce1b0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCopy.java @@ -0,0 +1,96 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.FileUtil; +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsFile; + +/** + * Test copy operation. + */ +public class ITestAzureBlobFileSystemCopy extends AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemCopy() throws Exception { + super(); + } + + @Test + public void testCopyFromLocalFileSystem() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path localFilePath = new Path(System.getProperty("test.build.data", + "azure_test")); + FileSystem localFs = FileSystem.getLocal(new Configuration()); + localFs.delete(localFilePath, true); + try { + writeString(localFs, localFilePath, "Testing"); + Path dstPath = new Path("copiedFromLocal"); + assertTrue(FileUtil.copy(localFs, localFilePath, fs, dstPath, false, + fs.getConf())); + assertIsFile(fs, dstPath); + assertEquals("Testing", readString(fs, dstPath)); + fs.delete(dstPath, true); + } finally { + localFs.delete(localFilePath, true); + } + } + + private String readString(FileSystem fs, Path testFile) throws IOException { + return readString(fs.open(testFile)); + } + + private String readString(FSDataInputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + inputStream))) { + final int bufferSize = 1024; + char[] buffer = new char[bufferSize]; + int count = reader.read(buffer, 0, bufferSize); + if (count > bufferSize) { + throw new IOException("Exceeded buffer size"); + } + return new String(buffer, 0, count); + } + } + + private void writeString(FileSystem fs, Path path, String value) + throws IOException { + writeString(fs.create(path, true), value); + } + + private void writeString(FSDataOutputStream outputStream, String value) + throws IOException { + try(BufferedWriter writer = new BufferedWriter( + new OutputStreamWriter(outputStream))) { + writer.write(value); + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCreate.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCreate.java new file mode 100644 index 00000000000..ab01166b9b3 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemCreate.java @@ -0,0 +1,107 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.FileNotFoundException; +import java.util.EnumSet; + +import org.junit.Test; + +import org.apache.hadoop.fs.CreateFlag; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.permission.FsPermission; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsFile; + +/** + * Test create operation. + */ +public class ITestAzureBlobFileSystemCreate extends + AbstractAbfsIntegrationTest { + private static final Path TEST_FILE_PATH = new Path("testfile"); + private static final Path TEST_FOLDER_PATH = new Path("testFolder"); + private static final String TEST_CHILD_FILE = "childFile"; + + public ITestAzureBlobFileSystemCreate() throws Exception { + super(); + } + + @Test + public void testEnsureFileCreatedImmediately() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + FSDataOutputStream out = fs.create(TEST_FILE_PATH); + try { + assertIsFile(fs, TEST_FILE_PATH); + } finally { + out.close(); + } + assertIsFile(fs, TEST_FILE_PATH); + } + + @Test + @SuppressWarnings("deprecation") + public void testCreateNonRecursive() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path testFile = new Path(TEST_FOLDER_PATH, TEST_CHILD_FILE); + try { + fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null); + fail("Should've thrown"); + } catch (FileNotFoundException expected) { + } + fs.mkdirs(TEST_FOLDER_PATH); + fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null) + .close(); + assertIsFile(fs, testFile); + } + + @Test + @SuppressWarnings("deprecation") + public void testCreateNonRecursive1() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path testFile = new Path(TEST_FOLDER_PATH, TEST_CHILD_FILE); + try { + fs.createNonRecursive(testFile, FsPermission.getDefault(), EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE), 1024, (short) 1, 1024, null); + fail("Should've thrown"); + } catch (FileNotFoundException expected) { + } + fs.mkdirs(TEST_FOLDER_PATH); + fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null) + .close(); + assertIsFile(fs, testFile); + + } + + @Test + @SuppressWarnings("deprecation") + public void testCreateNonRecursive2() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + + Path testFile = new Path(TEST_FOLDER_PATH, TEST_CHILD_FILE); + try { + fs.createNonRecursive(testFile, FsPermission.getDefault(), false, 1024, (short) 1, 1024, null); + fail("Should've thrown"); + } catch (FileNotFoundException e) { + } + fs.mkdirs(TEST_FOLDER_PATH); + fs.createNonRecursive(testFile, true, 1024, (short) 1, 1024, null) + .close(); + assertIsFile(fs, testFile); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemDelete.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemDelete.java new file mode 100644 index 00000000000..486daca4f11 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemDelete.java @@ -0,0 +1,133 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Test; + +import org.apache.hadoop.fs.FileAlreadyExistsException; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertDeleted; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertPathDoesNotExist; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Test delete operation. + */ +public class ITestAzureBlobFileSystemDelete extends + AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemDelete() throws Exception { + super(); + } + + @Test + public void testDeleteRoot() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + + fs.mkdirs(new Path("/testFolder0")); + fs.mkdirs(new Path("/testFolder1")); + fs.mkdirs(new Path("/testFolder2")); + touch(new Path("/testFolder1/testfile")); + touch(new Path("/testFolder1/testfile2")); + touch(new Path("/testFolder1/testfile3")); + + Path root = new Path("/"); + FileStatus[] ls = fs.listStatus(root); + assertEquals(3, ls.length); + + fs.delete(root, true); + ls = fs.listStatus(root); + assertEquals("listing size", 0, ls.length); + } + + @Test() + public void testOpenFileAfterDelete() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path testfile = new Path("/testFile"); + touch(testfile); + assertDeleted(fs, testfile, false); + + intercept(FileNotFoundException.class, + () -> fs.open(testfile)); + } + + @Test + public void testEnsureFileIsDeleted() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path testfile = new Path("testfile"); + touch(testfile); + assertDeleted(fs, testfile, false); + assertPathDoesNotExist(fs, "deleted", testfile); + } + + @Test + public void testDeleteDirectory() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path dir = new Path("testfile"); + fs.mkdirs(dir); + fs.mkdirs(new Path("testfile/test1")); + fs.mkdirs(new Path("testfile/test1/test2")); + + assertDeleted(fs, dir, true); + assertPathDoesNotExist(fs, "deleted", dir); + } + + @Test + public void testDeleteFirstLevelDirectory() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final List> tasks = new ArrayList<>(); + + ExecutorService es = Executors.newFixedThreadPool(10); + for (int i = 0; i < 1000; i++) { + final Path fileName = new Path("/test/" + i); + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + touch(fileName); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + for (Future task : tasks) { + task.get(); + } + + es.shutdownNow(); + Path dir = new Path("/test"); + // first try a non-recursive delete, expect failure + intercept(FileAlreadyExistsException.class, + () -> fs.delete(dir, false)); + assertDeleted(fs, dir, true); + assertPathDoesNotExist(fs, "deleted", dir); + + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2E.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2E.java new file mode 100644 index 00000000000..2e994911acc --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2E.java @@ -0,0 +1,148 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertArrayEquals; + +/** + * Test end to end between ABFS client and ABFS server. + */ +public class ITestAzureBlobFileSystemE2E extends AbstractAbfsIntegrationTest { + private static final int TEST_BYTE = 100; + private static final int TEST_OFFSET = 100; + private static final int TEST_DEFAULT_BUFFER_SIZE = 4 * 1024 * 1024; + private static final int TEST_DEFAULT_READ_BUFFER_SIZE = 1023900; + + public ITestAzureBlobFileSystemE2E() throws Exception { + super(); + AbfsConfiguration configuration = this.getConfiguration(); + configuration.set(ConfigurationKeys.FS_AZURE_READ_AHEAD_QUEUE_DEPTH, "0"); + } + + @Test + public void testWriteOneByteToFile() throws Exception { + final Path testFilePath = new Path(methodName.getMethodName()); + testWriteOneByteToFile(testFilePath); + } + + @Test + public void testReadWriteBytesToFile() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = new Path(methodName.getMethodName()); + testWriteOneByteToFile(testFilePath); + try(FSDataInputStream inputStream = fs.open(testFilePath, + TEST_DEFAULT_BUFFER_SIZE)) { + assertEquals(TEST_BYTE, inputStream.read()); + } + } + + @Test (expected = IOException.class) + public void testOOBWrites() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + int readBufferSize = fs.getAbfsStore().getAbfsConfiguration().getReadBufferSize(); + + byte[] bytesToRead = new byte[readBufferSize]; + final byte[] b = new byte[2 * readBufferSize]; + new Random().nextBytes(b); + + final Path testFilePath = new Path(methodName.getMethodName()); + try(FSDataOutputStream writeStream = fs.create(testFilePath)) { + writeStream.write(b); + writeStream.flush(); + } + + try (FSDataInputStream readStream = fs.open(testFilePath)) { + assertEquals(readBufferSize, + readStream.read(bytesToRead, 0, readBufferSize)); + + try (FSDataOutputStream writeStream = fs.create(testFilePath)) { + writeStream.write(b); + writeStream.flush(); + } + + assertEquals(readBufferSize, + readStream.read(bytesToRead, 0, readBufferSize)); + } + } + + @Test + public void testWriteWithBufferOffset() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = new Path(methodName.getMethodName()); + + final byte[] b = new byte[1024 * 1000]; + new Random().nextBytes(b); + try (FSDataOutputStream stream = fs.create(testFilePath)) { + stream.write(b, TEST_OFFSET, b.length - TEST_OFFSET); + } + + final byte[] r = new byte[TEST_DEFAULT_READ_BUFFER_SIZE]; + FSDataInputStream inputStream = fs.open(testFilePath, TEST_DEFAULT_BUFFER_SIZE); + int result = inputStream.read(r); + + assertNotEquals(-1, result); + assertArrayEquals(r, Arrays.copyOfRange(b, TEST_OFFSET, b.length)); + + inputStream.close(); + } + + @Test + public void testReadWriteHeavyBytesToFileWithSmallerChunks() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = new Path(methodName.getMethodName()); + + final byte[] writeBuffer = new byte[5 * 1000 * 1024]; + new Random().nextBytes(writeBuffer); + write(testFilePath, writeBuffer); + + final byte[] readBuffer = new byte[5 * 1000 * 1024]; + FSDataInputStream inputStream = fs.open(testFilePath, TEST_DEFAULT_BUFFER_SIZE); + int offset = 0; + while (inputStream.read(readBuffer, offset, TEST_OFFSET) > 0) { + offset += TEST_OFFSET; + } + + assertArrayEquals(readBuffer, writeBuffer); + inputStream.close(); + } + + private void testWriteOneByteToFile(Path testFilePath) throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + try(FSDataOutputStream stream = fs.create(testFilePath)) { + stream.write(TEST_BYTE); + } + + FileStatus fileStatus = fs.getFileStatus(testFilePath); + assertEquals(1, fileStatus.getLen()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2EScale.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2EScale.java new file mode 100644 index 00000000000..fccd0632375 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemE2EScale.java @@ -0,0 +1,120 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +/** + * Test end to end between ABFS client and ABFS server with heavy traffic. + */ +public class ITestAzureBlobFileSystemE2EScale extends + AbstractAbfsScaleTest { + private static final int TEN = 10; + private static final int ONE_THOUSAND = 1000; + private static final int BASE_SIZE = 1024; + private static final int ONE_MB = 1024 * 1024; + private static final int DEFAULT_WRITE_TIMES = 100; + + public ITestAzureBlobFileSystemE2EScale() throws Exception { + } + + @Test + public void testWriteHeavyBytesToFileAcrossThreads() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFile = path(methodName.getMethodName()); + final FSDataOutputStream stream = fs.create(testFile); + ExecutorService es = Executors.newFixedThreadPool(TEN); + + int testWriteBufferSize = 2 * TEN * ONE_THOUSAND * BASE_SIZE; + final byte[] b = new byte[testWriteBufferSize]; + new Random().nextBytes(b); + List> tasks = new ArrayList<>(); + + int operationCount = DEFAULT_WRITE_TIMES; + for (int i = 0; i < operationCount; i++) { + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + stream.write(b); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + for (Future task : tasks) { + task.get(); + } + + tasks.clear(); + stream.close(); + + es.shutdownNow(); + FileStatus fileStatus = fs.getFileStatus(testFile); + assertEquals(testWriteBufferSize * operationCount, fileStatus.getLen()); + } + + @Test + public void testReadWriteHeavyBytesToFileWithStatistics() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final FileSystem.Statistics abfsStatistics; + final Path testFile = path(methodName.getMethodName()); + int testBufferSize; + final byte[] sourceData; + try (FSDataOutputStream stream = fs.create(testFile)) { + abfsStatistics = fs.getFsStatistics(); + abfsStatistics.reset(); + + testBufferSize = 5 * TEN * ONE_THOUSAND * BASE_SIZE; + sourceData = new byte[testBufferSize]; + new Random().nextBytes(sourceData); + stream.write(sourceData); + } + + final byte[] remoteData = new byte[testBufferSize]; + int bytesRead; + try (FSDataInputStream inputStream = fs.open(testFile, 4 * ONE_MB)) { + bytesRead = inputStream.read(remoteData); + } + + String stats = abfsStatistics.toString(); + assertEquals("Bytes read in " + stats, + remoteData.length, abfsStatistics.getBytesRead()); + assertEquals("bytes written in " + stats, + sourceData.length, abfsStatistics.getBytesWritten()); + assertEquals("bytesRead from read() call", testBufferSize, bytesRead); + assertArrayEquals("round tripped data", sourceData, remoteData); + + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFileStatus.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFileStatus.java new file mode 100644 index 00000000000..02f938f19f4 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFileStatus.java @@ -0,0 +1,125 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; + +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.junit.Ignore; +import org.junit.Test; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.permission.FsPermission; + +/** + * Test FileStatus. + */ +public class ITestAzureBlobFileSystemFileStatus extends + AbstractAbfsIntegrationTest { + private static final String DEFAULT_FILE_PERMISSION_VALUE = "640"; + private static final String DEFAULT_DIR_PERMISSION_VALUE = "750"; + private static final String DEFAULT_UMASK_VALUE = "027"; + + private static final Path TEST_FILE = new Path("testFile"); + private static final Path TEST_FOLDER = new Path("testDir"); + + public ITestAzureBlobFileSystemFileStatus() throws Exception { + super(); + } + + @Test + public void testEnsureStatusWorksForRoot() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + + Path root = new Path("/"); + FileStatus[] rootls = fs.listStatus(root); + assertEquals("root listing", 0, rootls.length); + } + + @Ignore("When running against live abfs with Oauth account, this test will fail. Need to check the tenant.") + @Test + public void testFileStatusPermissionsAndOwnerAndGroup() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + fs.getConf().set(CommonConfigurationKeys.FS_PERMISSIONS_UMASK_KEY, DEFAULT_UMASK_VALUE); + touch(TEST_FILE); + validateStatus(fs, TEST_FILE, false); + } + + private FileStatus validateStatus(final AzureBlobFileSystem fs, final Path name, final boolean isDir) + throws IOException { + FileStatus fileStatus = fs.getFileStatus(name); + + String errorInStatus = "error in " + fileStatus + " from " + fs; + + // When running with Oauth, the owner and group info retrieved from server will be digit ids. + if (this.getAuthType() != AuthType.OAuth && !fs.isSecure()) { + assertEquals(errorInStatus + ": owner", + fs.getOwnerUser(), fileStatus.getOwner()); + assertEquals(errorInStatus + ": group", + fs.getOwnerUserPrimaryGroup(), fileStatus.getGroup()); + } else { + if (isDir) { + assertEquals(errorInStatus + ": permission", + new FsPermission(DEFAULT_DIR_PERMISSION_VALUE), fileStatus.getPermission()); + } else { + assertEquals(errorInStatus + ": permission", + new FsPermission(DEFAULT_FILE_PERMISSION_VALUE), fileStatus.getPermission()); + } + } + + return fileStatus; + } + + @Ignore("When running against live abfs with Oauth account, this test will fail. Need to check the tenant.") + @Test + public void testFolderStatusPermissionsAndOwnerAndGroup() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + fs.getConf().set(CommonConfigurationKeys.FS_PERMISSIONS_UMASK_KEY, DEFAULT_UMASK_VALUE); + fs.mkdirs(TEST_FOLDER); + + validateStatus(fs, TEST_FOLDER, true); + } + + @Test + public void testAbfsPathWithHost() throws IOException { + AzureBlobFileSystem fs = this.getFileSystem(); + Path pathWithHost1 = new Path("abfs://mycluster/abfs/file1.txt"); + Path pathwithouthost1 = new Path("/abfs/file1.txt"); + + Path pathWithHost2 = new Path("abfs://mycluster/abfs/file2.txt"); + Path pathwithouthost2 = new Path("/abfs/file2.txt"); + + // verify compatibility of this path format + fs.create(pathWithHost1); + assertTrue(fs.exists(pathwithouthost1)); + + fs.create(pathwithouthost2); + assertTrue(fs.exists(pathWithHost2)); + + // verify get + FileStatus fileStatus1 = fs.getFileStatus(pathWithHost1); + assertEquals(pathwithouthost1.getName(), fileStatus1.getPath().getName()); + + FileStatus fileStatus2 = fs.getFileStatus(pathwithouthost2); + assertEquals(pathWithHost2.getName(), fileStatus2.getPath().getName()); + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFinalize.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFinalize.java new file mode 100644 index 00000000000..9d1738857c6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFinalize.java @@ -0,0 +1,64 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.lang.ref.WeakReference; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +/** + * Test finalize() method when "fs.abfs.impl.disable.cache" is enabled. + */ +public class ITestAzureBlobFileSystemFinalize extends AbstractAbfsScaleTest{ + static final String DISABLE_ABFS_CACHE_KEY = "fs.abfs.impl.disable.cache"; + static final String DISABLE_ABFSSS_CACHE_KEY = "fs.abfss.impl.disable.cache"; + + public ITestAzureBlobFileSystemFinalize() throws Exception { + super(); + } + + @Test + public void testFinalize() throws Exception { + // Disable the cache for filesystem to make sure there is no reference. + Configuration rawConfig = this.getRawConfiguration(); + rawConfig.setBoolean( + this.getAuthType() == AuthType.SharedKey ? DISABLE_ABFS_CACHE_KEY : DISABLE_ABFSSS_CACHE_KEY, + true); + + AzureBlobFileSystem fs = (AzureBlobFileSystem) FileSystem.get(rawConfig); + + WeakReference ref = new WeakReference(fs); + fs = null; + + int i = 0; + int maxTries = 1000; + while (ref.get() != null && i < maxTries) { + System.gc(); + System.runFinalization(); + i++; + } + + Assert.assertTrue("testFinalizer didn't get cleaned up within maxTries", ref.get() == null); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFlush.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFlush.java new file mode 100644 index 00000000000..23a1ab5bb72 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemFlush.java @@ -0,0 +1,387 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.io.IOException; + +import org.apache.hadoop.fs.StreamCapabilities; +import org.apache.hadoop.fs.azurebfs.services.AbfsOutputStream; +import org.hamcrest.core.IsEqual; +import org.hamcrest.core.IsNot; +import org.junit.Assume; +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; + +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +/** + * Test flush operation. + * This class cannot be run in parallel test mode--check comments in + * testWriteHeavyBytesToFileSyncFlush(). + */ +public class ITestAzureBlobFileSystemFlush extends AbstractAbfsScaleTest { + private static final int BASE_SIZE = 1024; + private static final int ONE_THOUSAND = 1000; + private static final int TEST_BUFFER_SIZE = 5 * ONE_THOUSAND * BASE_SIZE; + private static final int ONE_MB = 1024 * 1024; + private static final int FLUSH_TIMES = 200; + private static final int THREAD_SLEEP_TIME = 1000; + + private static final int TEST_FILE_LENGTH = 1024 * 1024 * 8; + private static final int WAITING_TIME = 1000; + + public ITestAzureBlobFileSystemFlush() throws Exception { + super(); + } + + @Test + public void testAbfsOutputStreamAsyncFlushWithRetainUncommittedData() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = path(methodName.getMethodName()); + final byte[] b; + try (FSDataOutputStream stream = fs.create(testFilePath)) { + b = new byte[TEST_BUFFER_SIZE]; + new Random().nextBytes(b); + + for (int i = 0; i < 2; i++) { + stream.write(b); + + for (int j = 0; j < FLUSH_TIMES; j++) { + stream.flush(); + Thread.sleep(10); + } + } + } + + final byte[] r = new byte[TEST_BUFFER_SIZE]; + try (FSDataInputStream inputStream = fs.open(testFilePath, 4 * ONE_MB)) { + while (inputStream.available() != 0) { + int result = inputStream.read(r); + + assertNotEquals("read returned -1", -1, result); + assertArrayEquals("buffer read from stream", r, b); + } + } + } + + @Test + public void testAbfsOutputStreamSyncFlush() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = path(methodName.getMethodName()); + + final byte[] b; + try (FSDataOutputStream stream = fs.create(testFilePath)) { + b = new byte[TEST_BUFFER_SIZE]; + new Random().nextBytes(b); + stream.write(b); + + for (int i = 0; i < FLUSH_TIMES; i++) { + stream.hsync(); + stream.hflush(); + Thread.sleep(10); + } + } + + final byte[] r = new byte[TEST_BUFFER_SIZE]; + try (FSDataInputStream inputStream = fs.open(testFilePath, 4 * ONE_MB)) { + int result = inputStream.read(r); + + assertNotEquals(-1, result); + assertArrayEquals(r, b); + } + } + + + @Test + public void testWriteHeavyBytesToFileSyncFlush() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Path testFilePath = path(methodName.getMethodName()); + ExecutorService es; + try (FSDataOutputStream stream = fs.create(testFilePath)) { + es = Executors.newFixedThreadPool(10); + + final byte[] b = new byte[TEST_BUFFER_SIZE]; + new Random().nextBytes(b); + + List> tasks = new ArrayList<>(); + for (int i = 0; i < FLUSH_TIMES; i++) { + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + stream.write(b); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + boolean shouldStop = false; + while (!shouldStop) { + shouldStop = true; + for (Future task : tasks) { + if (!task.isDone()) { + stream.hsync(); + shouldStop = false; + Thread.sleep(THREAD_SLEEP_TIME); + } + } + } + + tasks.clear(); + } + + es.shutdownNow(); + FileStatus fileStatus = fs.getFileStatus(testFilePath); + long expectedWrites = (long) TEST_BUFFER_SIZE * FLUSH_TIMES; + assertEquals("Wrong file length in " + testFilePath, expectedWrites, fileStatus.getLen()); + } + + @Test + public void testWriteHeavyBytesToFileAsyncFlush() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + ExecutorService es = Executors.newFixedThreadPool(10); + + final Path testFilePath = path(methodName.getMethodName()); + try (FSDataOutputStream stream = fs.create(testFilePath)) { + + final byte[] b = new byte[TEST_BUFFER_SIZE]; + new Random().nextBytes(b); + + List> tasks = new ArrayList<>(); + for (int i = 0; i < FLUSH_TIMES; i++) { + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + stream.write(b); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + boolean shouldStop = false; + while (!shouldStop) { + shouldStop = true; + for (Future task : tasks) { + if (!task.isDone()) { + stream.flush(); + shouldStop = false; + } + } + } + Thread.sleep(THREAD_SLEEP_TIME); + tasks.clear(); + } + + es.shutdownNow(); + FileStatus fileStatus = fs.getFileStatus(testFilePath); + assertEquals((long) TEST_BUFFER_SIZE * FLUSH_TIMES, fileStatus.getLen()); + } + + @Test + public void testFlushWithFlushEnabled() throws Exception { + testFlush(true); + } + + @Test + public void testFlushWithFlushDisabled() throws Exception { + testFlush(false); + } + + private void testFlush(boolean flushEnabled) throws Exception { + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + + final AzureBlobFileSystem fs = (AzureBlobFileSystem) getFileSystem(); + + // Simulate setting "fs.azure.enable.flush" to true or false + fs.getAbfsStore().getAbfsConfiguration().setEnableFlush(flushEnabled); + + final Path testFilePath = path(methodName.getMethodName()); + byte[] buffer = getRandomBytesArray(); + + // The test case must write "fs.azure.write.request.size" bytes + // to the stream in order for the data to be uploaded to storage. + assertEquals( + fs.getAbfsStore().getAbfsConfiguration().getWriteBufferSize(), + buffer.length); + + try (FSDataOutputStream stream = fs.create(testFilePath)) { + stream.write(buffer); + + // Write asynchronously uploads data, so we must wait for completion + AbfsOutputStream abfsStream = (AbfsOutputStream) stream.getWrappedStream(); + abfsStream.waitForPendingUploads(); + + // Flush commits the data so it can be read. + stream.flush(); + + // Verify that the data can be read if flushEnabled is true; and otherwise + // cannot be read. + validate(fs.open(testFilePath), buffer, flushEnabled); + } + } + + @Test + public void testHflushWithFlushEnabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + String fileName = UUID.randomUUID().toString(); + final Path testFilePath = path(fileName); + + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, true)) { + stream.hflush(); + validate(fs, testFilePath, buffer, true); + } + } + + @Test + public void testHflushWithFlushDisabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + final Path testFilePath = path(methodName.getMethodName()); + + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, false)) { + stream.hflush(); + validate(fs, testFilePath, buffer, false); + } + } + + @Test + public void testHsyncWithFlushEnabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + + final Path testFilePath = path(methodName.getMethodName()); + + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, true)) { + stream.hsync(); + validate(fs, testFilePath, buffer, true); + } + } + + @Test + public void testStreamCapabilitiesWithFlushDisabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + + final Path testFilePath = path(methodName.getMethodName()); + + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, false)) { + assertFalse(stream.hasCapability(StreamCapabilities.HFLUSH)); + assertFalse(stream.hasCapability(StreamCapabilities.HSYNC)); + assertFalse(stream.hasCapability(StreamCapabilities.DROPBEHIND)); + assertFalse(stream.hasCapability(StreamCapabilities.READAHEAD)); + assertFalse(stream.hasCapability(StreamCapabilities.UNBUFFER)); + } + } + + @Test + public void testStreamCapabilitiesWithFlushEnabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + final Path testFilePath = path(methodName.getMethodName()); + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, true)) { + assertTrue(stream.hasCapability(StreamCapabilities.HFLUSH)); + assertTrue(stream.hasCapability(StreamCapabilities.HSYNC)); + assertFalse(stream.hasCapability(StreamCapabilities.DROPBEHIND)); + assertFalse(stream.hasCapability(StreamCapabilities.READAHEAD)); + assertFalse(stream.hasCapability(StreamCapabilities.UNBUFFER)); + } + } + + @Test + public void testHsyncWithFlushDisabled() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + byte[] buffer = getRandomBytesArray(); + final Path testFilePath = path(methodName.getMethodName()); + try (FSDataOutputStream stream = getStreamAfterWrite(fs, testFilePath, buffer, false)) { + stream.hsync(); + validate(fs, testFilePath, buffer, false); + } + } + + private byte[] getRandomBytesArray() { + final byte[] b = new byte[TEST_FILE_LENGTH]; + new Random().nextBytes(b); + return b; + } + + private FSDataOutputStream getStreamAfterWrite(AzureBlobFileSystem fs, Path path, byte[] buffer, boolean enableFlush) throws IOException { + fs.getAbfsStore().getAbfsConfiguration().setEnableFlush(enableFlush); + FSDataOutputStream stream = fs.create(path); + stream.write(buffer); + return stream; + } + + private void validate(InputStream stream, byte[] writeBuffer, boolean isEqual) + throws IOException { + try { + byte[] readBuffer = new byte[writeBuffer.length]; + + int numBytesRead = stream.read(readBuffer, 0, readBuffer.length); + + if (isEqual) { + assertArrayEquals( + "Bytes read do not match bytes written.", + writeBuffer, + readBuffer); + } else { + assertThat( + "Bytes read unexpectedly match bytes written.", + readBuffer, + IsNot.not(IsEqual.equalTo(writeBuffer))); + } + } finally { + stream.close(); + } + } + private void validate(FileSystem fs, Path path, byte[] writeBuffer, boolean isEqual) throws IOException { + String filePath = path.toUri().toString(); + try (FSDataInputStream inputStream = fs.open(path)) { + byte[] readBuffer = new byte[TEST_FILE_LENGTH]; + int numBytesRead = inputStream.read(readBuffer, 0, readBuffer.length); + if (isEqual) { + assertArrayEquals( + String.format("Bytes read do not match bytes written to %1$s", filePath), writeBuffer, readBuffer); + } else { + assertThat( + String.format("Bytes read unexpectedly match bytes written to %1$s", + filePath), + readBuffer, + IsNot.not(IsEqual.equalTo(writeBuffer))); + } + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemInitAndCreate.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemInitAndCreate.java new file mode 100644 index 00000000000..5f08721ada0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemInitAndCreate.java @@ -0,0 +1,53 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.FileNotFoundException; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.junit.Test; + +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; + +/** + * Test filesystem initialization and creation. + */ +public class ITestAzureBlobFileSystemInitAndCreate extends + AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemInitAndCreate() throws Exception { + this.getConfiguration().unset(ConfigurationKeys.AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION); + } + + @Override + public void setup() { + } + + @Override + public void teardown() { + } + + @Test (expected = FileNotFoundException.class) + public void ensureFilesystemWillNotBeCreatedIfCreationConfigIsNotSet() throws Exception { + super.setup(); + final AzureBlobFileSystem fs = this.getFileSystem(); + FileStatus[] fileStatuses = fs.listStatus(new Path("/")); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemListStatus.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemListStatus.java new file mode 100644 index 00000000000..60e0fbc7237 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemListStatus.java @@ -0,0 +1,172 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.LocatedFileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; + +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Test listStatus operation. + */ +public class ITestAzureBlobFileSystemListStatus extends + AbstractAbfsIntegrationTest { + private static final int TEST_FILES_NUMBER = 6000; + + public ITestAzureBlobFileSystemListStatus() throws Exception { + super(); + } + + @Test + public void testListPath() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final List> tasks = new ArrayList<>(); + + ExecutorService es = Executors.newFixedThreadPool(10); + for (int i = 0; i < TEST_FILES_NUMBER; i++) { + final Path fileName = new Path("/test" + i); + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + touch(fileName); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + for (Future task : tasks) { + task.get(); + } + + es.shutdownNow(); + FileStatus[] files = fs.listStatus(new Path("/")); + assertEquals(TEST_FILES_NUMBER, files.length /* user directory */); + } + + /** + * Creates a file, verifies that listStatus returns it, + * even while the file is still open for writing. + */ + @Test + public void testListFileVsListDir() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path path = new Path("/testFile"); + try(FSDataOutputStream ignored = fs.create(path)) { + FileStatus[] testFiles = fs.listStatus(path); + assertEquals("length of test files", 1, testFiles.length); + FileStatus status = testFiles[0]; + assertIsFileReference(status); + } + } + + @Test + public void testListFileVsListDir2() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + fs.mkdirs(new Path("/testFolder")); + fs.mkdirs(new Path("/testFolder/testFolder2")); + fs.mkdirs(new Path("/testFolder/testFolder2/testFolder3")); + Path testFile0Path = new Path("/testFolder/testFolder2/testFolder3/testFile"); + ContractTestUtils.touch(fs, testFile0Path); + + FileStatus[] testFiles = fs.listStatus(testFile0Path); + assertEquals("Wrong listing size of file " + testFile0Path, + 1, testFiles.length); + FileStatus file0 = testFiles[0]; + assertEquals("Wrong path for " + file0, + new Path(getTestUrl(), "/testFolder/testFolder2/testFolder3/testFile"), + file0.getPath()); + assertIsFileReference(file0); + } + + @Test(expected = FileNotFoundException.class) + public void testListNonExistentDir() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + fs.listStatus(new Path("/testFile/")); + } + + @Test + public void testListFiles() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path testDir = new Path("/test"); + fs.mkdirs(testDir); + + FileStatus[] fileStatuses = fs.listStatus(new Path("/")); + assertEquals(1, fileStatuses.length); + + fs.mkdirs(new Path("/test/sub")); + fileStatuses = fs.listStatus(testDir); + assertEquals(1, fileStatuses.length); + assertEquals("sub", fileStatuses[0].getPath().getName()); + assertIsDirectoryReference(fileStatuses[0]); + Path childF = fs.makeQualified(new Path("/test/f")); + touch(childF); + fileStatuses = fs.listStatus(testDir); + assertEquals(2, fileStatuses.length); + final FileStatus childStatus = fileStatuses[0]; + assertEquals(childF, childStatus.getPath()); + assertEquals("f", childStatus.getPath().getName()); + assertIsFileReference(childStatus); + assertEquals(0, childStatus.getLen()); + final FileStatus status1 = fileStatuses[1]; + assertEquals("sub", status1.getPath().getName()); + assertIsDirectoryReference(status1); + // look at the child through getFileStatus + LocatedFileStatus locatedChildStatus = fs.listFiles(childF, false).next(); + assertIsFileReference(locatedChildStatus); + + fs.delete(testDir, true); + intercept(FileNotFoundException.class, + () -> fs.listFiles(childF, false).next()); + + // do some final checks on the status (failing due to version checks) + assertEquals("Path mismatch of " + locatedChildStatus, + childF, locatedChildStatus.getPath()); + assertEquals("locatedstatus.equals(status)", + locatedChildStatus, childStatus); + assertEquals("status.equals(locatedstatus)", + childStatus, locatedChildStatus); + } + + private void assertIsDirectoryReference(FileStatus status) { + assertTrue("Not a directory: " + status, status.isDirectory()); + assertFalse("Not a directory: " + status, status.isFile()); + assertEquals(0, status.getLen()); + } + + private void assertIsFileReference(FileStatus status) { + assertFalse("Not a file: " + status, status.isDirectory()); + assertTrue("Not a file: " + status, status.isFile()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemMkDir.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemMkDir.java new file mode 100644 index 00000000000..382d3966485 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemMkDir.java @@ -0,0 +1,48 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import org.junit.Test; + +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertMkdirs; + +/** + * Test mkdir operation. + */ +public class ITestAzureBlobFileSystemMkDir extends AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemMkDir() throws Exception { + super(); + } + + @Test + public void testCreateDirWithExistingDir() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path path = new Path("testFolder"); + assertMkdirs(fs, path); + assertMkdirs(fs, path); + } + + @Test + public void testCreateRoot() throws Exception { + assertMkdirs(getFileSystem(), new Path("/")); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemOauth.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemOauth.java new file mode 100644 index 00000000000..533f4712565 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemOauth.java @@ -0,0 +1,178 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys; +import org.junit.Assume; +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.AbfsRestOperationException; +import org.apache.hadoop.fs.azurebfs.contracts.services.AzureServiceErrorCode; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID; +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_CLIENT_SECRET; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_ID; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_SECRET; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.FS_AZURE_BLOB_DATA_READER_CLIENT_ID; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.FS_AZURE_BLOB_DATA_READER_CLIENT_SECRET; + +/** + * Test Azure Oauth with Blob Data contributor role and Blob Data Reader role. + * The Test AAD client need to be configured manually through Azure Portal, then save their properties in + * configuration files. + */ +public class ITestAzureBlobFileSystemOauth extends AbstractAbfsIntegrationTest{ + + private static final Path FILE_PATH = new Path("/testFile"); + private static final Path EXISTED_FILE_PATH = new Path("/existedFile"); + private static final Path EXISTED_FOLDER_PATH = new Path("/existedFolder"); + + public ITestAzureBlobFileSystemOauth() throws Exception { + Assume.assumeTrue(this.getAuthType() == AuthType.OAuth); + } + /* + * BLOB DATA CONTRIBUTOR should have full access to the container and blobs in the container. + * */ + @Test + public void testBlobDataContributor() throws Exception { + String clientId = this.getConfiguration().get(TestConfigurationKeys.FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_ID); + Assume.assumeTrue("Contributor client id not provided", clientId != null); + String secret = this.getConfiguration().get(TestConfigurationKeys.FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_SECRET); + Assume.assumeTrue("Contributor client secret not provided", secret != null); + + prepareFiles(); + + final AzureBlobFileSystem fs = getBlobConributor(); + + // create and write into file in current container/fs + try(FSDataOutputStream stream = fs.create(FILE_PATH)) { + stream.write(0); + } + assertTrue(fs.exists(FILE_PATH)); + FileStatus fileStatus = fs.getFileStatus(FILE_PATH); + assertEquals(1, fileStatus.getLen()); + // delete file + assertTrue(fs.delete(FILE_PATH, true)); + assertFalse(fs.exists(FILE_PATH)); + + // Verify Blob Data Contributor has full access to existed folder, file + + // READ FOLDER + assertTrue(fs.exists(EXISTED_FOLDER_PATH)); + + //DELETE FOLDER + fs.delete(EXISTED_FOLDER_PATH, true); + assertFalse(fs.exists(EXISTED_FOLDER_PATH)); + + // READ FILE + try (FSDataInputStream stream = fs.open(EXISTED_FILE_PATH)) { + assertTrue(stream.read() != 0); + } + + assertEquals(0, fs.getFileStatus(EXISTED_FILE_PATH).getLen()); + + // WRITE FILE + try (FSDataOutputStream stream = fs.append(EXISTED_FILE_PATH)) { + stream.write(0); + } + + assertEquals(1, fs.getFileStatus(EXISTED_FILE_PATH).getLen()); + + // REMOVE FILE + fs.delete(EXISTED_FILE_PATH, true); + assertFalse(fs.exists(EXISTED_FILE_PATH)); + } + + /* + * BLOB DATA READER should have only READ access to the container and blobs in the container. + * */ + @Test + public void testBlobDataReader() throws Exception { + String clientId = this.getConfiguration().get(TestConfigurationKeys.FS_AZURE_BLOB_DATA_READER_CLIENT_ID); + Assume.assumeTrue("Reader client id not provided", clientId != null); + String secret = this.getConfiguration().get(TestConfigurationKeys.FS_AZURE_BLOB_DATA_READER_CLIENT_SECRET); + Assume.assumeTrue("Reader client secret not provided", secret != null); + + prepareFiles(); + final AzureBlobFileSystem fs = getBlobReader(); + + // Use abfsStore in this test to verify the ERROR code in AbfsRestOperationException + AzureBlobFileSystemStore abfsStore = fs.getAbfsStore(); + // TEST READ FS + Map properties = abfsStore.getFilesystemProperties(); + // TEST READ FOLDER + assertTrue(fs.exists(EXISTED_FOLDER_PATH)); + + // TEST DELETE FOLDER + try { + abfsStore.delete(EXISTED_FOLDER_PATH, true); + } catch (AbfsRestOperationException e) { + assertEquals(AzureServiceErrorCode.AUTHORIZATION_PERMISSION_MISS_MATCH, e.getErrorCode()); + } + + // TEST READ FILE + try (InputStream inputStream = abfsStore.openFileForRead(EXISTED_FILE_PATH, null)) { + assertTrue(inputStream.read() != 0); + } + + // TEST WRITE FILE + try { + abfsStore.openFileForWrite(EXISTED_FILE_PATH, true); + } catch (AbfsRestOperationException e) { + assertEquals(AzureServiceErrorCode.AUTHORIZATION_PERMISSION_MISS_MATCH, e.getErrorCode()); + } + + } + + private void prepareFiles() throws IOException { + // create test files/folders to verify access control diff between + // Blob data contributor and Blob data reader + final AzureBlobFileSystem fs = this.getFileSystem(); + fs.create(EXISTED_FILE_PATH); + assertTrue(fs.exists(EXISTED_FILE_PATH)); + fs.mkdirs(EXISTED_FOLDER_PATH); + assertTrue(fs.exists(EXISTED_FOLDER_PATH)); + } + + private AzureBlobFileSystem getBlobConributor() throws Exception { + AbfsConfiguration abfsConfig = this.getConfiguration(); + abfsConfig.set(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID + "." + this.getAccountName(), abfsConfig.get(FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_ID)); + abfsConfig.set(FS_AZURE_ACCOUNT_OAUTH_CLIENT_SECRET + "." + this.getAccountName(), abfsConfig.get(FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_SECRET)); + Configuration rawConfig = abfsConfig.getRawConfiguration(); + return getFileSystem(rawConfig); + } + + private AzureBlobFileSystem getBlobReader() throws Exception { + AbfsConfiguration abfsConfig = this.getConfiguration(); + abfsConfig.set(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID + "." + this.getAccountName(), abfsConfig.get(FS_AZURE_BLOB_DATA_READER_CLIENT_ID)); + abfsConfig.set(FS_AZURE_ACCOUNT_OAUTH_CLIENT_SECRET + "." + this.getAccountName(), abfsConfig.get(FS_AZURE_BLOB_DATA_READER_CLIENT_SECRET)); + Configuration rawConfig = abfsConfig.getRawConfiguration(); + return getFileSystem(rawConfig); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemPermission.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemPermission.java new file mode 100644 index 00000000000..bbb2e240bee --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemPermission.java @@ -0,0 +1,108 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.fs.azurebfs.utils.Parallelized; + +/** + * Test permission operations. + */ +@RunWith(Parallelized.class) +public class ITestAzureBlobFileSystemPermission extends AbstractAbfsIntegrationTest{ + + private static Path testRoot = new Path("/test"); + private static final String DEFAULT_UMASK_VALUE = "027"; + private static final FsPermission DEFAULT_UMASK_PERMISSION = new FsPermission(DEFAULT_UMASK_VALUE); + private static final int KILOBYTE = 1024; + private FsPermission permission; + + private Path path; + + public ITestAzureBlobFileSystemPermission(FsPermission testPermission) throws Exception { + super(); + permission = testPermission; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection abfsCreateNonRecursiveTestData() + throws Exception { + /* + Test Data + File/Folder name, User permission, Group permission, Other Permission, + Parent already exist + shouldCreateSucceed, expectedExceptionIfFileCreateFails + */ + final Collection datas = new ArrayList<>(); + for (FsAction g : FsAction.values()) { + for (FsAction o : FsAction.values()) { + datas.add(new Object[] {new FsPermission(FsAction.ALL, g, o)}); + } + } + return datas; + } + + @Test + public void testFilePermission() throws Exception { + + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(fs.getIsNamespaceEnabeld()); + fs.getConf().set(CommonConfigurationKeys.FS_PERMISSIONS_UMASK_KEY, DEFAULT_UMASK_VALUE); + path = new Path(testRoot, UUID.randomUUID().toString()); + + fs.mkdirs(path.getParent(), + new FsPermission(FsAction.ALL, FsAction.NONE, FsAction.NONE)); + fs.removeDefaultAcl(path.getParent()); + + fs.create(path, permission, true, KILOBYTE, (short) 1, KILOBYTE - 1, null); + FileStatus status = fs.getFileStatus(path); + Assert.assertEquals(permission.applyUMask(DEFAULT_UMASK_PERMISSION), status.getPermission()); + } + + @Test + public void testFolderPermission() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(fs.getIsNamespaceEnabeld()); + fs.getConf().set(CommonConfigurationKeys.FS_PERMISSIONS_UMASK_KEY, "027"); + + path = new Path(testRoot, UUID.randomUUID().toString()); + + fs.mkdirs(path.getParent(), + new FsPermission(FsAction.ALL, FsAction.WRITE, FsAction.NONE)); + fs.removeDefaultAcl(path.getParent()); + + fs.mkdirs(path, permission); + FileStatus status = fs.getFileStatus(path); + Assert.assertEquals(permission.applyUMask(DEFAULT_UMASK_PERMISSION), status.getPermission()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRandomRead.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRandomRead.java new file mode 100644 index 00000000000..38e7133ed8e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRandomRead.java @@ -0,0 +1,588 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.Callable; + +import org.junit.Assume; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FSExceptionMessages; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azure.NativeAzureFileSystem; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.apache.hadoop.fs.contract.ContractTestUtils; + +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Test random read operation. + */ +public class ITestAzureBlobFileSystemRandomRead extends + AbstractAbfsScaleTest { + private static final int KILOBYTE = 1024; + private static final int MEGABYTE = KILOBYTE * KILOBYTE; + private static final long TEST_FILE_SIZE = 8 * MEGABYTE; + private static final int MAX_ELAPSEDTIMEMS = 20; + private static final int SEQUENTIAL_READ_BUFFER_SIZE = 16 * KILOBYTE; + private static final int CREATE_BUFFER_SIZE = 26 * KILOBYTE; + + private static final int SEEK_POSITION_ONE = 2* KILOBYTE; + private static final int SEEK_POSITION_TWO = 5 * KILOBYTE; + private static final int SEEK_POSITION_THREE = 10 * KILOBYTE; + private static final int SEEK_POSITION_FOUR = 4100 * KILOBYTE; + + private static final Path TEST_FILE_PATH = new Path( + "/TestRandomRead.txt"); + private static final String WASB = "WASB"; + private static final String ABFS = "ABFS"; + private static long testFileLength = 0; + + private static final Logger LOG = + LoggerFactory.getLogger(ITestAzureBlobFileSystemRandomRead.class); + + public ITestAzureBlobFileSystemRandomRead() throws Exception { + super(); + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + } + + @Test + public void testBasicRead() throws Exception { + assumeHugeFileExists(); + + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + byte[] buffer = new byte[3 * MEGABYTE]; + + // forward seek and read a kilobyte into first kilobyte of bufferV2 + inputStream.seek(5 * MEGABYTE); + int numBytesRead = inputStream.read(buffer, 0, KILOBYTE); + assertEquals("Wrong number of bytes read", KILOBYTE, numBytesRead); + + int len = MEGABYTE; + int offset = buffer.length - len; + + // reverse seek and read a megabyte into last megabyte of bufferV1 + inputStream.seek(3 * MEGABYTE); + numBytesRead = inputStream.read(buffer, offset, len); + assertEquals("Wrong number of bytes read after seek", len, numBytesRead); + } + } + + /** + * Validates the implementation of random read in ABFS + * @throws IOException + */ + @Test + public void testRandomRead() throws Exception { + assumeHugeFileExists(); + try ( + FSDataInputStream inputStreamV1 + = this.getFileSystem().open(TEST_FILE_PATH); + FSDataInputStream inputStreamV2 + = this.getWasbFileSystem().open(TEST_FILE_PATH); + ) { + final int bufferSize = 4 * KILOBYTE; + byte[] bufferV1 = new byte[bufferSize]; + byte[] bufferV2 = new byte[bufferV1.length]; + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + inputStreamV1.seek(0); + inputStreamV2.seek(0); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + inputStreamV1.seek(SEEK_POSITION_ONE); + inputStreamV2.seek(SEEK_POSITION_ONE); + + inputStreamV1.seek(0); + inputStreamV2.seek(0); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + inputStreamV1.seek(SEEK_POSITION_TWO); + inputStreamV2.seek(SEEK_POSITION_TWO); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + inputStreamV1.seek(SEEK_POSITION_THREE); + inputStreamV2.seek(SEEK_POSITION_THREE); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + + inputStreamV1.seek(SEEK_POSITION_FOUR); + inputStreamV2.seek(SEEK_POSITION_FOUR); + + verifyConsistentReads(inputStreamV1, inputStreamV2, bufferV1, bufferV2); + } + } + + /** + * Validates the implementation of Seekable.seekToNewSource + * @throws IOException + */ + @Test + public void testSeekToNewSource() throws Exception { + assumeHugeFileExists(); + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + assertFalse(inputStream.seekToNewSource(0)); + } + } + + /** + * Validates the implementation of InputStream.skip and ensures there is no + * network I/O for AbfsInputStream + * @throws Exception + */ + @Test + public void testSkipBounds() throws Exception { + assumeHugeFileExists(); + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + + long skipped = inputStream.skip(-1); + assertEquals(0, skipped); + + skipped = inputStream.skip(0); + assertEquals(0, skipped); + + assertTrue(testFileLength > 0); + + skipped = inputStream.skip(testFileLength); + assertEquals(testFileLength, skipped); + + intercept(EOFException.class, + new Callable() { + @Override + public Long call() throws Exception { + return inputStream.skip(1); + } + } + ); + long elapsedTimeMs = timer.elapsedTimeMs(); + assertTrue( + String.format( + "There should not be any network I/O (elapsedTimeMs=%1$d).", + elapsedTimeMs), + elapsedTimeMs < MAX_ELAPSEDTIMEMS); + } + } + + /** + * Validates the implementation of Seekable.seek and ensures there is no + * network I/O for forward seek. + * @throws Exception + */ + @Test + public void testValidateSeekBounds() throws Exception { + assumeHugeFileExists(); + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + + inputStream.seek(0); + assertEquals(0, inputStream.getPos()); + + intercept(EOFException.class, + FSExceptionMessages.NEGATIVE_SEEK, + new Callable() { + @Override + public FSDataInputStream call() throws Exception { + inputStream.seek(-1); + return inputStream; + } + } + ); + + assertTrue("Test file length only " + testFileLength, testFileLength > 0); + inputStream.seek(testFileLength); + assertEquals(testFileLength, inputStream.getPos()); + + intercept(EOFException.class, + FSExceptionMessages.CANNOT_SEEK_PAST_EOF, + new Callable() { + @Override + public FSDataInputStream call() throws Exception { + inputStream.seek(testFileLength + 1); + return inputStream; + } + } + ); + + long elapsedTimeMs = timer.elapsedTimeMs(); + assertTrue( + String.format( + "There should not be any network I/O (elapsedTimeMs=%1$d).", + elapsedTimeMs), + elapsedTimeMs < MAX_ELAPSEDTIMEMS); + } + } + + /** + * Validates the implementation of Seekable.seek, Seekable.getPos, + * and InputStream.available. + * @throws Exception + */ + @Test + public void testSeekAndAvailableAndPosition() throws Exception { + assumeHugeFileExists(); + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + byte[] expected1 = {(byte) 'a', (byte) 'b', (byte) 'c'}; + byte[] expected2 = {(byte) 'd', (byte) 'e', (byte) 'f'}; + byte[] expected3 = {(byte) 'b', (byte) 'c', (byte) 'd'}; + byte[] expected4 = {(byte) 'g', (byte) 'h', (byte) 'i'}; + byte[] buffer = new byte[3]; + + int bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected1, buffer); + assertEquals(buffer.length, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected2, buffer); + assertEquals(2 * buffer.length, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + // reverse seek + int seekPos = 0; + inputStream.seek(seekPos); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected1, buffer); + assertEquals(buffer.length + seekPos, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + // reverse seek + seekPos = 1; + inputStream.seek(seekPos); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected3, buffer); + assertEquals(buffer.length + seekPos, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + // forward seek + seekPos = 6; + inputStream.seek(seekPos); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected4, buffer); + assertEquals(buffer.length + seekPos, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + } + } + + /** + * Validates the implementation of InputStream.skip, Seekable.getPos, + * and InputStream.available. + * @throws IOException + */ + @Test + public void testSkipAndAvailableAndPosition() throws Exception { + assumeHugeFileExists(); + try (FSDataInputStream inputStream = this.getFileSystem().open(TEST_FILE_PATH)) { + byte[] expected1 = {(byte) 'a', (byte) 'b', (byte) 'c'}; + byte[] expected2 = {(byte) 'd', (byte) 'e', (byte) 'f'}; + byte[] expected3 = {(byte) 'b', (byte) 'c', (byte) 'd'}; + byte[] expected4 = {(byte) 'g', (byte) 'h', (byte) 'i'}; + + assertEquals(testFileLength, inputStream.available()); + assertEquals(0, inputStream.getPos()); + + int n = 3; + long skipped = inputStream.skip(n); + + assertEquals(skipped, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + assertEquals(skipped, n); + + byte[] buffer = new byte[3]; + int bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected2, buffer); + assertEquals(buffer.length + skipped, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + // does skip still work after seek? + int seekPos = 1; + inputStream.seek(seekPos); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected3, buffer); + assertEquals(buffer.length + seekPos, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + + long currentPosition = inputStream.getPos(); + n = 2; + skipped = inputStream.skip(n); + + assertEquals(currentPosition + skipped, inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + assertEquals(skipped, n); + + bytesRead = inputStream.read(buffer); + assertEquals(buffer.length, bytesRead); + assertArrayEquals(expected4, buffer); + assertEquals(buffer.length + skipped + currentPosition, + inputStream.getPos()); + assertEquals(testFileLength - inputStream.getPos(), + inputStream.available()); + } + } + + /** + * Ensures parity in the performance of sequential read after reverse seek for + * abfs of the AbfsInputStream. + * @throws IOException + */ + @Test + public void testSequentialReadAfterReverseSeekPerformance() + throws Exception { + assumeHugeFileExists(); + final int maxAttempts = 10; + final double maxAcceptableRatio = 1.01; + double beforeSeekElapsedMs = 0, afterSeekElapsedMs = 0; + double ratio = Double.MAX_VALUE; + for (int i = 0; i < maxAttempts && ratio >= maxAcceptableRatio; i++) { + beforeSeekElapsedMs = sequentialRead(ABFS, + this.getFileSystem(), false); + afterSeekElapsedMs = sequentialRead(ABFS, + this.getFileSystem(), true); + ratio = afterSeekElapsedMs / beforeSeekElapsedMs; + LOG.info((String.format( + "beforeSeekElapsedMs=%1$d, afterSeekElapsedMs=%2$d, ratio=%3$.2f", + (long) beforeSeekElapsedMs, + (long) afterSeekElapsedMs, + ratio))); + } + assertTrue(String.format( + "Performance of ABFS stream after reverse seek is not acceptable:" + + " beforeSeekElapsedMs=%1$d, afterSeekElapsedMs=%2$d," + + " ratio=%3$.2f", + (long) beforeSeekElapsedMs, + (long) afterSeekElapsedMs, + ratio), + ratio < maxAcceptableRatio); + } + + @Test + public void testRandomReadPerformance() throws Exception { + createTestFile(); + assumeHugeFileExists(); + + final AzureBlobFileSystem abFs = this.getFileSystem(); + final NativeAzureFileSystem wasbFs = this.getWasbFileSystem(); + + final int maxAttempts = 10; + final double maxAcceptableRatio = 1.025; + double v1ElapsedMs = 0, v2ElapsedMs = 0; + double ratio = Double.MAX_VALUE; + for (int i = 0; i < maxAttempts && ratio >= maxAcceptableRatio; i++) { + v1ElapsedMs = randomRead(1, wasbFs); + v2ElapsedMs = randomRead(2, abFs); + + ratio = v2ElapsedMs / v1ElapsedMs; + + LOG.info(String.format( + "v1ElapsedMs=%1$d, v2ElapsedMs=%2$d, ratio=%3$.2f", + (long) v1ElapsedMs, + (long) v2ElapsedMs, + ratio)); + } + assertTrue(String.format( + "Performance of version 2 is not acceptable: v1ElapsedMs=%1$d," + + " v2ElapsedMs=%2$d, ratio=%3$.2f", + (long) v1ElapsedMs, + (long) v2ElapsedMs, + ratio), + ratio < maxAcceptableRatio); + } + + + private long sequentialRead(String version, + FileSystem fs, + boolean afterReverseSeek) throws IOException { + byte[] buffer = new byte[SEQUENTIAL_READ_BUFFER_SIZE]; + long totalBytesRead = 0; + long bytesRead = 0; + + try(FSDataInputStream inputStream = fs.open(TEST_FILE_PATH)) { + if (afterReverseSeek) { + while (bytesRead > 0 && totalBytesRead < 4 * MEGABYTE) { + bytesRead = inputStream.read(buffer); + totalBytesRead += bytesRead; + } + totalBytesRead = 0; + inputStream.seek(0); + } + + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + while ((bytesRead = inputStream.read(buffer)) > 0) { + totalBytesRead += bytesRead; + } + long elapsedTimeMs = timer.elapsedTimeMs(); + + LOG.info(String.format( + "v%1$s: bytesRead=%2$d, elapsedMs=%3$d, Mbps=%4$.2f," + + " afterReverseSeek=%5$s", + version, + totalBytesRead, + elapsedTimeMs, + toMbps(totalBytesRead, elapsedTimeMs), + afterReverseSeek)); + + assertEquals(testFileLength, totalBytesRead); + inputStream.close(); + return elapsedTimeMs; + } + } + + private long randomRead(int version, FileSystem fs) throws Exception { + assumeHugeFileExists(); + final long minBytesToRead = 2 * MEGABYTE; + Random random = new Random(); + byte[] buffer = new byte[8 * KILOBYTE]; + long totalBytesRead = 0; + long bytesRead = 0; + try(FSDataInputStream inputStream = fs.open(TEST_FILE_PATH)) { + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + do { + bytesRead = inputStream.read(buffer); + totalBytesRead += bytesRead; + inputStream.seek(random.nextInt( + (int) (TEST_FILE_SIZE - buffer.length))); + } while (bytesRead > 0 && totalBytesRead < minBytesToRead); + long elapsedTimeMs = timer.elapsedTimeMs(); + inputStream.close(); + LOG.info(String.format( + "v%1$d: totalBytesRead=%2$d, elapsedTimeMs=%3$d, Mbps=%4$.2f", + version, + totalBytesRead, + elapsedTimeMs, + toMbps(totalBytesRead, elapsedTimeMs))); + assertTrue(minBytesToRead <= totalBytesRead); + return elapsedTimeMs; + } + } + + /** + * Calculate megabits per second from the specified values for bytes and + * milliseconds. + * @param bytes The number of bytes. + * @param milliseconds The number of milliseconds. + * @return The number of megabits per second. + */ + private static double toMbps(long bytes, long milliseconds) { + return bytes / 1000.0 * 8 / milliseconds; + } + + private void createTestFile() throws Exception { + final AzureBlobFileSystem abFs = this.getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(abFs.getIsNamespaceEnabeld()); + FileSystem fs = this.getWasbFileSystem(); + + if (fs.exists(TEST_FILE_PATH)) { + FileStatus status = fs.getFileStatus(TEST_FILE_PATH); + if (status.getLen() >= TEST_FILE_SIZE) { + return; + } + } + + byte[] buffer = new byte[CREATE_BUFFER_SIZE]; + char character = 'a'; + for (int i = 0; i < buffer.length; i++) { + buffer[i] = (byte) character; + character = (character == 'z') ? 'a' : (char) ((int) character + 1); + } + + LOG.info(String.format("Creating test file %s of size: %d ", TEST_FILE_PATH, TEST_FILE_SIZE)); + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + + try (FSDataOutputStream outputStream = fs.create(TEST_FILE_PATH)) { + int bytesWritten = 0; + while (bytesWritten < TEST_FILE_SIZE) { + outputStream.write(buffer); + bytesWritten += buffer.length; + } + LOG.info("Closing stream {}", outputStream); + ContractTestUtils.NanoTimer closeTimer + = new ContractTestUtils.NanoTimer(); + outputStream.close(); + closeTimer.end("time to close() output stream"); + } + timer.end("time to write %d KB", TEST_FILE_SIZE / 1024); + testFileLength = fs.getFileStatus(TEST_FILE_PATH).getLen(); + + } + + private void assumeHugeFileExists() throws Exception{ + createTestFile(); + FileSystem fs = this.getFileSystem(); + ContractTestUtils.assertPathExists(this.getFileSystem(), "huge file not created", TEST_FILE_PATH); + FileStatus status = fs.getFileStatus(TEST_FILE_PATH); + ContractTestUtils.assertIsFile(TEST_FILE_PATH, status); + assertTrue("File " + TEST_FILE_PATH + " is empty", status.getLen() > 0); + } + + private void verifyConsistentReads(FSDataInputStream inputStreamV1, + FSDataInputStream inputStreamV2, + byte[] bufferV1, + byte[] bufferV2) throws IOException { + int size = bufferV1.length; + final int numBytesReadV1 = inputStreamV1.read(bufferV1, 0, size); + assertEquals("Bytes read from wasb stream", size, numBytesReadV1); + + final int numBytesReadV2 = inputStreamV2.read(bufferV2, 0, size); + assertEquals("Bytes read from abfs stream", size, numBytesReadV2); + + assertArrayEquals("Mismatch in read data", bufferV1, bufferV2); + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRename.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRename.java new file mode 100644 index 00000000000..e0e1d899a21 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRename.java @@ -0,0 +1,152 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertMkdirs; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertPathDoesNotExist; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertRenameOutcome; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsFile; + +/** + * Test rename operation. + */ +public class ITestAzureBlobFileSystemRename extends + AbstractAbfsIntegrationTest { + + public ITestAzureBlobFileSystemRename() throws Exception { + super(); + } + + @Test + public void testEnsureFileIsRenamed() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path src = path("testEnsureFileIsRenamed-src"); + touch(src); + Path dest = path("testEnsureFileIsRenamed-dest"); + fs.delete(dest, true); + assertRenameOutcome(fs, src, dest, true); + + assertIsFile(fs, dest); + assertPathDoesNotExist(fs, "expected renamed", src); + } + + @Test + public void testRenameFileUnderDir() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + Path sourceDir = new Path("/testSrc"); + assertMkdirs(fs, sourceDir); + String filename = "file1"; + Path file1 = new Path(sourceDir, filename); + touch(file1); + + Path destDir = new Path("/testDst"); + assertRenameOutcome(fs, sourceDir, destDir, true); + FileStatus[] fileStatus = fs.listStatus(destDir); + assertNotNull("Null file status", fileStatus); + FileStatus status = fileStatus[0]; + assertEquals("Wrong filename in " + status, + filename, status.getPath().getName()); + } + + @Test + public void testRenameDirectory() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + fs.mkdirs(new Path("testDir")); + Path test1 = new Path("testDir/test1"); + fs.mkdirs(test1); + fs.mkdirs(new Path("testDir/test1/test2")); + fs.mkdirs(new Path("testDir/test1/test2/test3")); + + assertRenameOutcome(fs, test1, + new Path("testDir/test10"), true); + assertPathDoesNotExist(fs, "rename source dir", test1); + } + + @Test + public void testRenameFirstLevelDirectory() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final List> tasks = new ArrayList<>(); + + ExecutorService es = Executors.newFixedThreadPool(10); + for (int i = 0; i < 1000; i++) { + final Path fileName = new Path("/test/" + i); + Callable callable = new Callable() { + @Override + public Void call() throws Exception { + touch(fileName); + return null; + } + }; + + tasks.add(es.submit(callable)); + } + + for (Future task : tasks) { + task.get(); + } + + es.shutdownNow(); + Path source = new Path("/test"); + Path dest = new Path("/renamedDir"); + assertRenameOutcome(fs, source, dest, true); + + FileStatus[] files = fs.listStatus(dest); + assertEquals("Wrong number of files in listing", 1000, files.length); + assertPathDoesNotExist(fs, "rename source dir", source); + } + + @Test + public void testRenameRoot() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + assertRenameOutcome(fs, + new Path("/"), + new Path("/testRenameRoot"), + false); + assertRenameOutcome(fs, + new Path(fs.getUri().toString() + "/"), + new Path(fs.getUri().toString() + "/s"), + false); + } + + @Test + public void testPosixRenameDirectory() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + fs.mkdirs(new Path("testDir2/test1/test2/test3")); + fs.mkdirs(new Path("testDir2/test4")); + Assert.assertTrue(fs.rename(new Path("testDir2/test1/test2/test3"), new Path("testDir2/test4"))); + assertTrue(fs.exists(new Path("testDir2"))); + assertTrue(fs.exists(new Path("testDir2/test1/test2"))); + assertTrue(fs.exists(new Path("testDir2/test4"))); + assertTrue(fs.exists(new Path("testDir2/test4/test3"))); + assertFalse(fs.exists(new Path("testDir2/test1/test2/test3"))); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRenameUnicode.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRenameUnicode.java new file mode 100644 index 00000000000..044c325c8c8 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFileSystemRenameUnicode.java @@ -0,0 +1,98 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsDirectory; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsFile; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertMkdirs; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertPathDoesNotExist; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertPathExists; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertRenameOutcome; + +/** + * Parameterized test of rename operations of unicode paths. + */ +@RunWith(Parameterized.class) +public class ITestAzureBlobFileSystemRenameUnicode extends + AbstractAbfsIntegrationTest { + + @Parameterized.Parameter + public String srcDir; + + @Parameterized.Parameter(1) + public String destDir; + + @Parameterized.Parameter(2) + public String filename; + + @Parameterized.Parameters + public static Iterable params() { + return Arrays.asList( + new Object[][]{ + {"/src", "/dest", "filename"}, + {"/%2c%26", "/abcÖ⇒123", "%2c%27"}, + {"/ÖáΠ⇒", "/abcÖáΠ⇒123", "中文"}, + {"/A +B", "/B+ C", "C +D"}, + { + "/A~`!@#$%^&*()-_+={};:'>,,, 0); + assertEquals(fileStatus[0].getPath().getName(), filename); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFilesystemAcl.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFilesystemAcl.java new file mode 100644 index 00000000000..acafe03be0d --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestAzureBlobFilesystemAcl.java @@ -0,0 +1,1264 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import com.google.common.collect.Lists; + +import java.io.FileNotFoundException; +import java.util.List; +import java.util.UUID; + +import org.junit.Assume; +import org.junit.Ignore; +import org.junit.Test; + +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.azurebfs.utils.AclTestHelpers; +import org.apache.hadoop.fs.permission.AclEntry; +import org.apache.hadoop.fs.permission.AclStatus; +import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.permission.FsPermission; + +import static org.junit.Assume.assumeTrue; + +import static org.apache.hadoop.fs.permission.AclEntryScope.ACCESS; +import static org.apache.hadoop.fs.permission.AclEntryScope.DEFAULT; +import static org.apache.hadoop.fs.permission.AclEntryType.USER; +import static org.apache.hadoop.fs.permission.AclEntryType.GROUP; +import static org.apache.hadoop.fs.permission.AclEntryType.OTHER; +import static org.apache.hadoop.fs.permission.AclEntryType.MASK; +import static org.apache.hadoop.fs.azurebfs.utils.AclTestHelpers.aclEntry; + +/** + * Test acl operations. + */ +public class ITestAzureBlobFilesystemAcl extends AbstractAbfsIntegrationTest { + private static final FsAction ALL = FsAction.ALL; + private static final FsAction NONE = FsAction.NONE; + private static final FsAction READ = FsAction.READ; + private static final FsAction READ_EXECUTE = FsAction.READ_EXECUTE; + private static final FsAction READ_WRITE = FsAction.READ_WRITE; + + private static final short RW = 0600; + private static final short RWX = 0700; + private static final short RWX_R = 0740; + private static final short RWX_RW = 0760; + private static final short RWX_RWX = 0770; + private static final short RWX_RX = 0750; + private static final short RWX_RX_RX = 0755; + private static final short RW_R = 0640; + private static final short RW_RW = 0660; + private static final short RW_RWX = 0670; + private static final short RW_R_R = 0644; + private static final short STICKY_RWX_RWX = 01770; + + private static Path testRoot = new Path("/test"); + private Path path; + + public ITestAzureBlobFilesystemAcl() throws Exception { + super(); + } + + @Test + public void testModifyAclEntries() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.mkdirs(path, FsPermission.createImmutable((short) RWX_RX)); + + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", READ_EXECUTE), + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE)); + fs.modifyAclEntries(path, aclSpec); + + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ_EXECUTE), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testModifyAclEntriesOnlyAccess() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", READ_EXECUTE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ_EXECUTE), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testModifyAclEntriesOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testModifyAclEntriesMinimal() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", READ_WRITE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ_WRITE), + aclEntry(ACCESS, GROUP, READ) }, returned); + assertPermission(fs, (short) RW_RW); + } + + @Test + public void testModifyAclEntriesMinimalDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testModifyAclEntriesCustomMask() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, MASK, NONE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ) }, returned); + assertPermission(fs, (short) RW); + } + + @Test + public void testModifyAclEntriesStickyBit() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) 01750)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", READ_EXECUTE), + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE)); + fs.modifyAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ_EXECUTE), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", READ_EXECUTE), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) 01750); + } + + @Test(expected=FileNotFoundException.class) + public void testModifyAclEntriesPathNotFound() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + // Path has not been created. + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE)); + fs.modifyAclEntries(path, aclSpec); + } + + @Test (expected=Exception.class) + public void testModifyAclEntriesDefaultOnFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.modifyAclEntries(path, aclSpec); + } + + @Test + public void testRemoveAclEntries() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo"), + aclEntry(DEFAULT, USER, "foo")); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveAclEntriesOnlyAccess() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, USER, "bar", READ_WRITE), + aclEntry(ACCESS, GROUP, READ_WRITE), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo")); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "bar", READ_WRITE), + aclEntry(ACCESS, GROUP, READ_WRITE) }, returned); + assertPermission(fs, (short) RWX_RW); + } + + @Test + public void testRemoveAclEntriesOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, USER, "bar", READ_EXECUTE)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo")); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "bar", READ_EXECUTE), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveAclEntriesMinimal() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RWX_RW)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_WRITE), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo"), + aclEntry(ACCESS, MASK)); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RWX_RW); + } + + @Test + public void testRemoveAclEntriesMinimalDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo"), + aclEntry(ACCESS, MASK), + aclEntry(DEFAULT, USER, "foo"), + aclEntry(DEFAULT, MASK)); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveAclEntriesStickyBit() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) 01750)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo"), + aclEntry(DEFAULT, USER, "foo")); + fs.removeAclEntries(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) 01750); + } + + @Test(expected=FileNotFoundException.class) + public void testRemoveAclEntriesPathNotFound() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + // Path has not been created. + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo")); + fs.removeAclEntries(path, aclSpec); + } + + @Test + public void testRemoveDefaultAcl() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.removeDefaultAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, (short) RWX_RWX); + } + + @Test + public void testRemoveDefaultAclOnlyAccess() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + fs.removeDefaultAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, (short) RWX_RWX); + } + + @Test + public void testRemoveDefaultAclOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.removeDefaultAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveDefaultAclMinimal() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + fs.removeDefaultAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveDefaultAclStickyBit() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) 01750)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.removeDefaultAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, (short) STICKY_RWX_RWX); + } + + @Test(expected=FileNotFoundException.class) + public void testRemoveDefaultAclPathNotFound() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + // Path has not been created. + fs.removeDefaultAcl(path); + } + + @Test + public void testRemoveAcl() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + + fs.setAcl(path, aclSpec); + fs.removeAcl(path); + + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testRemoveAclMinimalAcl() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + fs.removeAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RW_R); + } + + @Test + public void testRemoveAclStickyBit() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) 01750)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.removeAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) 01750); + } + + @Test + public void testRemoveAclOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.removeAcl(path); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test(expected=FileNotFoundException.class) + public void testRemoveAclPathNotFound() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + // Path has not been created. + fs.removeAcl(path); + } + + @Test + public void testSetAcl() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RWX); + } + + @Test + public void testSetAclOnlyAccess() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ) }, returned); + assertPermission(fs, (short) RW_R); + } + + @Test + public void testSetAclOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testSetAclMinimal() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, (short) RW_R); + } + + @Test + public void testSetAclMinimalDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX_RX); + } + + @Test + public void testSetAclCustomMask() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, MASK, ALL), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ) }, returned); + assertPermission(fs, (short) RW_RWX); + } + + @Test + public void testSetAclStickyBit() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) 01750)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) STICKY_RWX_RWX); + } + + @Test(expected=FileNotFoundException.class) + public void testSetAclPathNotFound() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + // Path has not been created. + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + } + + @Test(expected=Exception.class) + public void testSetAclDefaultOnFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + } + + @Test + public void testSetPermission() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.setPermission(path, FsPermission.createImmutable((short) RWX)); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX); + } + + @Test + public void testSetPermissionOnlyAccess() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + fs.create(path).close(); + fs.setPermission(path, FsPermission.createImmutable((short) RW_R)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, READ_WRITE), + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ), + aclEntry(ACCESS, OTHER, NONE)); + fs.setAcl(path, aclSpec); + fs.setPermission(path, FsPermission.createImmutable((short) RW)); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", READ), + aclEntry(ACCESS, GROUP, READ) }, returned); + assertPermission(fs, (short) RW); + } + + @Test + public void testSetPermissionOnlyDefault() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(ACCESS, OTHER, NONE), + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + fs.setPermission(path, FsPermission.createImmutable((short) RWX)); + AclStatus s = fs.getAclStatus(path); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, (short) RWX); + } + + @Test + public void testDefaultAclNewFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + Path filePath = new Path(path, "file1"); + fs.create(filePath).close(); + AclStatus s = fs.getAclStatus(filePath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, filePath, (short) RW_R); + } + + @Test + @Ignore // wait umask fix to be deployed + public void testOnlyAccessAclNewFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", ALL)); + fs.modifyAclEntries(path, aclSpec); + Path filePath = new Path(path, "file1"); + fs.create(filePath).close(); + AclStatus s = fs.getAclStatus(filePath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, filePath, (short) RW_R_R); + } + + @Test + @Ignore // wait investigation in service + public void testDefaultMinimalAclNewFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE)); + fs.setAcl(path, aclSpec); + Path filePath = new Path(path, "file1"); + fs.create(filePath).close(); + AclStatus s = fs.getAclStatus(filePath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, filePath, (short) RW_R); + } + + @Test + public void testDefaultAclNewDir() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + + Path dirPath = new Path(path, "dir1"); + fs.mkdirs(dirPath); + + AclStatus s = fs.getAclStatus(dirPath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, dirPath, (short) RWX_RWX); + } + + @Test + @Ignore // wait umask fix to be deployed + public void testOnlyAccessAclNewDir() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "foo", ALL)); + fs.modifyAclEntries(path, aclSpec); + Path dirPath = new Path(path, "dir1"); + fs.mkdirs(dirPath); + AclStatus s = fs.getAclStatus(dirPath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { }, returned); + assertPermission(fs, dirPath, (short) RWX_RX_RX); + } + + @Test + @Ignore // wait investigation in service + public void testDefaultMinimalAclNewDir() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE)); + fs.setAcl(path, aclSpec); + Path dirPath = new Path(path, "dir1"); + fs.mkdirs(dirPath); + AclStatus s = fs.getAclStatus(dirPath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, NONE) }, returned); + assertPermission(fs, dirPath, (short) RWX_RX); + } + + @Test + public void testDefaultAclNewFileWithMode() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + Path filePath = new Path(path, "file1"); + int bufferSize = 4 * 1024 * 1024; + fs.create(filePath, new FsPermission((short) RWX_R), false, bufferSize, + fs.getDefaultReplication(filePath), fs.getDefaultBlockSize(path), null) + .close(); + AclStatus s = fs.getAclStatus(filePath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE) }, returned); + assertPermission(fs, filePath, (short) RWX_R); + } + + @Test + public void testDefaultAclNewDirWithMode() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + FileSystem.mkdirs(fs, path, FsPermission.createImmutable((short) RWX_RX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(path, aclSpec); + Path dirPath = new Path(path, "dir1"); + fs.mkdirs(dirPath, new FsPermission((short) RWX_R)); + AclStatus s = fs.getAclStatus(dirPath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(new AclEntry[] { + aclEntry(ACCESS, USER, "foo", ALL), + aclEntry(ACCESS, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, READ_EXECUTE) }, returned); + assertPermission(fs, dirPath, (short) RWX_R); + } + + @Test + public void testDefaultAclRenamedFile() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + Path dirPath = new Path(path, "dir"); + FileSystem.mkdirs(fs, dirPath, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(dirPath, aclSpec); + Path filePath = new Path(path, "file1"); + fs.create(filePath).close(); + fs.setPermission(filePath, FsPermission.createImmutable((short) RW_R)); + Path renamedFilePath = new Path(dirPath, "file1"); + fs.rename(filePath, renamedFilePath); + AclEntry[] expected = new AclEntry[] { }; + AclStatus s = fs.getAclStatus(renamedFilePath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(expected, returned); + assertPermission(fs, renamedFilePath, (short) RW_R); + } + + @Test + public void testDefaultAclRenamedDir() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + path = new Path(testRoot, UUID.randomUUID().toString()); + Path dirPath = new Path(path, "dir"); + FileSystem.mkdirs(fs, dirPath, FsPermission.createImmutable((short) RWX_RX)); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL)); + fs.setAcl(dirPath, aclSpec); + Path subdirPath = new Path(path, "subdir"); + FileSystem.mkdirs(fs, subdirPath, FsPermission.createImmutable((short) RWX_RX)); + Path renamedSubdirPath = new Path(dirPath, "subdir"); + fs.rename(subdirPath, renamedSubdirPath); + AclEntry[] expected = new AclEntry[] { }; + AclStatus s = fs.getAclStatus(renamedSubdirPath); + AclEntry[] returned = s.getEntries().toArray(new AclEntry[0]); + assertArrayEquals(expected, returned); + assertPermission(fs, renamedSubdirPath, (short) RWX_RX); + } + + @Test + public void testEnsureAclOperationWorksForRoot() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + assumeTrue(fs.getIsNamespaceEnabeld()); + + Path rootPath = new Path("/"); + + List aclSpec1 = Lists.newArrayList( + aclEntry(DEFAULT, GROUP, "foo", ALL), + aclEntry(ACCESS, GROUP, "bar", ALL)); + fs.setAcl(rootPath, aclSpec1); + fs.getAclStatus(rootPath); + + fs.setOwner(rootPath, "", "testgroup"); + fs.setPermission(rootPath, new FsPermission("777")); + + List aclSpec2 = Lists.newArrayList( + aclEntry(DEFAULT, USER, "foo", ALL), + aclEntry(ACCESS, USER, "bar", ALL)); + fs.modifyAclEntries(rootPath, aclSpec2); + fs.removeAclEntries(rootPath, aclSpec2); + fs.removeDefaultAcl(rootPath); + fs.removeAcl(rootPath); + } + + @Test + public void testSetOwnerForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + + assertTrue(fs.exists(filePath)); + + FileStatus oldFileStatus = fs.getFileStatus(filePath); + fs.setOwner(filePath, "Alice", "testGroup"); + FileStatus newFileStatus = fs.getFileStatus(filePath); + + assertEquals(oldFileStatus.getOwner(), newFileStatus.getOwner()); + assertEquals(oldFileStatus.getGroup(), newFileStatus.getGroup()); + } + + @Test + public void testSetPermissionForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + + assertTrue(fs.exists(filePath)); + FsPermission oldPermission = fs.getFileStatus(filePath).getPermission(); + // default permission for non-namespace enabled account is "777" + FsPermission newPermission = new FsPermission("557"); + + assertNotEquals(oldPermission, newPermission); + + fs.setPermission(filePath, newPermission); + FsPermission updatedPermission = fs.getFileStatus(filePath).getPermission(); + assertEquals(oldPermission, updatedPermission); + } + + @Test + public void testModifyAclEntriesForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, GROUP, "foo", ALL), + aclEntry(ACCESS, GROUP, "bar", ALL)); + fs.modifyAclEntries(filePath, aclSpec); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + @Test + public void testRemoveAclEntriesEntriesForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, GROUP, "foo", ALL), + aclEntry(ACCESS, GROUP, "bar", ALL)); + fs.removeAclEntries(filePath, aclSpec); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + @Test + public void testRemoveDefaultAclForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + fs.removeDefaultAcl(filePath); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + @Test + public void testRemoveAclForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + fs.removeAcl(filePath); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + @Test + public void testSetAclForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, GROUP, "foo", ALL), + aclEntry(ACCESS, GROUP, "bar", ALL)); + fs.setAcl(filePath, aclSpec); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + @Test + public void testGetAclStatusForNonNamespaceEnabledAccount() throws Exception { + final AzureBlobFileSystem fs = this.getFileSystem(); + Assume.assumeTrue(!fs.getIsNamespaceEnabeld()); + final Path filePath = new Path(methodName.getMethodName()); + fs.create(filePath); + try { + AclStatus aclSpec = fs.getAclStatus(filePath); + assertFalse("UnsupportedOperationException is expected", false); + } catch (UnsupportedOperationException ex) { + //no-op + } + } + + private void assertPermission(FileSystem fs, short perm) throws Exception { + assertPermission(fs, path, perm); + } + + private void assertPermission(FileSystem fs, Path pathToCheck, short perm) + throws Exception { + AclTestHelpers.assertPermission(fs, pathToCheck, perm); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemInitialization.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemInitialization.java new file mode 100644 index 00000000000..8b60dd801cb --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemInitialization.java @@ -0,0 +1,77 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.net.URI; + +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +/** + * Test AzureBlobFileSystem initialization. + */ +public class ITestFileSystemInitialization extends AbstractAbfsIntegrationTest { + public ITestFileSystemInitialization() throws Exception { + super(); + } + + @Test + public void ensureAzureBlobFileSystemIsInitialized() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final String accountName = getAccountName(); + final String filesystem = getFileSystemName(); + + String scheme = this.getAuthType() == AuthType.SharedKey ? FileSystemUriSchemes.ABFS_SCHEME + : FileSystemUriSchemes.ABFS_SECURE_SCHEME; + assertEquals(fs.getUri(), + new URI(scheme, + filesystem + "@" + accountName, + null, + null, + null)); + assertNotNull("working directory", fs.getWorkingDirectory()); + } + + @Test + public void ensureSecureAzureBlobFileSystemIsInitialized() throws Exception { + final String accountName = getAccountName(); + final String filesystem = getFileSystemName(); + final URI defaultUri = new URI(FileSystemUriSchemes.ABFS_SECURE_SCHEME, + filesystem + "@" + accountName, + null, + null, + null); + Configuration rawConfig = getRawConfiguration(); + rawConfig.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, defaultUri.toString()); + + try(SecureAzureBlobFileSystem fs = (SecureAzureBlobFileSystem) FileSystem.newInstance(rawConfig)) { + assertEquals(fs.getUri(), new URI(FileSystemUriSchemes.ABFS_SECURE_SCHEME, + filesystem + "@" + accountName, + null, + null, + null)); + assertNotNull("working directory", fs.getWorkingDirectory()); + } + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemProperties.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemProperties.java new file mode 100644 index 00000000000..e6b45c8f8b8 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemProperties.java @@ -0,0 +1,119 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.util.Hashtable; + +import org.junit.Test; + +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; + +/** + * Test FileSystemProperties. + */ +public class ITestFileSystemProperties extends AbstractAbfsIntegrationTest { + private static final int TEST_DATA = 100; + private static final Path TEST_PATH = new Path("/testfile"); + public ITestFileSystemProperties() throws Exception { + } + + @Test + public void testReadWriteBytesToFileAndEnsureThreadPoolCleanup() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + testWriteOneByteToFileAndEnsureThreadPoolCleanup(); + + try(FSDataInputStream inputStream = fs.open(TEST_PATH, 4 * 1024 * 1024)) { + int i = inputStream.read(); + assertEquals(TEST_DATA, i); + } + } + + @Test + public void testWriteOneByteToFileAndEnsureThreadPoolCleanup() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + try(FSDataOutputStream stream = fs.create(TEST_PATH)) { + stream.write(TEST_DATA); + } + + FileStatus fileStatus = fs.getFileStatus(TEST_PATH); + assertEquals(1, fileStatus.getLen()); + } + + @Test + public void testBase64FileSystemProperties() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + + final Hashtable properties = new Hashtable<>(); + properties.put("key", "{ value: value }"); + fs.getAbfsStore().setFilesystemProperties(properties); + Hashtable fetchedProperties = fs.getAbfsStore().getFilesystemProperties(); + + assertEquals(properties, fetchedProperties); + } + + @Test + public void testBase64PathProperties() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Hashtable properties = new Hashtable<>(); + properties.put("key", "{ value: valueTest }"); + touch(TEST_PATH); + fs.getAbfsStore().setPathProperties(TEST_PATH, properties); + Hashtable fetchedProperties = + fs.getAbfsStore().getPathProperties(TEST_PATH); + + assertEquals(properties, fetchedProperties); + } + + @Test (expected = Exception.class) + public void testBase64InvalidFileSystemProperties() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Hashtable properties = new Hashtable<>(); + properties.put("key", "{ value: value歲 }"); + fs.getAbfsStore().setFilesystemProperties(properties); + Hashtable fetchedProperties = fs.getAbfsStore().getFilesystemProperties(); + + assertEquals(properties, fetchedProperties); + } + + @Test (expected = Exception.class) + public void testBase64InvalidPathProperties() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Hashtable properties = new Hashtable<>(); + properties.put("key", "{ value: valueTest兩 }"); + touch(TEST_PATH); + fs.getAbfsStore().setPathProperties(TEST_PATH, properties); + Hashtable fetchedProperties = fs.getAbfsStore().getPathProperties(TEST_PATH); + + assertEquals(properties, fetchedProperties); + } + + @Test + public void testSetFileSystemProperties() throws Exception { + final AzureBlobFileSystem fs = getFileSystem(); + final Hashtable properties = new Hashtable<>(); + properties.put("containerForDevTest", "true"); + fs.getAbfsStore().setFilesystemProperties(properties); + Hashtable fetchedProperties = fs.getAbfsStore().getFilesystemProperties(); + + assertEquals(properties, fetchedProperties); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemRegistration.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemRegistration.java new file mode 100644 index 00000000000..4393bd82b11 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestFileSystemRegistration.java @@ -0,0 +1,113 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.net.URI; + +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FileContext; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +/** + * Test AzureBlobFileSystem registration. + * Use casts to have interesting stack traces on failures. + */ +public class ITestFileSystemRegistration extends AbstractAbfsIntegrationTest { + + protected static final String ABFS = "org.apache.hadoop.fs.azurebfs.Abfs"; + protected static final String ABFSS = "org.apache.hadoop.fs.azurebfs.Abfss"; + + public ITestFileSystemRegistration() throws Exception { + } + + private void assertConfigMatches(Configuration conf, String key, String expected) { + String v = conf.get(key); + assertNotNull("No value for key " + key, v); + assertEquals("Wrong value for key " + key, expected, v); + } + + @Test + public void testAbfsFileSystemRegistered() throws Throwable { + assertConfigMatches(new Configuration(true), + "fs.abfs.impl", + "org.apache.hadoop.fs.azurebfs.AzureBlobFileSystem"); + } + + @Test + public void testSecureAbfsFileSystemRegistered() throws Throwable { + assertConfigMatches(new Configuration(true), + "fs.abfss.impl", + "org.apache.hadoop.fs.azurebfs.SecureAzureBlobFileSystem"); + } + + @Test + public void testAbfsFileContextRegistered() throws Throwable { + assertConfigMatches(new Configuration(true), + "fs.AbstractFileSystem.abfs.impl", + ABFS); + } + + @Test + public void testSecureAbfsFileContextRegistered() throws Throwable { + assertConfigMatches(new Configuration(true), + "fs.AbstractFileSystem.abfss.impl", + ABFSS); + } + + @Test + public void ensureAzureBlobFileSystemIsDefaultFileSystem() throws Exception { + Configuration rawConfig = getRawConfiguration(); + AzureBlobFileSystem fs = (AzureBlobFileSystem) FileSystem.get(rawConfig); + assertNotNull("filesystem", fs); + + if (this.getAuthType() == AuthType.OAuth) { + Abfss afs = (Abfss) FileContext.getFileContext(rawConfig).getDefaultFileSystem(); + assertNotNull("filecontext", afs); + } else { + Abfs afs = (Abfs) FileContext.getFileContext(rawConfig).getDefaultFileSystem(); + assertNotNull("filecontext", afs); + } + + } + + @Test + public void ensureSecureAzureBlobFileSystemIsDefaultFileSystem() throws Exception { + final String accountName = getAccountName(); + final String fileSystemName = getFileSystemName(); + + final URI defaultUri = new URI(FileSystemUriSchemes.ABFS_SECURE_SCHEME, + fileSystemName + "@" + accountName, + null, + null, + null); + Configuration rawConfig = getRawConfiguration(); + rawConfig.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, + defaultUri.toString()); + + SecureAzureBlobFileSystem fs = (SecureAzureBlobFileSystem) FileSystem.get(rawConfig); + assertNotNull("filesystem", fs); + Abfss afs = (Abfss) FileContext.getFileContext(rawConfig).getDefaultFileSystem(); + assertNotNull("filecontext", afs); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestWasbAbfsCompatibility.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestWasbAbfsCompatibility.java new file mode 100644 index 00000000000..33a5805ec98 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/ITestWasbAbfsCompatibility.java @@ -0,0 +1,192 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +import org.junit.Assume; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azure.NativeAzureFileSystem; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertDeleted; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertIsDirectory; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertMkdirs; +import static org.apache.hadoop.fs.contract.ContractTestUtils.assertPathExists; + +/** + * Test compatibility between ABFS client and WASB client. + */ +public class ITestWasbAbfsCompatibility extends AbstractAbfsIntegrationTest { + private static final String WASB_TEST_CONTEXT = "wasb test file"; + private static final String ABFS_TEST_CONTEXT = "abfs test file"; + private static final String TEST_CONTEXT = "THIS IS FOR TEST"; + + private static final Logger LOG = + LoggerFactory.getLogger(ITestWasbAbfsCompatibility.class); + + public ITestWasbAbfsCompatibility() throws Exception { + Assume.assumeFalse("Emulator is not supported", isIPAddress()); + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + } + + @Test + public void testListFileStatus() throws Exception { + // crate file using abfs + AzureBlobFileSystem fs = getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(fs.getIsNamespaceEnabeld()); + + NativeAzureFileSystem wasb = getWasbFileSystem(); + + Path path1 = new Path("/testfiles/~12/!008/3/abFsTestfile"); + try(FSDataOutputStream abfsStream = fs.create(path1, true)) { + abfsStream.write(ABFS_TEST_CONTEXT.getBytes()); + abfsStream.flush(); + abfsStream.hsync(); + } + + // create file using wasb + Path path2 = new Path("/testfiles/~12/!008/3/nativeFsTestfile"); + LOG.info("{}", wasb.getUri()); + try(FSDataOutputStream nativeFsStream = wasb.create(path2, true)) { + nativeFsStream.write(WASB_TEST_CONTEXT.getBytes()); + nativeFsStream.flush(); + nativeFsStream.hsync(); + } + // list file using abfs and wasb + FileStatus[] abfsFileStatus = fs.listStatus(new Path("/testfiles/~12/!008/3/")); + FileStatus[] nativeFsFileStatus = wasb.listStatus(new Path("/testfiles/~12/!008/3/")); + + assertEquals(2, abfsFileStatus.length); + assertEquals(2, nativeFsFileStatus.length); + } + + @Test + public void testReadFile() throws Exception { + boolean[] createFileWithAbfs = new boolean[]{false, true, false, true}; + boolean[] readFileWithAbfs = new boolean[]{false, true, true, false}; + + AzureBlobFileSystem abfs = getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(abfs.getIsNamespaceEnabeld()); + + NativeAzureFileSystem wasb = getWasbFileSystem(); + + for (int i = 0; i< 4; i++) { + Path path = new Path("/testReadFile/~12/!008/testfile" + i); + final FileSystem createFs = createFileWithAbfs[i] ? abfs : wasb; + + // Write + try(FSDataOutputStream nativeFsStream = createFs.create(path, true)) { + nativeFsStream.write(TEST_CONTEXT.getBytes()); + nativeFsStream.flush(); + nativeFsStream.hsync(); + } + + // Check file status + ContractTestUtils.assertIsFile(createFs, path); + + // Read + final FileSystem readFs = readFileWithAbfs[i] ? abfs : wasb; + + try(BufferedReader br =new BufferedReader(new InputStreamReader(readFs.open(path)))) { + String line = br.readLine(); + assertEquals("Wrong text from " + readFs, + TEST_CONTEXT, line); + } + + // Remove file + assertDeleted(readFs, path, true); + } + } + + @Test + public void testDir() throws Exception { + boolean[] createDirWithAbfs = new boolean[]{false, true, false, true}; + boolean[] readDirWithAbfs = new boolean[]{false, true, true, false}; + + AzureBlobFileSystem abfs = getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(abfs.getIsNamespaceEnabeld()); + + NativeAzureFileSystem wasb = getWasbFileSystem(); + + for (int i = 0; i < 4; i++) { + Path path = new Path("/testDir/t" + i); + //create + final FileSystem createFs = createDirWithAbfs[i] ? abfs : wasb; + assertTrue(createFs.mkdirs(path)); + //check + assertPathExists(createFs, "Created dir not found with " + createFs, path); + //read + final FileSystem readFs = readDirWithAbfs[i] ? abfs : wasb; + assertPathExists(readFs, "Created dir not found with " + readFs, + path); + assertIsDirectory(readFs, path); + assertDeleted(readFs, path, true); + } + } + + + @Test + public void testUrlConversion(){ + String abfsUrl = "abfs://abcde-1111-1111-1111-1111@xxxx.dfs.xxx.xxx.xxxx.xxxx"; + String wabsUrl = "wasb://abcde-1111-1111-1111-1111@xxxx.blob.xxx.xxx.xxxx.xxxx"; + assertEquals(abfsUrl, wasbUrlToAbfsUrl(wabsUrl)); + assertEquals(wabsUrl, abfsUrlToWasbUrl(abfsUrl)); + } + + @Test + public void testSetWorkingDirectory() throws Exception { + //create folders + AzureBlobFileSystem abfs = getFileSystem(); + // test only valid for non-namespace enabled account + Assume.assumeFalse(abfs.getIsNamespaceEnabeld()); + + NativeAzureFileSystem wasb = getWasbFileSystem(); + + Path d1d4 = new Path("/d1/d2/d3/d4"); + assertMkdirs(abfs, d1d4); + + //set working directory to path1 + Path path1 = new Path("/d1/d2"); + wasb.setWorkingDirectory(path1); + abfs.setWorkingDirectory(path1); + assertEquals(path1, wasb.getWorkingDirectory()); + assertEquals(path1, abfs.getWorkingDirectory()); + + //set working directory to path2 + Path path2 = new Path("d3/d4"); + wasb.setWorkingDirectory(path2); + abfs.setWorkingDirectory(path2); + + Path path3 = d1d4; + assertEquals(path3, wasb.getWorkingDirectory()); + assertEquals(path3, abfs.getWorkingDirectory()); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAbfsConfigurationFieldsValidation.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAbfsConfigurationFieldsValidation.java new file mode 100644 index 00000000000..eeed6cec872 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAbfsConfigurationFieldsValidation.java @@ -0,0 +1,179 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.apache.commons.codec.Charsets; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.IntegerConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.BooleanConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.StringConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.LongConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.annotations.ConfigurationValidationAnnotations.Base64StringConfigurationValidatorAnnotation; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.ConfigurationPropertyNotFoundException; +import org.apache.hadoop.fs.azurebfs.utils.Base64; + +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_SSL_CHANNEL_MODE_KEY; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_WRITE_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_MAX_RETRY_ATTEMPTS; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_BACKOFF_INTERVAL; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_MAX_BACKOFF_INTERVAL; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_MIN_BACKOFF_INTERVAL; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.MAX_AZURE_BLOCK_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.AZURE_BLOCK_LOCATION_HOST_DEFAULT; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; +import org.junit.Test; + +/** + * Test ConfigurationServiceFieldsValidation. + */ +public class TestAbfsConfigurationFieldsValidation { + private AbfsConfiguration abfsConfiguration; + + private static final String INT_KEY = "intKey"; + private static final String LONG_KEY = "longKey"; + private static final String STRING_KEY = "stringKey"; + private static final String BASE64_KEY = "base64Key"; + private static final String BOOLEAN_KEY = "booleanKey"; + private static final int DEFAULT_INT = 4194304; + private static final int DEFAULT_LONG = 4194304; + + private static final int TEST_INT = 1234565; + private static final int TEST_LONG = 4194304; + + private final String accountName; + private final String encodedString; + private final String encodedAccountKey; + + @IntegerConfigurationValidatorAnnotation(ConfigurationKey = INT_KEY, + MinValue = Integer.MIN_VALUE, + MaxValue = Integer.MAX_VALUE, + DefaultValue = DEFAULT_INT) + private int intField; + + @LongConfigurationValidatorAnnotation(ConfigurationKey = LONG_KEY, + MinValue = Long.MIN_VALUE, + MaxValue = Long.MAX_VALUE, + DefaultValue = DEFAULT_LONG) + private int longField; + + @StringConfigurationValidatorAnnotation(ConfigurationKey = STRING_KEY, + DefaultValue = "default") + private String stringField; + + @Base64StringConfigurationValidatorAnnotation(ConfigurationKey = BASE64_KEY, + DefaultValue = "base64") + private String base64Field; + + @BooleanConfigurationValidatorAnnotation(ConfigurationKey = BOOLEAN_KEY, + DefaultValue = false) + private boolean boolField; + + public TestAbfsConfigurationFieldsValidation() throws Exception { + super(); + this.accountName = "testaccount1.blob.core.windows.net"; + this.encodedString = Base64.encode("base64Value".getBytes(Charsets.UTF_8)); + this.encodedAccountKey = Base64.encode("someAccountKey".getBytes(Charsets.UTF_8)); + Configuration configuration = new Configuration(); + configuration.addResource(TestConfigurationKeys.TEST_CONFIGURATION_FILE_NAME); + configuration.set(INT_KEY, "1234565"); + configuration.set(LONG_KEY, "4194304"); + configuration.set(STRING_KEY, "stringValue"); + configuration.set(BASE64_KEY, encodedString); + configuration.set(BOOLEAN_KEY, "true"); + configuration.set(ConfigurationKeys.FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME + "." + accountName, this.encodedAccountKey); + abfsConfiguration = new AbfsConfiguration(configuration, accountName); + } + + @Test + public void testValidateFunctionsInConfigServiceImpl() throws Exception { + Field[] fields = this.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + if (field.isAnnotationPresent(IntegerConfigurationValidatorAnnotation.class)) { + assertEquals(TEST_INT, abfsConfiguration.validateInt(field)); + } else if (field.isAnnotationPresent(LongConfigurationValidatorAnnotation.class)) { + assertEquals(DEFAULT_LONG, abfsConfiguration.validateLong(field)); + } else if (field.isAnnotationPresent(StringConfigurationValidatorAnnotation.class)) { + assertEquals("stringValue", abfsConfiguration.validateString(field)); + } else if (field.isAnnotationPresent(Base64StringConfigurationValidatorAnnotation.class)) { + assertEquals(this.encodedString, abfsConfiguration.validateBase64String(field)); + } else if (field.isAnnotationPresent(BooleanConfigurationValidatorAnnotation.class)) { + assertEquals(true, abfsConfiguration.validateBoolean(field)); + } + } + } + + @Test + public void testConfigServiceImplAnnotatedFieldsInitialized() throws Exception { + // test that all the ConfigurationServiceImpl annotated fields have been initialized in the constructor + assertEquals(DEFAULT_WRITE_BUFFER_SIZE, abfsConfiguration.getWriteBufferSize()); + assertEquals(DEFAULT_READ_BUFFER_SIZE, abfsConfiguration.getReadBufferSize()); + assertEquals(DEFAULT_MIN_BACKOFF_INTERVAL, abfsConfiguration.getMinBackoffIntervalMilliseconds()); + assertEquals(DEFAULT_MAX_BACKOFF_INTERVAL, abfsConfiguration.getMaxBackoffIntervalMilliseconds()); + assertEquals(DEFAULT_BACKOFF_INTERVAL, abfsConfiguration.getBackoffIntervalMilliseconds()); + assertEquals(DEFAULT_MAX_RETRY_ATTEMPTS, abfsConfiguration.getMaxIoRetries()); + assertEquals(MAX_AZURE_BLOCK_SIZE, abfsConfiguration.getAzureBlockSize()); + assertEquals(AZURE_BLOCK_LOCATION_HOST_DEFAULT, abfsConfiguration.getAzureBlockLocationHost()); + } + + @Test + public void testGetAccountKey() throws Exception { + String accountKey = abfsConfiguration.getStorageAccountKey(); + assertEquals(this.encodedAccountKey, accountKey); + } + + @Test(expected = ConfigurationPropertyNotFoundException.class) + public void testGetAccountKeyWithNonExistingAccountName() throws Exception { + Configuration configuration = new Configuration(); + configuration.addResource(TestConfigurationKeys.TEST_CONFIGURATION_FILE_NAME); + configuration.unset(ConfigurationKeys.FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME); + AbfsConfiguration abfsConfig = new AbfsConfiguration(configuration, "bogusAccountName"); + abfsConfig.getStorageAccountKey(); + } + + @Test + public void testSSLSocketFactoryConfiguration() + throws InvalidConfigurationValueException, IllegalAccessException, IOException { + assertEquals(SSLSocketFactoryEx.SSLChannelMode.Default, abfsConfiguration.getPreferredSSLFactoryOption()); + assertNotEquals(SSLSocketFactoryEx.SSLChannelMode.Default_JSSE, abfsConfiguration.getPreferredSSLFactoryOption()); + assertNotEquals(SSLSocketFactoryEx.SSLChannelMode.OpenSSL, abfsConfiguration.getPreferredSSLFactoryOption()); + + Configuration configuration = new Configuration(); + configuration.setEnum(FS_AZURE_SSL_CHANNEL_MODE_KEY, SSLSocketFactoryEx.SSLChannelMode.Default_JSSE); + AbfsConfiguration localAbfsConfiguration = new AbfsConfiguration(configuration, accountName); + assertEquals(SSLSocketFactoryEx.SSLChannelMode.Default_JSSE, localAbfsConfiguration.getPreferredSSLFactoryOption()); + + configuration = new Configuration(); + configuration.setEnum(FS_AZURE_SSL_CHANNEL_MODE_KEY, SSLSocketFactoryEx.SSLChannelMode.OpenSSL); + localAbfsConfiguration = new AbfsConfiguration(configuration, accountName); + assertEquals(SSLSocketFactoryEx.SSLChannelMode.OpenSSL, localAbfsConfiguration.getPreferredSSLFactoryOption()); + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAccountConfiguration.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAccountConfiguration.java new file mode 100644 index 00000000000..a790cf21487 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/TestAccountConfiguration.java @@ -0,0 +1,285 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs; + +import java.io.IOException; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +/** + * Tests correct precedence of various configurations that might be returned. + * Configuration can be specified with the account name as a suffix to the + * config key, or without one. Account-specific values should be returned + * whenever they exist. Account-agnostic values are returned if they do not. + * Default values are returned if neither exists. + * + * These tests are in 2 main groups: tests of methods that allow default values + * (such as get and getPasswordString) are of one form, while tests of methods + * that do allow default values (all others) follow another form. + */ +public class TestAccountConfiguration { + + @Test + public void testStringPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + AbfsConfiguration abfsConf; + final Configuration conf = new Configuration(); + + final String accountName1 = "account1"; + final String accountName2 = "account2"; + final String accountName3 = "account3"; + + final String globalKey = "fs.azure.configuration"; + final String accountKey1 = globalKey + "." + accountName1; + final String accountKey2 = globalKey + "." + accountName2; + final String accountKey3 = globalKey + "." + accountName3; + + final String globalValue = "global"; + final String accountValue1 = "one"; + final String accountValue2 = "two"; + + conf.set(accountKey1, accountValue1); + conf.set(accountKey2, accountValue2); + conf.set(globalKey, globalValue); + + abfsConf = new AbfsConfiguration(conf, accountName1); + assertEquals("Wrong value returned when account-specific value was requested", + abfsConf.get(accountKey1), accountValue1); + assertEquals("Account-specific value was not returned when one existed", + abfsConf.get(globalKey), accountValue1); + + abfsConf = new AbfsConfiguration(conf, accountName2); + assertEquals("Wrong value returned when a different account-specific value was requested", + abfsConf.get(accountKey1), accountValue1); + assertEquals("Wrong value returned when account-specific value was requested", + abfsConf.get(accountKey2), accountValue2); + assertEquals("Account-agnostic value return even though account-specific value was set", + abfsConf.get(globalKey), accountValue2); + + abfsConf = new AbfsConfiguration(conf, accountName3); + assertNull("Account-specific value returned when none was set", + abfsConf.get(accountKey3)); + assertEquals("Account-agnostic value not returned when no account-specific value was set", + abfsConf.get(globalKey), globalValue); + } + + @Test + public void testPasswordPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + AbfsConfiguration abfsConf; + final Configuration conf = new Configuration(); + + final String accountName1 = "account1"; + final String accountName2 = "account2"; + final String accountName3 = "account3"; + + final String globalKey = "fs.azure.password"; + final String accountKey1 = globalKey + "." + accountName1; + final String accountKey2 = globalKey + "." + accountName2; + final String accountKey3 = globalKey + "." + accountName3; + + final String globalValue = "global"; + final String accountValue1 = "one"; + final String accountValue2 = "two"; + + conf.set(accountKey1, accountValue1); + conf.set(accountKey2, accountValue2); + conf.set(globalKey, globalValue); + + abfsConf = new AbfsConfiguration(conf, accountName1); + assertEquals("Wrong value returned when account-specific value was requested", + abfsConf.getPasswordString(accountKey1), accountValue1); + assertEquals("Account-specific value was not returned when one existed", + abfsConf.getPasswordString(globalKey), accountValue1); + + abfsConf = new AbfsConfiguration(conf, accountName2); + assertEquals("Wrong value returned when a different account-specific value was requested", + abfsConf.getPasswordString(accountKey1), accountValue1); + assertEquals("Wrong value returned when account-specific value was requested", + abfsConf.getPasswordString(accountKey2), accountValue2); + assertEquals("Account-agnostic value return even though account-specific value was set", + abfsConf.getPasswordString(globalKey), accountValue2); + + abfsConf = new AbfsConfiguration(conf, accountName3); + assertNull("Account-specific value returned when none was set", + abfsConf.getPasswordString(accountKey3)); + assertEquals("Account-agnostic value not returned when no account-specific value was set", + abfsConf.getPasswordString(globalKey), globalValue); + } + + @Test + public void testBooleanPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + + final String accountName = "account"; + final String globalKey = "fs.azure.bool"; + final String accountKey = globalKey + "." + accountName; + + final Configuration conf = new Configuration(); + final AbfsConfiguration abfsConf = new AbfsConfiguration(conf, accountName); + + conf.setBoolean(globalKey, false); + assertEquals("Default value returned even though account-agnostic config was set", + abfsConf.getBoolean(globalKey, true), false); + conf.unset(globalKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getBoolean(globalKey, true), true); + + conf.setBoolean(accountKey, false); + assertEquals("Default value returned even though account-specific config was set", + abfsConf.getBoolean(globalKey, true), false); + conf.unset(accountKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getBoolean(globalKey, true), true); + + conf.setBoolean(accountKey, true); + conf.setBoolean(globalKey, false); + assertEquals("Account-agnostic or default value returned even though account-specific config was set", + abfsConf.getBoolean(globalKey, false), true); + } + + @Test + public void testLongPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + + final String accountName = "account"; + final String globalKey = "fs.azure.long"; + final String accountKey = globalKey + "." + accountName; + + final Configuration conf = new Configuration(); + final AbfsConfiguration abfsConf = new AbfsConfiguration(conf, accountName); + + conf.setLong(globalKey, 0); + assertEquals("Default value returned even though account-agnostic config was set", + abfsConf.getLong(globalKey, 1), 0); + conf.unset(globalKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getLong(globalKey, 1), 1); + + conf.setLong(accountKey, 0); + assertEquals("Default value returned even though account-specific config was set", + abfsConf.getLong(globalKey, 1), 0); + conf.unset(accountKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getLong(globalKey, 1), 1); + + conf.setLong(accountKey, 1); + conf.setLong(globalKey, 0); + assertEquals("Account-agnostic or default value returned even though account-specific config was set", + abfsConf.getLong(globalKey, 0), 1); + } + + /** + * Dummy type used for testing handling of enums in configuration. + */ + public enum GetEnumType { + TRUE, FALSE + } + + @Test + public void testEnumPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + + final String accountName = "account"; + final String globalKey = "fs.azure.enum"; + final String accountKey = globalKey + "." + accountName; + + final Configuration conf = new Configuration(); + final AbfsConfiguration abfsConf = new AbfsConfiguration(conf, accountName); + + conf.setEnum(globalKey, GetEnumType.FALSE); + assertEquals("Default value returned even though account-agnostic config was set", + abfsConf.getEnum(globalKey, GetEnumType.TRUE), GetEnumType.FALSE); + conf.unset(globalKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getEnum(globalKey, GetEnumType.TRUE), GetEnumType.TRUE); + + conf.setEnum(accountKey, GetEnumType.FALSE); + assertEquals("Default value returned even though account-specific config was set", + abfsConf.getEnum(globalKey, GetEnumType.TRUE), GetEnumType.FALSE); + conf.unset(accountKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getEnum(globalKey, GetEnumType.TRUE), GetEnumType.TRUE); + + conf.setEnum(accountKey, GetEnumType.TRUE); + conf.setEnum(globalKey, GetEnumType.FALSE); + assertEquals("Account-agnostic or default value returned even though account-specific config was set", + abfsConf.getEnum(globalKey, GetEnumType.FALSE), GetEnumType.TRUE); + } + + /** + * Dummy type used for testing handling of classes in configuration. + */ + interface GetClassInterface { + } + + /** + * Dummy type used for testing handling of classes in configuration. + */ + private class GetClassImpl0 implements GetClassInterface { + } + + /** + * Dummy type used for testing handling of classes in configuration. + */ + private class GetClassImpl1 implements GetClassInterface { + } + + @Test + public void testClassPrecedence() + throws IllegalAccessException, IOException, InvalidConfigurationValueException { + + final String accountName = "account"; + final String globalKey = "fs.azure.class"; + final String accountKey = globalKey + "." + accountName; + + final Configuration conf = new Configuration(); + final AbfsConfiguration abfsConf = new AbfsConfiguration(conf, accountName); + + final Class class0 = GetClassImpl0.class; + final Class class1 = GetClassImpl1.class; + final Class xface = GetClassInterface.class; + + conf.setClass(globalKey, class0, xface); + assertEquals("Default value returned even though account-agnostic config was set", + abfsConf.getClass(globalKey, class1, xface), class0); + conf.unset(globalKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getClass(globalKey, class1, xface), class1); + + conf.setClass(accountKey, class0, xface); + assertEquals("Default value returned even though account-specific config was set", + abfsConf.getClass(globalKey, class1, xface), class0); + conf.unset(accountKey); + assertEquals("Default value not returned even though config was unset", + abfsConf.getClass(globalKey, class1, xface), class1); + + conf.setClass(accountKey, class1, xface); + conf.setClass(globalKey, class0, xface); + assertEquals("Account-agnostic or default value returned even though account-specific config was set", + abfsConf.getClass(globalKey, class0, xface), class1); + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/TestConfigurationKeys.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/TestConfigurationKeys.java new file mode 100644 index 00000000000..5565a4920e4 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/TestConfigurationKeys.java @@ -0,0 +1,41 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.constants; + +/** + * Responsible to keep all the Azure Blob File System configurations keys in Hadoop configuration file. + */ +public final class TestConfigurationKeys { + public static final String FS_AZURE_ACCOUNT_NAME = "fs.azure.account.name"; + public static final String FS_AZURE_ABFS_ACCOUNT_NAME = "fs.azure.abfs.account.name"; + public static final String FS_AZURE_ACCOUNT_KEY = "fs.azure.account.key"; + public static final String FS_AZURE_CONTRACT_TEST_URI = "fs.contract.test.fs.abfs"; + + public static final String FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_ID = "fs.azure.account.oauth2.contributor.client.id"; + public static final String FS_AZURE_BLOB_DATA_CONTRIBUTOR_CLIENT_SECRET = "fs.azure.account.oauth2.contributor.client.secret"; + + public static final String FS_AZURE_BLOB_DATA_READER_CLIENT_ID = "fs.azure.account.oauth2.reader.client.id"; + public static final String FS_AZURE_BLOB_DATA_READER_CLIENT_SECRET = "fs.azure.account.oauth2.reader.client.secret"; + + public static final String TEST_CONFIGURATION_FILE_NAME = "azure-test.xml"; + public static final String TEST_CONTAINER_PREFIX = "abfs-testcontainer-"; + public static final int TEST_TIMEOUT = 15 * 60 * 1000; + + private TestConfigurationKeys() {} +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java new file mode 100644 index 00000000000..109f887e29a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/constants/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.constants; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ABFSContractTestBinding.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ABFSContractTestBinding.java new file mode 100644 index 00000000000..79e295a99fd --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ABFSContractTestBinding.java @@ -0,0 +1,67 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import java.net.URI; + +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.AbstractAbfsIntegrationTest; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.services.AuthType; +import org.junit.Assume; + +/** + * Bind ABFS contract tests to the Azure test setup/teardown. + */ +public class ABFSContractTestBinding extends AbstractAbfsIntegrationTest { + private final URI testUri; + + public ABFSContractTestBinding() throws Exception { + this(true); + } + + public ABFSContractTestBinding( + final boolean useExistingFileSystem) throws Exception{ + if (useExistingFileSystem) { + AbfsConfiguration configuration = getConfiguration(); + String testUrl = configuration.get(TestConfigurationKeys.FS_AZURE_CONTRACT_TEST_URI); + Assume.assumeTrue("Contract tests are skipped because of missing config property :" + + TestConfigurationKeys.FS_AZURE_CONTRACT_TEST_URI, testUrl != null); + + if (getAuthType() != AuthType.SharedKey) { + testUrl = testUrl.replaceFirst(FileSystemUriSchemes.ABFS_SCHEME, FileSystemUriSchemes.ABFS_SECURE_SCHEME); + } + setTestUrl(testUrl); + + this.testUri = new URI(testUrl); + //Get container for contract tests + configuration.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, this.testUri.toString()); + String[] splitAuthority = this.testUri.getAuthority().split("\\@"); + setFileSystemName(splitAuthority[0]); + } else { + this.testUri = new URI(super.getTestUrl()); + } + } + + public boolean isSecureMode() { + return this.getAuthType() == AuthType.SharedKey ? false : true; + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/AbfsFileSystemContract.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/AbfsFileSystemContract.java new file mode 100644 index 00000000000..c0c5f91fabc --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/AbfsFileSystemContract.java @@ -0,0 +1,64 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; +import org.apache.hadoop.fs.azurebfs.utils.UriUtils; +import org.apache.hadoop.fs.contract.AbstractBondedFSContract; + +/** + * Azure BlobFileSystem Contract. Test paths are created using any maven fork + * identifier, if defined. This guarantees paths unique to tests + * running in parallel. + */ +public class AbfsFileSystemContract extends AbstractBondedFSContract { + + public static final String CONTRACT_XML = "abfs.xml"; + private final boolean isSecure; + + protected AbfsFileSystemContract(final Configuration conf, boolean secure) { + super(conf); + //insert the base features + addConfResource(CONTRACT_XML); + this.isSecure = secure; + } + + @Override + public String getScheme() { + return isSecure ? FileSystemUriSchemes.ABFS_SECURE_SCHEME + : FileSystemUriSchemes.ABFS_SCHEME; + } + + @Override + public Path getTestPath() { + return new Path(UriUtils.generateUniqueTestPath()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder( + "AbfsFileSystemContract{"); + sb.append("isSecure=").append(isSecure); + sb.append(super.toString()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractAppend.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractAppend.java new file mode 100644 index 00000000000..59df4f0deb8 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractAppend.java @@ -0,0 +1,61 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractAppendTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; +import org.junit.Test; + +import static org.apache.hadoop.fs.contract.ContractTestUtils.skip; + +/** + * Contract test for open operation. + */ +public class ITestAbfsFileSystemContractAppend extends AbstractContractAppendTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractAppend() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } + + @Override + @Test + public void testRenameFileBeingAppended() throws Throwable { + skip("Skipping as renaming an opened file is not supported"); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractConcat.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractConcat.java new file mode 100644 index 00000000000..c67e2bc5144 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractConcat.java @@ -0,0 +1,51 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractConcatTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for concat operation. + */ +public class ITestAbfsFileSystemContractConcat extends AbstractContractConcatTest{ + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractConcat() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractCreate.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractCreate.java new file mode 100644 index 00000000000..11d01641ead --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractCreate.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractCreateTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for create operation. + */ +public class ITestAbfsFileSystemContractCreate extends AbstractContractCreateTest{ + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractCreate() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDelete.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDelete.java new file mode 100644 index 00000000000..9d77829c6fb --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDelete.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractDeleteTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for delete operation. + */ +public class ITestAbfsFileSystemContractDelete extends AbstractContractDeleteTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractDelete() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDistCp.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDistCp.java new file mode 100644 index 00000000000..529fe831e2b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractDistCp.java @@ -0,0 +1,49 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.tools.contract.AbstractContractDistCpTest; + +/** + * Contract test for distCp operation. + */ +public class ITestAbfsFileSystemContractDistCp extends AbstractContractDistCpTest { + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractDistCp() throws Exception { + binding = new ABFSContractTestBinding(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbfsFileSystemContract createContract(Configuration conf) { + return new AbfsFileSystemContract(conf, false); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractGetFileStatus.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractGetFileStatus.java new file mode 100644 index 00000000000..f64abf9cb37 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractGetFileStatus.java @@ -0,0 +1,51 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractGetFileStatusTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for getFileStatus operation. + */ +public class ITestAbfsFileSystemContractGetFileStatus extends AbstractContractGetFileStatusTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractGetFileStatus() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return this.binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, this.isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractMkdir.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractMkdir.java new file mode 100644 index 00000000000..99959ed2d02 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractMkdir.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractMkdirTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for mkdir operation. + */ +public class ITestAbfsFileSystemContractMkdir extends AbstractContractMkdirTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractMkdir() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractOpen.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractOpen.java new file mode 100644 index 00000000000..43552e50b7a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractOpen.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractOpenTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for open operation. + */ +public class ITestAbfsFileSystemContractOpen extends AbstractContractOpenTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractOpen() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRename.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRename.java new file mode 100644 index 00000000000..b92bef68a09 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRename.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractRenameTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for rename operation. + */ +public class ITestAbfsFileSystemContractRename extends AbstractContractRenameTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractRename() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRootDirectory.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRootDirectory.java new file mode 100644 index 00000000000..5da2c76907e --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractRootDirectory.java @@ -0,0 +1,57 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractRootDirectoryTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; +import org.junit.Ignore; + +/** + * Contract test for root directory operation. + */ +public class ITestAbfsFileSystemContractRootDirectory extends AbstractContractRootDirectoryTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractRootDirectory() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } + + @Override + @Ignore("ABFS always return false when non-recursively remove root dir") + public void testRmNonEmptyRootDirNonRecursive() throws Throwable { + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSecureDistCp.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSecureDistCp.java new file mode 100644 index 00000000000..fa77c2e649c --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSecureDistCp.java @@ -0,0 +1,49 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.tools.contract.AbstractContractDistCpTest; + +/** + * Contract test for secure distCP operation. + */ +public class ITestAbfsFileSystemContractSecureDistCp extends AbstractContractDistCpTest { + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractSecureDistCp() throws Exception { + binding = new ABFSContractTestBinding(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbfsFileSystemContract createContract(Configuration conf) { + return new AbfsFileSystemContract(conf, true); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSeek.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSeek.java new file mode 100644 index 00000000000..35a5e1733d0 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSeek.java @@ -0,0 +1,52 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractSeekTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for seek operation. + */ +public class ITestAbfsFileSystemContractSeek extends AbstractContractSeekTest{ + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractSeek() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSetTimes.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSetTimes.java new file mode 100644 index 00000000000..40434842eb9 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAbfsFileSystemContractSetTimes.java @@ -0,0 +1,51 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.contract.AbstractContractSetTimesTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; + +/** + * Contract test for setTimes operation. + */ +public class ITestAbfsFileSystemContractSetTimes extends AbstractContractSetTimesTest { + private final boolean isSecure; + private final ABFSContractTestBinding binding; + + public ITestAbfsFileSystemContractSetTimes() throws Exception { + binding = new ABFSContractTestBinding(); + this.isSecure = binding.isSecureMode(); + } + + @Override + public void setup() throws Exception { + binding.setup(); + super.setup(); + } + + @Override + protected Configuration createConfiguration() { + return binding.getRawConfiguration(); + } + + @Override + protected AbstractFSContract createContract(final Configuration conf) { + return new AbfsFileSystemContract(conf, isSecure); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAzureBlobFileSystemBasics.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAzureBlobFileSystemBasics.java new file mode 100644 index 00000000000..a9fa2d77194 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/ITestAzureBlobFileSystemBasics.java @@ -0,0 +1,105 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.contract; + +import java.io.IOException; + +import org.apache.hadoop.fs.FileSystemContractBaseTest; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Basic Contract test for Azure BlobFileSystem. + */ +public class ITestAzureBlobFileSystemBasics extends FileSystemContractBaseTest { + private final ABFSContractTestBinding binding; + + public ITestAzureBlobFileSystemBasics() throws Exception { + // If all contract tests are running in parallel, some root level tests in FileSystemContractBaseTest will fail + // due to the race condition. Hence for this contract test it should be tested in different container + binding = new ABFSContractTestBinding(false); + } + + + @Before + public void setUp() throws Exception { + binding.setup(); + fs = binding.getFileSystem(); + } + + @Override + public void tearDown() throws Exception { + // This contract test is not using existing container for test, + // instead it creates its own temp container for test, hence we need to destroy + // it after the test. + try { + super.tearDown(); + } finally { + binding.teardown(); + } + } + + @Test + public void testListOnFolderWithNoChildren() throws IOException { + assertTrue(fs.mkdirs(path("testListStatus/c/1"))); + + FileStatus[] paths; + paths = fs.listStatus(path("testListStatus")); + assertEquals(1, paths.length); + + // ListStatus on folder with child + paths = fs.listStatus(path("testListStatus/c")); + assertEquals(1, paths.length); + + // Remove the child and listStatus + fs.delete(path("testListStatus/c/1"), true); + paths = fs.listStatus(path("testListStatus/c")); + assertEquals(0, paths.length); + assertTrue(fs.delete(path("testListStatus"), true)); + } + + @Test + public void testListOnfileAndFolder() throws IOException { + Path folderPath = path("testListStatus/folder"); + Path filePath = path("testListStatus/file"); + + assertTrue(fs.mkdirs(folderPath)); + ContractTestUtils.touch(fs, filePath); + + FileStatus[] listFolderStatus; + listFolderStatus = fs.listStatus(path("testListStatus")); + assertEquals(filePath, listFolderStatus[0].getPath()); + + //List on file should return absolute path + FileStatus[] listFileStatus = fs.listStatus(filePath); + assertEquals(filePath, listFileStatus[0].getPath()); + } + + @Override + @Ignore("Not implemented in ABFS yet") + public void testMkdirsWithUmask() throws Exception { + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/package-info.java new file mode 100644 index 00000000000..f3ff4834c0f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/contract/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.contract; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/TestConfigurationValidators.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/TestConfigurationValidators.java new file mode 100644 index 00000000000..f02eadc9a04 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/TestConfigurationValidators.java @@ -0,0 +1,121 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.diagnostics; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.InvalidConfigurationValueException; +import org.apache.hadoop.fs.azurebfs.utils.Base64; + +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.MIN_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.MAX_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_READ_BUFFER_SIZE; +import static org.apache.hadoop.fs.azurebfs.constants.FileSystemConfigurations.DEFAULT_WRITE_BUFFER_SIZE; + + +/** + * Test configuration validators. + */ +public class TestConfigurationValidators extends Assert { + + private static final String FAKE_KEY = "FakeKey"; + + public TestConfigurationValidators() throws Exception { + super(); + } + + @Test + public void testIntegerConfigValidator() throws Exception { + IntegerConfigurationBasicValidator integerConfigurationValidator = new IntegerConfigurationBasicValidator( + MIN_BUFFER_SIZE, MAX_BUFFER_SIZE, DEFAULT_READ_BUFFER_SIZE, FAKE_KEY, false); + + assertEquals(MIN_BUFFER_SIZE, (int) integerConfigurationValidator.validate("3072")); + assertEquals(DEFAULT_READ_BUFFER_SIZE, (int) integerConfigurationValidator.validate(null)); + assertEquals(MAX_BUFFER_SIZE, (int) integerConfigurationValidator.validate("104857600")); + } + + @Test(expected = InvalidConfigurationValueException.class) + public void testIntegerConfigValidatorThrowsIfMissingValidValue() throws Exception { + IntegerConfigurationBasicValidator integerConfigurationValidator = new IntegerConfigurationBasicValidator( + MIN_BUFFER_SIZE, MAX_BUFFER_SIZE, DEFAULT_READ_BUFFER_SIZE, FAKE_KEY, true); + integerConfigurationValidator.validate("3072"); + } + + @Test + public void testLongConfigValidator() throws Exception { + LongConfigurationBasicValidator longConfigurationValidator = new LongConfigurationBasicValidator( + MIN_BUFFER_SIZE, MAX_BUFFER_SIZE, DEFAULT_WRITE_BUFFER_SIZE, FAKE_KEY, false); + + assertEquals(DEFAULT_WRITE_BUFFER_SIZE, (long) longConfigurationValidator.validate(null)); + assertEquals(MIN_BUFFER_SIZE, (long) longConfigurationValidator.validate("3072")); + assertEquals(MAX_BUFFER_SIZE, (long) longConfigurationValidator.validate("104857600")); + } + + @Test(expected = InvalidConfigurationValueException.class) + public void testLongConfigValidatorThrowsIfMissingValidValue() throws Exception { + LongConfigurationBasicValidator longConfigurationValidator = new LongConfigurationBasicValidator( + MIN_BUFFER_SIZE, MAX_BUFFER_SIZE, DEFAULT_READ_BUFFER_SIZE, FAKE_KEY, true); + longConfigurationValidator.validate(null); + } + + @Test + public void testBooleanConfigValidator() throws Exception { + BooleanConfigurationBasicValidator booleanConfigurationValidator = new BooleanConfigurationBasicValidator(FAKE_KEY, false, false); + + assertEquals(true, booleanConfigurationValidator.validate("true")); + assertEquals(false, booleanConfigurationValidator.validate("False")); + assertEquals(false, booleanConfigurationValidator.validate(null)); + } + + @Test(expected = InvalidConfigurationValueException.class) + public void testBooleanConfigValidatorThrowsIfMissingValidValue() throws Exception { + BooleanConfigurationBasicValidator booleanConfigurationValidator = new BooleanConfigurationBasicValidator(FAKE_KEY, false, true); + booleanConfigurationValidator.validate("almostTrue"); + } + + @Test + public void testStringConfigValidator() throws Exception { + StringConfigurationBasicValidator stringConfigurationValidator = new StringConfigurationBasicValidator(FAKE_KEY, "value", false); + + assertEquals("value", stringConfigurationValidator.validate(null)); + assertEquals("someValue", stringConfigurationValidator.validate("someValue")); + } + + @Test(expected = InvalidConfigurationValueException.class) + public void testStringConfigValidatorThrowsIfMissingValidValue() throws Exception { + StringConfigurationBasicValidator stringConfigurationValidator = new StringConfigurationBasicValidator(FAKE_KEY, "value", true); + stringConfigurationValidator.validate(null); + } + + @Test + public void testBase64StringConfigValidator() throws Exception { + String encodedVal = Base64.encode("someValue".getBytes()); + Base64StringConfigurationBasicValidator base64StringConfigurationValidator = new Base64StringConfigurationBasicValidator(FAKE_KEY, "", false); + + assertEquals("", base64StringConfigurationValidator.validate(null)); + assertEquals(encodedVal, base64StringConfigurationValidator.validate(encodedVal)); + } + + @Test(expected = InvalidConfigurationValueException.class) + public void testBase64StringConfigValidatorThrowsIfMissingValidValue() throws Exception { + Base64StringConfigurationBasicValidator base64StringConfigurationValidator = new Base64StringConfigurationBasicValidator(FAKE_KEY, "value", true); + base64StringConfigurationValidator.validate("some&%Value"); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java new file mode 100644 index 00000000000..c3434acfc2c --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/diagnostics/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.diagnostics; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/package-info.java new file mode 100644 index 00000000000..811fdcb9f37 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClient.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClient.java new file mode 100644 index 00000000000..6a92bb2b4e3 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClient.java @@ -0,0 +1,86 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.net.URL; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.hadoop.fs.azurebfs.AbfsConfiguration; +import org.apache.hadoop.fs.azurebfs.utils.SSLSocketFactoryEx; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; + +/** + * Test useragent of abfs client. + * + */ +public final class TestAbfsClient { + + private final String accountName = "bogusAccountName"; + + private void validateUserAgent(String expectedPattern, + URL baseUrl, + AbfsConfiguration config, + boolean includeSSLProvider) { + AbfsClient client = new AbfsClient(baseUrl, null, + config, null, null); + String sslProviderName = null; + if (includeSSLProvider) { + sslProviderName = SSLSocketFactoryEx.getDefaultFactory().getProviderName(); + } + String userAgent = client.initializeUserAgent(config, sslProviderName); + Pattern pattern = Pattern.compile(expectedPattern); + Assert.assertTrue(pattern.matcher(userAgent).matches()); + } + + @Test + public void verifyUnknownUserAgent() throws Exception { + String expectedUserAgentPattern = "Azure Blob FS\\/1.0 \\(JavaJRE ([^\\)]+)\\)"; + final Configuration configuration = new Configuration(); + configuration.unset(ConfigurationKeys.FS_AZURE_USER_AGENT_PREFIX_KEY); + AbfsConfiguration abfsConfiguration = new AbfsConfiguration(configuration, accountName); + validateUserAgent(expectedUserAgentPattern, new URL("http://azure.com"), + abfsConfiguration, false); + } + + @Test + public void verifyUserAgent() throws Exception { + String expectedUserAgentPattern = "Azure Blob FS\\/1.0 \\(JavaJRE ([^\\)]+)\\) Partner Service"; + final Configuration configuration = new Configuration(); + configuration.set(ConfigurationKeys.FS_AZURE_USER_AGENT_PREFIX_KEY, "Partner Service"); + AbfsConfiguration abfsConfiguration = new AbfsConfiguration(configuration, accountName); + validateUserAgent(expectedUserAgentPattern, new URL("http://azure.com"), + abfsConfiguration, false); + } + + @Test + public void verifyUserAgentWithSSLProvider() throws Exception { + String expectedUserAgentPattern = "Azure Blob FS\\/1.0 \\(JavaJRE ([^\\)]+) SunJSSE-1.8\\) Partner Service"; + final Configuration configuration = new Configuration(); + configuration.set(ConfigurationKeys.FS_AZURE_USER_AGENT_PREFIX_KEY, "Partner Service"); + configuration.set(ConfigurationKeys.FS_AZURE_SSL_CHANNEL_MODE_KEY, + SSLSocketFactoryEx.SSLChannelMode.Default_JSSE.name()); + AbfsConfiguration abfsConfiguration = new AbfsConfiguration(configuration, accountName); + validateUserAgent(expectedUserAgentPattern, new URL("https://azure.com"), + abfsConfiguration, true); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClientThrottlingAnalyzer.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClientThrottlingAnalyzer.java new file mode 100644 index 00000000000..3f680e49930 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestAbfsClientThrottlingAnalyzer.java @@ -0,0 +1,177 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests for AbfsClientThrottlingAnalyzer. + */ +public class TestAbfsClientThrottlingAnalyzer { + private static final int ANALYSIS_PERIOD = 1000; + private static final int ANALYSIS_PERIOD_PLUS_10_PERCENT = ANALYSIS_PERIOD + + ANALYSIS_PERIOD / 10; + private static final long MEGABYTE = 1024 * 1024; + private static final int MAX_ACCEPTABLE_PERCENT_DIFFERENCE = 20; + + private void sleep(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void fuzzyValidate(long expected, long actual, double percentage) { + final double lowerBound = Math.max(expected - percentage / 100 * expected, 0); + final double upperBound = expected + percentage / 100 * expected; + + assertTrue( + String.format( + "The actual value %1$d is not within the expected range: " + + "[%2$.2f, %3$.2f].", + actual, + lowerBound, + upperBound), + actual >= lowerBound && actual <= upperBound); + } + + private void validate(long expected, long actual) { + assertEquals( + String.format("The actual value %1$d is not the expected value %2$d.", + actual, + expected), + expected, actual); + } + + private void validateLessThanOrEqual(long maxExpected, long actual) { + assertTrue( + String.format( + "The actual value %1$d is not less than or equal to the maximum" + + " expected value %2$d.", + actual, + maxExpected), + actual < maxExpected); + } + + /** + * Ensure that there is no waiting (sleepDuration = 0) if the metrics have + * never been updated. This validates proper initialization of + * ClientThrottlingAnalyzer. + */ + @Test + public void testNoMetricUpdatesThenNoWaiting() { + AbfsClientThrottlingAnalyzer analyzer = new AbfsClientThrottlingAnalyzer( + "test", + ANALYSIS_PERIOD); + validate(0, analyzer.getSleepDuration()); + sleep(ANALYSIS_PERIOD_PLUS_10_PERCENT); + validate(0, analyzer.getSleepDuration()); + } + + /** + * Ensure that there is no waiting (sleepDuration = 0) if the metrics have + * only been updated with successful requests. + */ + @Test + public void testOnlySuccessThenNoWaiting() { + AbfsClientThrottlingAnalyzer analyzer = new AbfsClientThrottlingAnalyzer( + "test", + ANALYSIS_PERIOD); + analyzer.addBytesTransferred(8 * MEGABYTE, false); + validate(0, analyzer.getSleepDuration()); + sleep(ANALYSIS_PERIOD_PLUS_10_PERCENT); + validate(0, analyzer.getSleepDuration()); + } + + /** + * Ensure that there is waiting (sleepDuration != 0) if the metrics have + * only been updated with failed requests. Also ensure that the + * sleepDuration decreases over time. + */ + @Test + public void testOnlyErrorsAndWaiting() { + AbfsClientThrottlingAnalyzer analyzer = new AbfsClientThrottlingAnalyzer( + "test", + ANALYSIS_PERIOD); + validate(0, analyzer.getSleepDuration()); + analyzer.addBytesTransferred(4 * MEGABYTE, true); + sleep(ANALYSIS_PERIOD_PLUS_10_PERCENT); + final int expectedSleepDuration1 = 1100; + validateLessThanOrEqual(expectedSleepDuration1, analyzer.getSleepDuration()); + sleep(10 * ANALYSIS_PERIOD); + final int expectedSleepDuration2 = 900; + validateLessThanOrEqual(expectedSleepDuration2, analyzer.getSleepDuration()); + } + + /** + * Ensure that there is waiting (sleepDuration != 0) if the metrics have + * only been updated with both successful and failed requests. Also ensure + * that the sleepDuration decreases over time. + */ + @Test + public void testSuccessAndErrorsAndWaiting() { + AbfsClientThrottlingAnalyzer analyzer = new AbfsClientThrottlingAnalyzer( + "test", + ANALYSIS_PERIOD); + validate(0, analyzer.getSleepDuration()); + analyzer.addBytesTransferred(8 * MEGABYTE, false); + analyzer.addBytesTransferred(2 * MEGABYTE, true); + sleep(ANALYSIS_PERIOD_PLUS_10_PERCENT); + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + analyzer.suspendIfNecessary(); + final int expectedElapsedTime = 126; + fuzzyValidate(expectedElapsedTime, + timer.elapsedTimeMs(), + MAX_ACCEPTABLE_PERCENT_DIFFERENCE); + sleep(10 * ANALYSIS_PERIOD); + final int expectedSleepDuration = 110; + validateLessThanOrEqual(expectedSleepDuration, analyzer.getSleepDuration()); + } + + /** + * Ensure that there is waiting (sleepDuration != 0) if the metrics have + * only been updated with many successful and failed requests. Also ensure + * that the sleepDuration decreases to zero over time. + */ + @Test + public void testManySuccessAndErrorsAndWaiting() { + AbfsClientThrottlingAnalyzer analyzer = new AbfsClientThrottlingAnalyzer( + "test", + ANALYSIS_PERIOD); + validate(0, analyzer.getSleepDuration()); + final int numberOfRequests = 20; + for (int i = 0; i < numberOfRequests; i++) { + analyzer.addBytesTransferred(8 * MEGABYTE, false); + analyzer.addBytesTransferred(2 * MEGABYTE, true); + } + sleep(ANALYSIS_PERIOD_PLUS_10_PERCENT); + ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); + analyzer.suspendIfNecessary(); + fuzzyValidate(7, + timer.elapsedTimeMs(), + MAX_ACCEPTABLE_PERCENT_DIFFERENCE); + sleep(10 * ANALYSIS_PERIOD); + validate(0, analyzer.getSleepDuration()); + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestOauthFailOverHttp.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestOauthFailOverHttp.java new file mode 100644 index 00000000000..de07c4b2b91 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestOauthFailOverHttp.java @@ -0,0 +1,55 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.net.URI; + +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.azurebfs.constants.FileSystemUriSchemes; + +import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME; +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.FS_AZURE_ABFS_ACCOUNT_NAME; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Test Oauth fail fast when uri scheme is incorrect. + */ +public class TestOauthFailOverHttp { + + @Test + public void testOauthFailWithSchemeAbfs() throws Exception { + Configuration conf = new Configuration(); + final String account = "fakeaccount.dfs.core.windows.net"; + conf.set(FS_AZURE_ABFS_ACCOUNT_NAME, account); + conf.setEnum(FS_AZURE_ACCOUNT_AUTH_TYPE_PROPERTY_NAME, AuthType.OAuth); + URI defaultUri = new URI(FileSystemUriSchemes.ABFS_SCHEME, + "fakecontainer@" + account, + null, + null, + null); + conf.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, defaultUri.toString()); + // IllegalArgumentException is expected + // when authenticating using Oauth and scheme is not abfss + intercept(IllegalArgumentException.class, "Incorrect URI", + () -> FileSystem.get(conf)); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestQueryParams.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestQueryParams.java new file mode 100644 index 00000000000..e6c6993b1dc --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestQueryParams.java @@ -0,0 +1,72 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.hadoop.fs.azurebfs.oauth2.QueryParams; +/** + * Test query params serialization. + */ +public class TestQueryParams { + private static final String SEPARATOR = "&"; + private static final String[][] PARAM_ARRAY = {{"K0", "V0"}, {"K1", "V1"}, {"K2", "V2"}}; + + @Test + public void testOneParam() { + String key = PARAM_ARRAY[0][0]; + String value = PARAM_ARRAY[0][1]; + + Map paramMap = new HashMap<>(); + paramMap.put(key, value); + + QueryParams qp = new QueryParams(); + qp.add(key, value); + Assert.assertEquals(key + "=" + value, qp.serialize()); + } + + @Test + public void testMultipleParams() { + QueryParams qp = new QueryParams(); + for (String[] entry : PARAM_ARRAY) { + qp.add(entry[0], entry[1]); + } + Map paramMap = constructMap(qp.serialize()); + Assert.assertEquals(PARAM_ARRAY.length, paramMap.size()); + + for (String[] entry : PARAM_ARRAY) { + Assert.assertTrue(paramMap.containsKey(entry[0])); + Assert.assertEquals(entry[1], paramMap.get(entry[0])); + } + } + + private Map constructMap(String input) { + String[] entries = input.split(SEPARATOR); + Map paramMap = new HashMap<>(); + for (String entry : entries) { + String[] keyValue = entry.split("="); + paramMap.put(keyValue[0], keyValue[1]); + } + return paramMap; + } + +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestShellDecryptionKeyProvider.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestShellDecryptionKeyProvider.java new file mode 100644 index 00000000000..b8df38eed0a --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/TestShellDecryptionKeyProvider.java @@ -0,0 +1,92 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.services; + +import java.io.File; +import java.nio.charset.Charset; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys; +import org.apache.hadoop.fs.azurebfs.contracts.exceptions.KeyProviderException; +import org.apache.hadoop.util.Shell; + +import static org.junit.Assert.assertEquals; + +/** + * Test ShellDecryptionKeyProvider. + * + */ +public class TestShellDecryptionKeyProvider { + public static final Log LOG = LogFactory + .getLog(TestShellDecryptionKeyProvider.class); + private static final File TEST_ROOT_DIR = new File(System.getProperty( + "test.build.data", "/tmp"), "TestShellDecryptionKeyProvider"); + + @Test + public void testScriptPathNotSpecified() throws Exception { + if (!Shell.WINDOWS) { + return; + } + ShellDecryptionKeyProvider provider = new ShellDecryptionKeyProvider(); + Configuration conf = new Configuration(); + String account = "testacct"; + String key = "key"; + + conf.set(ConfigurationKeys.FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME + account, key); + + try { + provider.getStorageAccountKey(account, conf); + Assert + .fail("fs.azure.shellkeyprovider.script is not specified, we should throw"); + } catch (KeyProviderException e) { + LOG.info("Received an expected exception: " + e.getMessage()); + } + } + + @Test + public void testValidScript() throws Exception { + if (!Shell.WINDOWS) { + return; + } + String expectedResult = "decretedKey"; + + // Create a simple script which echoes the given key plus the given + // expected result (so that we validate both script input and output) + File scriptFile = new File(TEST_ROOT_DIR, "testScript.cmd"); + FileUtils.writeStringToFile(scriptFile, "@echo %1 " + expectedResult, + Charset.forName("UTF-8")); + + ShellDecryptionKeyProvider provider = new ShellDecryptionKeyProvider(); + Configuration conf = new Configuration(); + String account = "testacct"; + String key = "key1"; + conf.set(ConfigurationKeys.FS_AZURE_ACCOUNT_KEY_PROPERTY_NAME + account, key); + conf.set(ConfigurationKeys.AZURE_KEY_ACCOUNT_SHELLKEYPROVIDER_SCRIPT, + "cmd /c " + scriptFile.getAbsolutePath()); + + String result = provider.getStorageAccountKey(account, conf); + assertEquals(key + " " + expectedResult, result); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/package-info.java new file mode 100644 index 00000000000..97c1d71251f --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/services/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.services; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AbfsTestUtils.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AbfsTestUtils.java new file mode 100644 index 00000000000..21edfe800e1 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AbfsTestUtils.java @@ -0,0 +1,85 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.blob.CloudBlobClient; +import com.microsoft.azure.storage.blob.CloudBlobContainer; + +import org.junit.Assume; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.fs.azure.AzureBlobStorageTestAccount; +import org.apache.hadoop.fs.azurebfs.AbstractAbfsIntegrationTest; +import org.apache.hadoop.fs.azurebfs.services.AuthType; + +import static org.apache.hadoop.fs.azurebfs.constants.TestConfigurationKeys.TEST_CONTAINER_PREFIX; + +/** + * Some Utils for ABFS tests. + */ +public final class AbfsTestUtils extends AbstractAbfsIntegrationTest{ + private static final Logger LOG = + LoggerFactory.getLogger(AbfsTestUtils.class); + + public AbfsTestUtils() throws Exception { + super(); + } + + /** + * If unit tests were interrupted and crushed accidentally, the test containers won't be deleted. + * In that case, dev can use this tool to list and delete all test containers. + * By default, all test container used in E2E tests sharing same prefix: "abfs-testcontainer-" + */ + + public void checkContainers() throws Throwable { + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + int count = 0; + CloudStorageAccount storageAccount = AzureBlobStorageTestAccount.createTestAccount(); + CloudBlobClient blobClient = storageAccount.createCloudBlobClient(); + Iterable containers + = blobClient.listContainers(TEST_CONTAINER_PREFIX); + for (CloudBlobContainer container : containers) { + count++; + LOG.info("Container {}, URI {}", + container.getName(), + container.getUri()); + } + LOG.info("Found {} test containers", count); + } + + + public void deleteContainers() throws Throwable { + Assume.assumeTrue(this.getAuthType() == AuthType.SharedKey); + int count = 0; + CloudStorageAccount storageAccount = AzureBlobStorageTestAccount.createTestAccount(); + CloudBlobClient blobClient = storageAccount.createCloudBlobClient(); + Iterable containers + = blobClient.listContainers(TEST_CONTAINER_PREFIX); + for (CloudBlobContainer container : containers) { + LOG.info("Container {} URI {}", + container.getName(), + container.getUri()); + if (container.deleteIfExists()) { + count++; + } + } + LOG.info("Deleted {} test containers", count); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AclTestHelpers.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AclTestHelpers.java new file mode 100644 index 00000000000..2ec97220492 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/AclTestHelpers.java @@ -0,0 +1,119 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import java.io.IOException; + +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.permission.AclEntry; +import org.apache.hadoop.fs.permission.AclEntryScope; +import org.apache.hadoop.fs.permission.AclEntryType; +import org.apache.hadoop.fs.permission.FsAction; + +import static org.junit.Assert.assertEquals; + +/** + * Helper methods useful for writing ACL tests. + */ +public final class AclTestHelpers { + + /** + * Create a new AclEntry with scope, type and permission (no name). + * + * @param scope AclEntryScope scope of the ACL entry + * @param type AclEntryType ACL entry type + * @param permission FsAction set of permissions in the ACL entry + * @return AclEntry new AclEntry + */ + public static AclEntry aclEntry(AclEntryScope scope, AclEntryType type, + FsAction permission) { + return new AclEntry.Builder() + .setScope(scope) + .setType(type) + .setPermission(permission) + .build(); + } + + /** + * Create a new AclEntry with scope, type, name and permission. + * + * @param scope AclEntryScope scope of the ACL entry + * @param type AclEntryType ACL entry type + * @param name String optional ACL entry name + * @param permission FsAction set of permissions in the ACL entry + * @return AclEntry new AclEntry + */ + public static AclEntry aclEntry(AclEntryScope scope, AclEntryType type, + String name, FsAction permission) { + return new AclEntry.Builder() + .setScope(scope) + .setType(type) + .setName(name) + .setPermission(permission) + .build(); + } + + /** + * Create a new AclEntry with scope, type and name (no permission). + * + * @param scope AclEntryScope scope of the ACL entry + * @param type AclEntryType ACL entry type + * @param name String optional ACL entry name + * @return AclEntry new AclEntry + */ + public static AclEntry aclEntry(AclEntryScope scope, AclEntryType type, + String name) { + return new AclEntry.Builder() + .setScope(scope) + .setType(type) + .setName(name) + .build(); + } + + /** + * Create a new AclEntry with scope and type (no name or permission). + * + * @param scope AclEntryScope scope of the ACL entry + * @param type AclEntryType ACL entry type + * @return AclEntry new AclEntry + */ + public static AclEntry aclEntry(AclEntryScope scope, AclEntryType type) { + return new AclEntry.Builder() + .setScope(scope) + .setType(type) + .build(); + } + + /** + * Asserts the value of the FsPermission bits on the inode of a specific path. + * + * @param fs FileSystem to use for check + * @param pathToCheck Path inode to check + * @param perm short expected permission bits + * @throws IOException thrown if there is an I/O error + */ + public static void assertPermission(FileSystem fs, Path pathToCheck, + short perm) throws IOException { + assertEquals(perm, fs.getFileStatus(pathToCheck).getPermission().toShort()); + } + + private AclTestHelpers() { + // Not called. + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/Parallelized.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/Parallelized.java new file mode 100644 index 00000000000..994b8ec07b6 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/Parallelized.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import org.junit.runners.Parameterized; +import org.junit.runners.model.RunnerScheduler; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Provided for convenience to execute parametrized test cases concurrently. + */ +public class Parallelized extends Parameterized { + + public Parallelized(Class classObj) throws Throwable { + super(classObj); + setScheduler(new ThreadPoolScheduler()); + } + + private static class ThreadPoolScheduler implements RunnerScheduler { + private ExecutorService executor; + + ThreadPoolScheduler() { + int numThreads = 10; + executor = Executors.newFixedThreadPool(numThreads); + } + + public void finished() { + executor.shutdown(); + try { + executor.awaitTermination(10, TimeUnit.MINUTES); + } catch (InterruptedException exc) { + throw new RuntimeException(exc); + } + } + + public void schedule(Runnable childStatement) { + executor.submit(childStatement); + } + } +} \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/TestUriUtils.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/TestUriUtils.java new file mode 100644 index 00000000000..690e56c5105 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/TestUriUtils.java @@ -0,0 +1,48 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.azurebfs.utils; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Test ABFS UriUtils. + */ +public final class TestUriUtils { + @Test + public void testIfUriContainsAbfs() throws Exception { + Assert.assertTrue(UriUtils.containsAbfsUrl("abfs.dfs.core.windows.net")); + Assert.assertTrue(UriUtils.containsAbfsUrl("abfs.dfs.preprod.core.windows.net")); + Assert.assertFalse(UriUtils.containsAbfsUrl("abfs.dfs.cores.windows.net")); + Assert.assertFalse(UriUtils.containsAbfsUrl("")); + Assert.assertFalse(UriUtils.containsAbfsUrl(null)); + Assert.assertFalse(UriUtils.containsAbfsUrl("abfs.dfs.cores.windows.net")); + Assert.assertFalse(UriUtils.containsAbfsUrl("xhdfs.blob.core.windows.net")); + } + + @Test + public void testExtractRawAccountName() throws Exception { + Assert.assertEquals("abfs", UriUtils.extractAccountNameFromHostName("abfs.dfs.core.windows.net")); + Assert.assertEquals("abfs", UriUtils.extractAccountNameFromHostName("abfs.dfs.preprod.core.windows.net")); + Assert.assertEquals(null, UriUtils.extractAccountNameFromHostName("abfs.dfs.cores.windows.net")); + Assert.assertEquals(null, UriUtils.extractAccountNameFromHostName("")); + Assert.assertEquals(null, UriUtils.extractAccountNameFromHostName(null)); + Assert.assertEquals(null, UriUtils.extractAccountNameFromHostName("abfs.dfs.cores.windows.net")); + } +} diff --git a/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java new file mode 100644 index 00000000000..d8cc940da1b --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/java/org/apache/hadoop/fs/azurebfs/utils/package-info.java @@ -0,0 +1,22 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +package org.apache.hadoop.fs.azurebfs.utils; +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; \ No newline at end of file diff --git a/hadoop-tools/hadoop-azure/src/test/resources/abfs.xml b/hadoop-tools/hadoop-azure/src/test/resources/abfs.xml new file mode 100644 index 00000000000..d065ace8b58 --- /dev/null +++ b/hadoop-tools/hadoop-azure/src/test/resources/abfs.xml @@ -0,0 +1,64 @@ + + + + + fs.contract.test.root-tests-enabled + false + + + + fs.contract.supports-append + true + + + + fs.contract.supports-seek + true + + + + fs.contract.rename-overwrites-dest + false + + + + fs.contract.rename-returns-false-if-source-missing + true + + + + fs.contract.rename-creates-dest-dirs + false + + + + fs.contract.supports-settimes + false + + + + fs.contract.supports-concat + false + + + + fs.contract.supports-getfilestatus + true + + diff --git a/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml b/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml index 8cea256de8c..a36a391cd56 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml +++ b/hadoop-tools/hadoop-azure/src/test/resources/azure-test.xml @@ -16,49 +16,39 @@ - + + + + fs.azure.test.emulator + false + - + + fs.azure.secure.mode + false + - - + + + fs.azure.user.agent.prefix MSFT - - - - + + + + - - - - fs.AbstractFileSystem.wasb.impl - org.apache.hadoop.fs.azure.Wasb - diff --git a/hadoop-tools/hadoop-azure/src/test/resources/log4j.properties b/hadoop-tools/hadoop-azure/src/test/resources/log4j.properties index a5e0c4f94e8..bac431d482d 100644 --- a/hadoop-tools/hadoop-azure/src/test/resources/log4j.properties +++ b/hadoop-tools/hadoop-azure/src/test/resources/log4j.properties @@ -24,3 +24,37 @@ log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %-5p [%t]: %c{2} (%F: log4j.logger.org.apache.hadoop.fs.azure.AzureFileSystemThreadPoolExecutor=DEBUG log4j.logger.org.apache.hadoop.fs.azure.BlockBlobAppendStream=DEBUG +log4j.logger.org.apache.hadoop.fs.azurebfs.contracts.services.TracingService=TRACE +log4j.logger.org.apache.hadoop.fs.azurebfs.services.AbfsClient=DEBUG + +# after here: turn off log messages from other parts of the system +# which only clutter test reports. +log4j.logger.org.apache.hadoop.util.NativeCodeLoader=ERROR +log4j.logger.org.apache.hadoop.conf.Configuration.deprecation=WARN +log4j.logger.org.apache.hadoop.util.GSet=WARN +# MiniDFS clusters can be noisy +log4j.logger.org.apache.hadoop.hdfs.server=ERROR +log4j.logger.org.apache.hadoop.metrics2=WARN +log4j.logger.org.apache.hadoop.net.NetworkTopology=WARN +log4j.logger.org.apache.hadoop.util.JvmPauseMonitor=WARN +log4j.logger.org.apache.hadoop.ipc=WARN +log4j.logger.org.apache.hadoop.http=WARN +log4j.logger.org.apache.hadoop.security.authentication.server.AuthenticationFilter=WARN +log4j.logger.org.apache.hadoop.util.HostsFileReader=WARN +log4j.logger.org.apache.commons.beanutils=WARN +log4j.logger.org.apache.hadoop.hdfs.StateChange=WARN +log4j.logger.BlockStateChange=WARN +log4j.logger.org.apache.hadoop.hdfs.DFSUtil=WARN +## YARN can be noisy too +log4j.logger.org.apache.hadoop.yarn.server.resourcemanager.scheduler=WARN +log4j.logger.org.apache.hadoop.yarn.server.nodemanager=WARN +log4j.logger.org.apache.hadoop.yarn.event=WARN +log4j.logger.org.apache.hadoop.yarn.util.ResourceCalculatorPlugin=ERROR +log4j.logger.org.apache.hadoop.yarn.server.nodemanager.containermanager.monitor=WARN +log4j.logger.org.apache.hadoop.mapred.IndexCache=WARN +log4j.logger.org.apache.hadoop.yarn.webapp.WebApps=WARN +log4j.logger.org.apache.hadoop.yarn.server.resourcemanager.security=WARN +log4j.logger.org.apache.hadoop.yarn.util.AbstractLivelinessMonitor=WARN +log4j.logger.org.apache.hadoop.security.token.delegation=WARN +log4j.logger.org.apache.hadoop.mapred.ShuffleHandler=WARN +log4j.logger.org.apache.hadoop.ipc.Server=WARN