diff --git a/build.gradle b/build.gradle index ead0cba08..6b3bbc298 100644 --- a/build.gradle +++ b/build.gradle @@ -305,6 +305,30 @@ dependencies { implementation 'org.slf4j:slf4j-api:1.7.36' zipArchive group: 'org.opensearch.plugin', name:'opensearch-security', version: "${opensearch_build}" + + // aws sdk v2 stack + api "software.amazon.awssdk:sdk-core:${versions.aws}" + api "software.amazon.awssdk:annotations:${versions.aws}" + api "software.amazon.awssdk:aws-core:${versions.aws}" + api "software.amazon.awssdk:auth:${versions.aws}" + api "software.amazon.awssdk:endpoints-spi:${versions.aws}" + api "software.amazon.awssdk:http-client-spi:${versions.aws}" + api "software.amazon.awssdk:apache-client:${versions.aws}" + api "software.amazon.awssdk:metrics-spi:${versions.aws}" + api "software.amazon.awssdk:profiles:${versions.aws}" + api "software.amazon.awssdk:regions:${versions.aws}" + api "software.amazon.awssdk:utils:${versions.aws}" + api "software.amazon.awssdk:aws-json-protocol:${versions.aws}" + api "software.amazon.awssdk:protocol-core:${versions.aws}" + api "software.amazon.awssdk:json-utils:${versions.aws}" + api "software.amazon.awssdk:third-party-jackson-core:${versions.aws}" + api "software.amazon.awssdk:s3:${versions.aws}" + api "software.amazon.awssdk:signer:${versions.aws}" + api "software.amazon.awssdk:aws-xml-protocol:${versions.aws}" + api "software.amazon.awssdk:aws-json-protocol:${versions.aws}" + api "software.amazon.awssdk:aws-query-protocol:${versions.aws}" + api "software.amazon.awssdk:sts:${versions.aws}" + api "software.amazon.awssdk:netty-nio-client:${versions.aws}" } task windowsPatches(type:Exec) { diff --git a/src/main/java/org/opensearch/knn/index/KNNSettings.java b/src/main/java/org/opensearch/knn/index/KNNSettings.java index b81a54124..6eff61d4b 100644 --- a/src/main/java/org/opensearch/knn/index/KNNSettings.java +++ b/src/main/java/org/opensearch/knn/index/KNNSettings.java @@ -98,7 +98,7 @@ public class KNNSettings { public static final boolean KNN_DEFAULT_FAISS_AVX2_DISABLED_VALUE = false; public static final boolean KNN_DEFAULT_FAISS_AVX512_DISABLED_VALUE = false; public static final String INDEX_KNN_DEFAULT_SPACE_TYPE = "l2"; - public static final Integer INDEX_KNN_ADVANCED_APPROXIMATE_THRESHOLD_DEFAULT_VALUE = 15_000; + public static final Integer INDEX_KNN_ADVANCED_APPROXIMATE_THRESHOLD_DEFAULT_VALUE = 0; public static final Integer INDEX_KNN_BUILD_VECTOR_DATA_STRUCTURE_THRESHOLD_MIN = -1; public static final Integer INDEX_KNN_BUILD_VECTOR_DATA_STRUCTURE_THRESHOLD_MAX = Integer.MAX_VALUE - 2; public static final String INDEX_KNN_DEFAULT_SPACE_TYPE_FOR_BINARY = "hamming"; diff --git a/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsWriter.java b/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsWriter.java index 7c8636577..a9ee93275 100644 --- a/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsWriter.java +++ b/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsWriter.java @@ -28,12 +28,16 @@ import org.opensearch.knn.index.VectorDataType; import org.opensearch.knn.index.codec.nativeindex.NativeIndexWriter; import org.opensearch.knn.index.quantizationservice.QuantizationService; +import org.opensearch.knn.index.vectorvalues.KNNFloatVectorValues; import org.opensearch.knn.index.vectorvalues.KNNVectorValues; +import org.opensearch.knn.index.vectorvalues.VectorValuesInputStream; import org.opensearch.knn.plugin.stats.KNNGraphValue; import org.opensearch.knn.quantization.models.quantizationParams.QuantizationParams; import org.opensearch.knn.quantization.models.quantizationState.QuantizationState; +import org.opensearch.knn.remote.index.s3.S3Client; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; @@ -42,7 +46,7 @@ import static org.opensearch.knn.index.vectorvalues.KNNVectorValuesFactory.getVectorValues; /** - * A KNNVectorsWriter class for writing the vector data strcutures and flat vectors for Native Engines. + * A KNNVectorsWriter class for writing the vector data structures and flat vectors for Native Engines. */ @Log4j2 public class NativeEngines990KnnVectorsWriter extends KnnVectorsWriter { @@ -54,6 +58,7 @@ public class NativeEngines990KnnVectorsWriter extends KnnVectorsWriter { private final List> fields = new ArrayList<>(); private boolean finished; private final Integer approximateThreshold; + private final S3Client s3Client; public NativeEngines990KnnVectorsWriter( SegmentWriteState segmentWriteState, @@ -63,6 +68,11 @@ public NativeEngines990KnnVectorsWriter( this.segmentWriteState = segmentWriteState; this.flatVectorsWriter = flatVectorsWriter; this.approximateThreshold = approximateThreshold; + try { + s3Client = S3Client.getInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -115,6 +125,7 @@ public void flush(int maxDoc, final Sorter.DocMap sortMap) throws IOException { ); continue; } + uploadToS3(fieldInfo, knnVectorValuesSupplier); final NativeIndexWriter writer = NativeIndexWriter.getWriter(fieldInfo, segmentWriteState, quantizationState); final KNNVectorValues knnVectorValues = knnVectorValuesSupplier.get(); @@ -167,6 +178,30 @@ public void mergeOneField(final FieldInfo fieldInfo, final MergeState mergeState log.debug("Merge took {} ms for vector field [{}]", time_in_millis, fieldInfo.getName()); } + private void uploadToS3(final FieldInfo fieldInfo, final Supplier> knnVectorValuesSupplier) { + // s3 uploader + String segmentName = segmentWriteState.segmentInfo.name; + String fieldName = fieldInfo.getName(); + String s3Key = segmentName + "_" + fieldName + ".s3vec"; + try (InputStream vectorInputStream = new VectorValuesInputStream((KNNFloatVectorValues) knnVectorValuesSupplier.get())) { + StopWatch stopWatch = new StopWatch().start(); + // Lets upload data to s3. + long totalBytesUploaded = s3Client.uploadWithProgress(vectorInputStream, s3Key); + long time_in_millis = stopWatch.stop().totalTime().millis(); + log.info( + "Time taken to upload vector for segment : {}, field: {}, totalBytes: {}, dimension: {} is : {}ms", + segmentName, + fieldInfo.getName(), + totalBytesUploaded, + fieldInfo.getVectorDimension(), + time_in_millis + ); + } catch (Exception e) { + // logging here as this is in internal error + log.error("Error while uploading data to s3.", e); + } + } + /** * Called once at the end before close */ diff --git a/src/main/java/org/opensearch/knn/index/vectorvalues/VectorValuesInputStream.java b/src/main/java/org/opensearch/knn/index/vectorvalues/VectorValuesInputStream.java new file mode 100644 index 000000000..a5f8df814 --- /dev/null +++ b/src/main/java/org/opensearch/knn/index/vectorvalues/VectorValuesInputStream.java @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.index.vectorvalues; + +import lombok.extern.log4j.Log4j2; +import org.apache.lucene.search.DocIdSetIterator; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@Log4j2 +public class VectorValuesInputStream extends InputStream { + private final KNNFloatVectorValues floatVectorValues; + private ByteBuffer currentBuffer; + private int position = 0; + + public VectorValuesInputStream(final KNNFloatVectorValues floatVectorValues) { + this.floatVectorValues = floatVectorValues; + try { + loadNextVector(); + } catch (IOException e) { + log.error("Failed to load initial vector", e); + } + } + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an {@code int} in the range {@code 0} to + * {@code 255}. If no byte is available because the end of the stream + * has been reached, the value {@code -1} is returned. This method + * blocks until input data is available, the end of the stream is detected, + * or an exception is thrown. + * + * @return the next byte of data, or {@code -1} if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + if (currentBuffer == null || position >= currentBuffer.capacity()) { + loadNextVector(); + if (currentBuffer == null) { + return -1; + } + } + + return currentBuffer.get(position++) & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (currentBuffer == null) { + return -1; + } + + int available = currentBuffer.capacity() - position; + if (available <= 0) { + loadNextVector(); + if (currentBuffer == null) { + return -1; + } + available = currentBuffer.capacity() - position; + } + + int bytesToRead = Math.min(available, len); + currentBuffer.position(position); + currentBuffer.get(b, off, bytesToRead); + position += bytesToRead; + + return bytesToRead; + } + + private void loadNextVector() throws IOException { + int docId = floatVectorValues.nextDoc(); + if (docId != -1 && docId != DocIdSetIterator.NO_MORE_DOCS) { + float[] vector = floatVectorValues.getVector(); + // this is a costly operation. We should optimize this + currentBuffer = ByteBuffer.allocate(vector.length * 4).order(ByteOrder.LITTLE_ENDIAN); + currentBuffer.asFloatBuffer().put(vector); + position = 0; + } else { + currentBuffer = null; + } + } +} diff --git a/src/main/java/org/opensearch/knn/remote/index/s3/ProgressCallback.java b/src/main/java/org/opensearch/knn/remote/index/s3/ProgressCallback.java new file mode 100644 index 000000000..03da8c015 --- /dev/null +++ b/src/main/java/org/opensearch/knn/remote/index/s3/ProgressCallback.java @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.remote.index.s3; + +public interface ProgressCallback { + void onProgress(long bytesUploaded); + + void onComplete(long totalBytesUploaded); + + void onUploadStarted(long bytesUploaded); +} diff --git a/src/main/java/org/opensearch/knn/remote/index/s3/S3Client.java b/src/main/java/org/opensearch/knn/remote/index/s3/S3Client.java new file mode 100644 index 000000000..e0441cc09 --- /dev/null +++ b/src/main/java/org/opensearch/knn/remote/index/s3/S3Client.java @@ -0,0 +1,226 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.remote.index.s3; + +import lombok.extern.log4j.Log4j2; +import org.opensearch.common.SuppressForbidden; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.CreateBucketResponse; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.UploadPartRequest; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * A simple S3 client to upload the data to S3 bucket. The class is thread + */ +@Log4j2 +public class S3Client { + private static volatile S3Client INSTANCE; + private static final int CHUNK_SIZE = 10 * 1024 * 1024; // 10MB chunk size + private static final String BUCKET_NAME = "remote-knn-index-build-navneet"; + + private static final Region REGION = Region.US_WEST_2; + private static software.amazon.awssdk.services.s3.S3AsyncClient s3AsyncClient; + + public static S3Client getInstance() throws IOException { + S3Client result = INSTANCE; + if (result == null) { + synchronized (S3Client.class) { + result = INSTANCE; + if (result == null) { + INSTANCE = result = new S3Client(); + } + } + } + return result; + } + + @SuppressForbidden(reason = "Need to provide this override to v2 SDK so that path does not default to home path") + private S3Client() throws IOException { + SocketAccess.doPrivilegedIOException(() -> { + if (ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.getStringValue().isEmpty()) { + System.setProperty( + ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.property(), + System.getProperty("opensearch.path.conf") + ); + } + if (ProfileFileSystemSetting.AWS_CONFIG_FILE.getStringValue().isEmpty()) { + System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), System.getProperty("opensearch.path.conf")); + } + // String accessKey = System.getenv("AWS_ACCESS_KEY_ID"); + String accessKey = ""; + // String secretKey = System.getenv("AWS_SECRET_ACCESS_KEY"); + String secretKey = ""; + + // String sessionToken = System.getenv("AWS_SESSION_TOKEN"); + String sessionToken = ""; + AwsSessionCredentials credentials = AwsSessionCredentials.create(accessKey, secretKey, sessionToken); + log.info("**********Credentials are: {}", credentials.toString()); + + software.amazon.awssdk.services.s3.S3AsyncClientBuilder builder = software.amazon.awssdk.services.s3.S3AsyncClient.builder() + .region(REGION) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .overrideConfiguration(ClientOverrideConfiguration.builder().defaultProfileFile(null).defaultProfileName(null).build()); + + // SpecialPermission.check(); + + s3AsyncClient = builder.build(); + if (doesBucketExist(BUCKET_NAME) == false) { + CreateBucketRequest createBucketRequest = CreateBucketRequest.builder().bucket(BUCKET_NAME).build(); + CreateBucketResponse response = s3AsyncClient.createBucket(createBucketRequest).get(); + log.info("**********Response is: {}", response.toString()); + } + return null; + }); + + } + + public void downloadFile() { + // TODO: Implement this later + } + + public long uploadWithProgress(final InputStream inputStream, final String key) { + ProgressCallback progressCallback = SimpleS3ProgressCallback.builder().bucket(BUCKET_NAME).key(key).build(); + long totalBytesUploaded = 0; + long totalBytesStarted = 0; + try { + CreateMultipartUploadRequest createMultipartUploadRequest = CreateMultipartUploadRequest.builder() + .bucket(BUCKET_NAME) + .key(key) + .build(); + + CreateMultipartUploadResponse multipartUpload = SocketAccess.doPrivilegedIOException( + () -> s3AsyncClient.createMultipartUpload(createMultipartUploadRequest).get() + ); + + String uploadId = multipartUpload.uploadId(); + List completedParts = new ArrayList<>(); + + byte[] buffer = new byte[CHUNK_SIZE]; + int partNumber = 1; + int bytesRead; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + List> completableFutureList = new ArrayList<>(); + // Read and upload parts + while ((bytesRead = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + + // Upload when we have at least 5MB or reached end of stream + if (baos.size() >= CHUNK_SIZE) { + byte[] partData = baos.toByteArray(); + + // Upload the part + UploadPartRequest uploadPartRequest = UploadPartRequest.builder() + .bucket(BUCKET_NAME) + .key(key) + .uploadId(uploadId) + .partNumber(partNumber) + .build(); + + CompletableFuture uploadPartResponse = SocketAccess.doPrivilegedIOException( + () -> s3AsyncClient.uploadPart(uploadPartRequest, AsyncRequestBody.fromBytes(partData)) + ); + completableFutureList.add(uploadPartResponse); + // CompletedPart part = CompletedPart.builder() + // .partNumber(partNumber) + // .eTag((uploadPartResponse).eTag()) + // .build(); + + // completedParts.add(part); + partNumber++; + baos.reset(); + totalBytesStarted += bytesRead; + progressCallback.onUploadStarted(totalBytesStarted); + totalBytesUploaded += bytesRead; + } + } + + if (baos.size() > 0) { + byte[] partData = baos.toByteArray(); + + // Upload the part + UploadPartRequest uploadPartRequest = UploadPartRequest.builder() + .bucket(BUCKET_NAME) + .key(key) + .uploadId(uploadId) + .partNumber(partNumber) + .build(); + + CompletableFuture uploadPartResponse = SocketAccess.doPrivileged( + () -> s3AsyncClient.uploadPart(uploadPartRequest, AsyncRequestBody.fromBytes(partData)) + ); + completableFutureList.add(uploadPartResponse); + totalBytesUploaded += partData.length; + } + + for (CompletableFuture future : completableFutureList) { + UploadPartResponse response = future.get(); + CompletedPart part = CompletedPart.builder().partNumber(partNumber).eTag((response).eTag()).build(); + completedParts.add(part); + // TODO: Fix this, add the way to find how many bytes are uploaded + // totalBytesUploaded += partData.length; + // progressCallback.onProgress(totalBytesUploaded); + } + + // Complete the multipart upload + CompletedMultipartUpload completedMultipartUpload = CompletedMultipartUpload.builder().parts(completedParts).build(); + + CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder() + .bucket(BUCKET_NAME) + .key(key) + .uploadId(uploadId) + .multipartUpload(completedMultipartUpload) + .build(); + + CompleteMultipartUploadResponse response = SocketAccess.doPrivilegedIOException( + () -> s3AsyncClient.completeMultipartUpload(completeRequest).get() + ); + log.debug("********** CompleteMultipartUploadResponse : {} **************", response); + progressCallback.onComplete(totalBytesUploaded); + return totalBytesUploaded; + } catch (Exception e) { + // Handle exceptions appropriately + throw new RuntimeException("Failed to upload file", e); + } + } + + public boolean doesBucketExist(String bucketName) throws ExecutionException, InterruptedException { + try { + s3AsyncClient.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()).get(); + return true; + } catch (NoSuchBucketException e) { + return false; + } catch (S3Exception e) { + if (e.statusCode() == 404) { + return false; + } + throw e; + } + } + +} diff --git a/src/main/java/org/opensearch/knn/remote/index/s3/SimpleS3ProgressCallback.java b/src/main/java/org/opensearch/knn/remote/index/s3/SimpleS3ProgressCallback.java new file mode 100644 index 000000000..43ddc4b50 --- /dev/null +++ b/src/main/java/org/opensearch/knn/remote/index/s3/SimpleS3ProgressCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.remote.index.s3; + +import lombok.Builder; +import lombok.Value; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Value +@Builder +public class SimpleS3ProgressCallback implements ProgressCallback { + + String key; + String bucket; + + @Override + public void onProgress(long bytesUploaded) { + log.info("Uploaded {} bytes for key {}, bucket: {}", bytesUploaded, key, bucket); + } + + @Override + public void onUploadStarted(long bytesUploaded) { + log.info("Upload started for {} bytes for key {}, bucket: {}", bytesUploaded, key, bucket); + } + + @Override + public void onComplete(long totalBytesUploaded) { + log.info("Uploaded {} bytes in total for key {}, bucket: {}", totalBytesUploaded, key, bucket); + } +} diff --git a/src/main/java/org/opensearch/knn/remote/index/s3/SocketAccess.java b/src/main/java/org/opensearch/knn/remote/index/s3/SocketAccess.java new file mode 100644 index 000000000..02d68cdd4 --- /dev/null +++ b/src/main/java/org/opensearch/knn/remote/index/s3/SocketAccess.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.remote.index.s3; + +import org.opensearch.SpecialPermission; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +public final class SocketAccess { + + private SocketAccess() {} + + public static T doPrivileged(PrivilegedAction operation) { + SpecialPermission.check(); + return AccessController.doPrivileged(operation); + } + + public static T doPrivilegedIOException(PrivilegedExceptionAction operation) throws IOException { + SpecialPermission.check(); + try { + return AccessController.doPrivileged(operation); + } catch (PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + public static void doPrivilegedVoid(Runnable action) { + SpecialPermission.check(); + AccessController.doPrivileged((PrivilegedAction) () -> { + action.run(); + return null; + }); + } + +} diff --git a/src/main/plugin-metadata/plugin-security.policy b/src/main/plugin-metadata/plugin-security.policy index ed329740f..0d2d67718 100644 --- a/src/main/plugin-metadata/plugin-security.policy +++ b/src/main/plugin-metadata/plugin-security.policy @@ -7,4 +7,36 @@ grant { permission java.net.SocketPermission "*", "connect,resolve"; permission java.lang.RuntimePermission "accessDeclaredMembers"; permission java.io.FilePermission "/proc/cpuinfo", "read"; + // needed because of problems in ClientConfiguration + // TODO: get these fixed in aws sdk + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "setContextClassLoader"; + // Needed because of problems in AmazonS3Client: + // When no region is set on a AmazonS3Client instance, the + // AWS SDK loads all known partitions from a JSON file and + // uses a Jackson's ObjectMapper for that: this one, in + // version 2.5.3 with the default binding options, tries + // to suppress access checks of ctor/field/method and thus + // requires this special permission. AWS must be fixed to + // uses Jackson correctly and have the correct modifiers + // on binded classes. + // TODO: get these fixed in aws sdk + // See https://github.com/aws/aws-sdk-java/issues/766 + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + + // s3 client opens socket connections for to access repository + permission java.net.SocketPermission "*", "connect"; + + // s3 client set Authenticator for proxy username/password + permission java.net.NetPermission "setDefaultAuthenticator"; + + // only for tests : org.opensearch.repositories.s3.S3RepositoryPlugin + permission java.util.PropertyPermission "opensearch.allow_insecure_settings", "read,write"; + permission java.util.PropertyPermission "aws.sharedCredentialsFile", "read,write"; + permission java.util.PropertyPermission "aws.configFile", "read,write"; + permission java.util.PropertyPermission "opensearch.path.conf", "read,write"; + permission java.io.FilePermission "config", "read"; + + permission java.lang.RuntimePermission "accessDeclaredMembers"; };