This is an automated email from the ASF dual-hosted git repository.
yasith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/master by this push:
new 82506ff512 fix: sharing init, storage mapping, SDK facades, and dev
infra (#614)
82506ff512 is described below
commit 82506ff512fa13290df1d5314fae765303a28d17
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Sat Apr 11 23:12:07 2026 -0500
fix: sharing init, storage mapping, SDK facades, and dev infra (#614)
* feat: airavata server fixes, SDK facades, auto-generated SFTP keys
- Fix sharing EntityRepository null-safe casting
- Fix ResearchMapper projectId/projectID mapping
- Update credential store encryption and key generation
- SDK facade clients updated for gRPC service alignment
- Tiltfile auto-generates SSH keypair for SFTP container (no static keys in
repo)
- DevStorageInitializer reads keypair from conf/sftp/ instead of generating
- Keycloak custom theme for dev login
- Gitignore conf/sftp/id_* generated keys
* fix: use proc check instead of ss for SFTP healthcheck (ss not available
in image)
* fix: map DataMovementInterfaceEntity to STORAGE_INTERFACE table instead
of DATA_MOVEMENT_INTERFACE
---
.gitignore | 3 +
Tiltfile | 17 +-
.../airavata/compute/util/SSHJStorageAdaptor.java | 250 +++++++++++++----
airavata-api/credential-service/pom.xml | 9 +
.../credential/model/CredentialEntity.java | 2 +-
.../apache/airavata/credential/util/Utility.java | 23 +-
airavata-api/iam-service/pom.xml | 6 +
.../orchestration/util/DevStorageInitializer.java | 135 ++++++++++
.../orchestration/util/ExpCatalogDBInitConfig.java | 49 ++++
.../airavata/research/mapper/ResearchMapper.java | 2 +
.../sharing/repository/EntityRepository.java | 8 +-
.../org/apache/airavata/grpc/GrpcStatusMapper.java | 8 +-
.../apache/airavata/task/AdaptorSupportImpl.java | 2 +-
.../storage/grpc/UserStorageGrpcService.java | 93 ++++++-
.../airavata/storage/mapper/StorageMapper.java | 18 +-
.../storage/model/DataMovementInterfaceEntity.java | 4 +-
airavata-python-sdk/airavata_sdk/client.py | 7 +-
airavata-python-sdk/airavata_sdk/facade/compute.py | 66 +++--
.../airavata_sdk/facade/credential.py | 18 +-
airavata-python-sdk/airavata_sdk/facade/iam.py | 15 +-
.../airavata_sdk/facade/research.py | 108 +++++---
airavata-python-sdk/airavata_sdk/facade/sharing.py | 15 +-
airavata-python-sdk/airavata_sdk/facade/storage.py | 15 +-
.../src/main/resources/application.properties | 4 +-
.../db/migration/airavata/V1__Baseline_schema.sql | 1 +
compose.yml | 21 ++
conf/keycloak/realm-default.json | 53 +---
.../themes/airavata/login/resources/css/custom.css | 297 +++++++++++++++++++++
.../themes/airavata/login/resources/img/logo.png | Bin 0 -> 5510 bytes
.../themes/airavata/login/theme.properties | 4 +
pom.xml | 7 +
31 files changed, 1032 insertions(+), 228 deletions(-)
diff --git a/.gitignore b/.gitignore
index c73fc685c9..80e16aa986 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Generated dev SSH keypair (created by Tiltfile on startup)
+conf/sftp/id_*
+
# From
https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
diff --git a/Tiltfile b/Tiltfile
index c7e5c93539..84a87cec15 100644
--- a/Tiltfile
+++ b/Tiltfile
@@ -1,6 +1,15 @@
# Airavata Development Tiltfile
# Usage: tilt up
+# --- Generate dev SSH keypair for SFTP container (idempotent) ---
+local(
+ 'mkdir -p conf/sftp && '
+ 'test -f conf/sftp/id_rsa || '
+ 'ssh-keygen -t rsa -b 2048 -f conf/sftp/id_rsa -N "" -q && '
+ 'echo "Generated dev SSH keypair at conf/sftp/id_rsa"',
+ quiet=True,
+)
+
# --- Infrastructure (from compose.yml) ---
docker_compose('./compose.yml')
@@ -9,16 +18,10 @@ local_resource(
'build',
cmd='mvn install -DskipTests -Dmaven.test.skip=true -T4 -q',
deps=[
- 'airavata-api/src',
- 'airavata-api/pom.xml',
- 'airavata-api/agent-service/src',
- 'airavata-api/agent-service/pom.xml',
- 'airavata-api/research-service/src',
- 'airavata-api/research-service/pom.xml',
- 'airavata-server/src',
'airavata-server/pom.xml',
],
ignore=['**/target/**'],
+ trigger_mode=TRIGGER_MODE_AUTO,
labels=['build'],
)
diff --git
a/airavata-api/compute-service/src/main/java/org/apache/airavata/compute/util/SSHJStorageAdaptor.java
b/airavata-api/compute-service/src/main/java/org/apache/airavata/compute/util/SSHJStorageAdaptor.java
index a5ecbf18a7..1d1e00289e 100644
---
a/airavata-api/compute-service/src/main/java/org/apache/airavata/compute/util/SSHJStorageAdaptor.java
+++
b/airavata-api/compute-service/src/main/java/org/apache/airavata/compute/util/SSHJStorageAdaptor.java
@@ -19,10 +19,24 @@
*/
package org.apache.airavata.compute.util;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
import java.util.Optional;
+
+import net.schmizz.sshj.SSHClient;
+import net.schmizz.sshj.sftp.FileAttributes;
+import net.schmizz.sshj.sftp.FileMode;
+import net.schmizz.sshj.sftp.RemoteResourceInfo;
+import net.schmizz.sshj.sftp.SFTPClient;
+import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
+import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
+
import org.apache.airavata.interfaces.AgentException;
import org.apache.airavata.interfaces.CommandOutput;
-import org.apache.airavata.interfaces.SSHConnectionService;
+import org.apache.airavata.interfaces.FileMetadata;
import org.apache.airavata.interfaces.StorageResourceAdaptor;
import
org.apache.airavata.model.appcatalog.storageresource.proto.StorageResourceDescription;
import org.apache.airavata.model.credential.store.proto.SSHCredential;
@@ -32,93 +46,223 @@ import
org.apache.airavata.model.data.movement.proto.SCPDataMovement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class SSHJStorageAdaptor extends SSHJAgentAdaptor implements
StorageResourceAdaptor {
-
- private static final Logger logger =
LoggerFactory.getLogger(SSHJStorageAdaptor.class);
+/**
+ * Simple SFTP-based storage adaptor using SSHJ directly.
+ * No connection pooling, no abstract hierarchy — just SSH + SFTP.
+ */
+public class SSHJStorageAdaptor implements StorageResourceAdaptor {
- public SSHJStorageAdaptor() {}
+ private static final Logger log =
LoggerFactory.getLogger(SSHJStorageAdaptor.class);
- public SSHJStorageAdaptor(SSHConnectionService sshConnectionService) {
- super(sshConnectionService);
- }
+ private String host;
+ private int port;
+ private String username;
+ private String privateKey;
+ private String passphrase;
@Override
public void init(String storageResourceId, String gatewayId, String
loginUser, String token) throws AgentException {
try {
- logger.info("Initializing Storage Resource SCP Adaptor for storage
resource : " + storageResourceId
- + ", gateway : " + gatewayId + ", user " + loginUser + ",
token : " + token);
+ log.info("Initializing SFTP adaptor: resource={}, gateway={},
user={}", storageResourceId, gatewayId, loginUser);
+
+ StorageResourceDescription sr =
AgentUtils.getRegistryServiceClient().getStorageResource(storageResourceId);
+
+ Optional<DataMovementInterface> dmOp =
sr.getDataMovementInterfacesList().stream()
+ .filter(iface -> iface.getDataMovementProtocol() ==
DataMovementProtocol.SCP)
+ .findFirst();
+
+ DataMovementInterface dm = dmOp.orElseThrow(() ->
+ new AgentException("No SCP data movement interface for
storage resource " + storageResourceId));
- StorageResourceDescription storageResourceDescription =
-
AgentUtils.getRegistryServiceClient().getStorageResource(storageResourceId);
+ SCPDataMovement scp =
AgentUtils.getRegistryServiceClient().getSCPDataMovement(dm.getDataMovementInterfaceId());
- logger.info("Fetching data movement interfaces for storage
resource " + storageResourceId);
+ SSHCredential cred =
AgentUtils.getCredentialClient().getSSHCredential(token, gatewayId);
+ if (cred == null) throw new AgentException("No credential for
token " + token);
- Optional<DataMovementInterface> dmInterfaceOp =
-
storageResourceDescription.getDataMovementInterfacesList().stream()
- .filter(iface -> iface.getDataMovementProtocol()
== DataMovementProtocol.SCP)
- .findFirst();
+ String altHost = scp.getAlternativeScpHostName();
+ this.host = (altHost != null && !altHost.isEmpty()) ? altHost :
sr.getHostName();
+ this.port = scp.getSshPort() == 0 ? 22 : scp.getSshPort();
+ this.username = loginUser;
+ this.privateKey = cred.getPrivateKey();
+ this.passphrase = cred.getPassphrase();
- DataMovementInterface scpInterface = dmInterfaceOp.orElseThrow(()
->
- new AgentException("Could not find a SCP interface for
storage resource " + storageResourceId));
+ log.info("SFTP adaptor ready: {}@{}:{}", username, host, port);
- SCPDataMovement scpDataMovement =
-
AgentUtils.getRegistryServiceClient().getSCPDataMovement(scpInterface.getDataMovementInterfaceId());
+ } catch (Exception e) {
+ log.error("Failed to init SFTP adaptor for " + storageResourceId,
e);
+ throw new AgentException("Failed to init SFTP adaptor for " +
storageResourceId, e);
+ }
+ }
+
+ private SFTPClient openSftp() throws Exception {
+ SSHClient ssh = new SSHClient();
+ ssh.addHostKeyVerifier(new PromiscuousVerifier());
+ ssh.connect(host, port);
- logger.info("Fetching credentials for cred store token " + token);
+ // Write PEM key to temp file since SSHJ loadKeys expects a file path
+ java.io.File keyFile = java.io.File.createTempFile("airavata-ssh-",
".pem");
+ try {
+ java.nio.file.Files.writeString(keyFile.toPath(), privateKey);
+ ssh.authPublickey(username,
ssh.loadKeys(keyFile.getAbsolutePath()));
+ } finally {
+ keyFile.delete();
+ }
+
+ return ssh.newSFTPClient();
+ }
- SSHCredential sshCredential =
AgentUtils.getCredentialClient().getSSHCredential(token, gatewayId);
- if (sshCredential == null) {
- throw new AgentException("Null credential for token " + token);
+ @Override
+ public List<String> listDirectory(String path) throws AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ List<RemoteResourceInfo> entries = sftp.ls(path);
+ List<String> names = new ArrayList<>();
+ for (RemoteResourceInfo entry : entries) {
+ names.add(entry.getName());
}
+ return names;
+ } catch (Exception e) {
+ log.error("Failed to list directory: " + path, e);
+ throw new AgentException("Failed to list directory: " + path, e);
+ }
+ }
+
+ @Override
+ public Boolean doesFileExist(String filePath) throws AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ sftp.stat(filePath);
+ return true;
+ } catch (net.schmizz.sshj.sftp.SFTPException e) {
+ if (e.getMessage().contains("No such file")) return false;
+ log.error("SFTP error checking file: " + filePath, e);
+ throw new AgentException("Failed to check file existence: " +
filePath, e);
+ } catch (Exception e) {
+ log.error("Failed to check file existence: " + filePath, e);
+ throw new AgentException("Failed to check file existence: " +
filePath, e);
+ }
+ }
- logger.info("Description for token : " + token + " : " +
sshCredential.getDescription());
+ @Override
+ public FileMetadata getFileMetadata(String remoteFile) throws
AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ FileAttributes attrs = sftp.stat(remoteFile);
+ FileMetadata meta = new FileMetadata();
+ meta.setName(remoteFile.substring(remoteFile.lastIndexOf('/') +
1));
+ meta.setSize(attrs.getSize());
+ meta.setDirectory(attrs.getType() == FileMode.Type.DIRECTORY);
+ return meta;
+ } catch (Exception e) {
+ throw new AgentException("Failed to get file metadata: " +
remoteFile, e);
+ }
+ }
- String alternateHostName =
scpDataMovement.getAlternativeScpHostName();
- String selectedHostName = (alternateHostName == null ||
"".equals(alternateHostName))
- ? storageResourceDescription.getHostName()
- : alternateHostName;
+ @Override
+ public
org.apache.airavata.model.appcatalog.storageresource.proto.StorageDirectoryInfo
getStorageDirectoryInfo(String location) throws AgentException {
+ return
org.apache.airavata.model.appcatalog.storageresource.proto.StorageDirectoryInfo.getDefaultInstance();
+ }
- int selectedPort = scpDataMovement.getSshPort() == 0 ? 22 :
scpDataMovement.getSshPort();
+ @Override
+ public void createDirectory(String path) throws AgentException {
+ createDirectory(path, false);
+ }
- // Use the parent's init method which delegates to
SSHConnectionService
- init(
- loginUser,
- selectedHostName,
- selectedPort,
- sshCredential.getPublicKey(),
- sshCredential.getPrivateKey(),
- sshCredential.getPassphrase());
+ @Override
+ public void createDirectory(String path, boolean recursive) throws
AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ if (recursive) {
+ sftp.mkdirs(path);
+ } else {
+ sftp.mkdir(path);
+ }
+ } catch (Exception e) {
+ throw new AgentException("Failed to create directory: " + path, e);
+ }
+ }
+ @Override
+ public void deleteDirectory(String path) throws AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ sftp.rmdir(path);
} catch (Exception e) {
- logger.error(
- "Error while initializing ssh agent for storage resource "
+ storageResourceId + " to token "
- + token,
- e);
- throw new AgentException(
- "Error while initializing ssh agent for storage resource "
+ storageResourceId + " to token "
- + token,
- e);
+ throw new AgentException("Failed to delete directory: " + path, e);
}
}
@Override
public void uploadFile(String localFile, String remoteFile) throws
AgentException {
- super.uploadFile(localFile, remoteFile);
+ try (SFTPClient sftp = openSftp()) {
+ sftp.put(localFile, remoteFile);
+ } catch (Exception e) {
+ throw new AgentException("Failed to upload file: " + localFile + "
-> " + remoteFile, e);
+ }
+ }
+
+ @Override
+ public void uploadFile(InputStream localInStream, FileMetadata metadata,
String remoteFile) throws AgentException {
+ // Write stream to temp file then upload
+ try (SFTPClient sftp = openSftp()) {
+ java.io.File tempFile =
java.io.File.createTempFile("airavata-upload-", ".tmp");
+ try {
+ java.nio.file.Files.copy(localInStream, tempFile.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
+ sftp.put(tempFile.getAbsolutePath(), remoteFile);
+ } finally {
+ tempFile.delete();
+ }
+ } catch (Exception e) {
+ throw new AgentException("Failed to upload stream to: " +
remoteFile, e);
+ }
}
@Override
public void downloadFile(String remoteFile, String localFile) throws
AgentException {
- super.downloadFile(remoteFile, localFile);
+ try (SFTPClient sftp = openSftp()) {
+ sftp.get(remoteFile, localFile);
+ } catch (Exception e) {
+ throw new AgentException("Failed to download file: " + remoteFile,
e);
+ }
+ }
+
+ @Override
+ public void downloadFile(String remoteFile, OutputStream localOutStream,
FileMetadata metadata) throws AgentException {
+ try (SFTPClient sftp = openSftp()) {
+ java.io.File tempFile =
java.io.File.createTempFile("airavata-download-", ".tmp");
+ try {
+ sftp.get(remoteFile, tempFile.getAbsolutePath());
+ java.nio.file.Files.copy(tempFile.toPath(), localOutStream);
+ } finally {
+ tempFile.delete();
+ }
+ } catch (Exception e) {
+ throw new AgentException("Failed to download file: " + remoteFile,
e);
+ }
}
@Override
public CommandOutput executeCommand(String command, String
workingDirectory) throws AgentException {
- return super.executeCommand(command, workingDirectory);
+ throw new AgentException("Command execution not supported on storage
resources");
+ }
+
+ @Override
+ public List<String> getFileNameFromExtension(String fileName, String
parentPath) throws AgentException {
+ List<String> result = new ArrayList<>();
+ try (SFTPClient sftp = openSftp()) {
+ for (RemoteResourceInfo entry : sftp.ls(parentPath)) {
+ if (entry.getName().endsWith(fileName)) {
+ result.add(entry.getName());
+ }
+ }
+ } catch (Exception e) {
+ throw new AgentException("Failed to search files by extension: " +
fileName, e);
+ }
+ return result;
+ }
+
+ @Override
+ public
org.apache.airavata.model.appcatalog.storageresource.proto.StorageVolumeInfo
getStorageVolumeInfo(String location) throws AgentException {
+ return
org.apache.airavata.model.appcatalog.storageresource.proto.StorageVolumeInfo.getDefaultInstance();
}
@Override
- public
org.apache.airavata.model.appcatalog.storageresource.proto.StorageVolumeInfo
getStorageVolumeInfo(
- String location) throws AgentException {
- return super.getStorageVolumeInfo(location);
+ public void destroy() {
+ // No persistent connections to clean up
}
}
diff --git a/airavata-api/credential-service/pom.xml
b/airavata-api/credential-service/pom.xml
index 02152a7781..354ce0fdbb 100644
--- a/airavata-api/credential-service/pom.xml
+++ b/airavata-api/credential-service/pom.xml
@@ -44,6 +44,15 @@ under the License.
<artifactId>iam-service</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- BouncyCastle (for SSH key generation/PEM encoding) -->
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcpkix-jdk18on</artifactId>
+ </dependency>
<!-- SSH -->
<dependency>
<groupId>com.hierynomus</groupId>
diff --git
a/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/model/CredentialEntity.java
b/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/model/CredentialEntity.java
index 6ecd8aa58d..f4c94cd246 100644
---
a/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/model/CredentialEntity.java
+++
b/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/model/CredentialEntity.java
@@ -36,7 +36,7 @@ public class CredentialEntity {
private String tokenId;
@Lob
- @Column(name = "CREDENTIAL", nullable = false)
+ @Column(name = "CREDENTIAL", nullable = false, columnDefinition =
"MEDIUMBLOB")
private byte[] credential;
@Column(name = "PORTAL_USER_ID", length = 256, nullable = false)
diff --git
a/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/util/Utility.java
b/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/util/Utility.java
index 6326ad410c..528eef25db 100644
---
a/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/util/Utility.java
+++
b/airavata-api/credential-service/src/main/java/org/apache/airavata/credential/util/Utility.java
@@ -24,6 +24,7 @@ import java.io.StringWriter;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
+import java.security.Security;
import java.security.interfaces.RSAPublicKey;
import java.text.DateFormat;
import java.text.ParseException;
@@ -45,6 +46,12 @@ public class Utility {
protected static Logger log = LoggerFactory.getLogger(Utility.class);
+ static {
+ if (Security.getProvider("BC") == null) {
+ Security.addProvider(new
org.bouncycastle.jce.provider.BouncyCastleProvider());
+ }
+ }
+
private static final String DATE_FORMAT = "MM/dd/yyyy HH:mm:ss";
public static String convertDateToString(Date date) {
@@ -94,22 +101,10 @@ public class Utility {
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
- // Encode private key in PEM format using BouncyCastle
+ // Encode private key in PEM format (unencrypted — credential
store handles encryption at rest)
StringWriter privateKeyWriter = new StringWriter();
try (JcaPEMWriter pemWriter = new JcaPEMWriter(privateKeyWriter)) {
- String passphrase = credential.getPassphrase();
- if (passphrase != null && !passphrase.isEmpty()) {
-
org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder
encryptorBuilder =
- new
org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder(
-
org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_aes256_CBC);
- encryptorBuilder.setPassword(passphrase.toCharArray());
- org.bouncycastle.operator.OutputEncryptor encryptor =
encryptorBuilder.build();
-
org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder
pkcs8Builder =
- new
org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder(kp.getPrivate());
- pemWriter.writeObject(pkcs8Builder.build(encryptor));
- } else {
- pemWriter.writeObject(new
JcaMiscPEMGenerator(kp.getPrivate()));
- }
+ pemWriter.writeObject(new
JcaMiscPEMGenerator(kp.getPrivate()));
}
String privateKeyPem = privateKeyWriter.toString();
diff --git a/airavata-api/iam-service/pom.xml b/airavata-api/iam-service/pom.xml
index 7ec71abde0..a623bb9dd9 100644
--- a/airavata-api/iam-service/pom.xml
+++ b/airavata-api/iam-service/pom.xml
@@ -57,6 +57,12 @@ under the License.
<optional>true</optional>
</dependency>
+ <!-- MapStruct -->
+ <dependency>
+ <groupId>org.mapstruct</groupId>
+ <artifactId>mapstruct</artifactId>
+ </dependency>
+
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
diff --git
a/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/DevStorageInitializer.java
b/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/DevStorageInitializer.java
new file mode 100644
index 0000000000..4574be358c
--- /dev/null
+++
b/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/DevStorageInitializer.java
@@ -0,0 +1,135 @@
+package org.apache.airavata.orchestration.util;
+
+import jakarta.annotation.PostConstruct;
+import org.apache.airavata.config.ServerSettings;
+import org.apache.airavata.credential.service.CredentialStoreService;
+import
org.apache.airavata.model.appcatalog.gatewayprofile.proto.StoragePreference;
+import
org.apache.airavata.model.appcatalog.storageresource.proto.StorageResourceDescription;
+import org.apache.airavata.model.credential.store.proto.SSHCredential;
+import org.apache.airavata.model.data.movement.proto.DMType;
+import org.apache.airavata.model.data.movement.proto.DataMovementProtocol;
+import org.apache.airavata.model.data.movement.proto.SCPDataMovement;
+import org.apache.airavata.model.data.movement.proto.SecurityProtocol;
+import org.apache.airavata.orchestration.service.RegistryServerHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Cold-start initializer that registers a dev SFTP storage resource, SSH
credential,
+ * and gateway storage preference so the portal Storage page works out of the
box.
+ *
+ * Reads the SSH keypair from conf/sftp/ (generated by Tiltfile on startup).
+ * Idempotent: checks if the storage resource already exists before creating.
+ */
+@Component
+@Order(200) // Run after ExpCatalogDBInitConfig and AppCatalogDBInitConfig
+public class DevStorageInitializer {
+
+ private static final Logger logger =
LoggerFactory.getLogger(DevStorageInitializer.class);
+
+ private static final String STORAGE_HOST = "localhost";
+ private static final String STORAGE_DESCRIPTION = "Dev SFTP storage
(atmoz/sftp container)";
+ private static final String SFTP_LOGIN_USER = "airavata";
+ private static final String SFTP_FS_ROOT = "/storage";
+ private static final String CREDENTIAL_DESCRIPTION = "Dev SFTP";
+
+ @Value("${dev.sftp.private-key-path:conf/sftp/id_rsa}")
+ private String privateKeyPath;
+
+ @Value("${dev.sftp.public-key-path:conf/sftp/id_rsa.pub}")
+ private String publicKeyPath;
+
+ @Autowired
+ private RegistryServerHandler registryHandler;
+
+ @Autowired
+ private CredentialStoreService credentialStoreService;
+
+ @PostConstruct
+ public void initialize() {
+ try {
+ String gatewayId = ServerSettings.getDefaultUserGateway();
+ String defaultUser = ServerSettings.getDefaultUser();
+
+ // Check if a storage resource for this host already exists
+ var allNames = registryHandler.getAllStorageResourceNames();
+ for (var entry : allNames.entrySet()) {
+ if (STORAGE_HOST.equals(entry.getValue())) {
+ logger.info("Dev storage resource already exists: {}
({})", entry.getValue(), entry.getKey());
+ return;
+ }
+ }
+
+ // 1. Register storage resource
+ StorageResourceDescription storageResource =
StorageResourceDescription.newBuilder()
+ .setHostName(STORAGE_HOST)
+ .setStorageResourceDescription(STORAGE_DESCRIPTION)
+ .setEnabled(true)
+ .build();
+ String storageResourceId =
registryHandler.registerStorageResource(storageResource);
+ logger.info("Registered dev storage resource: {} ({})",
STORAGE_HOST, storageResourceId);
+
+ // 2. Add SCP data movement interface
+ SCPDataMovement scpDm = SCPDataMovement.newBuilder()
+ .setSecurityProtocol(SecurityProtocol.SSH_KEYS)
+ .setSshPort(22)
+ .build();
+ registryHandler.addSCPDataMovementDetails(
+ storageResourceId, DMType.STORAGE_RESOURCE, 0, scpDm);
+ logger.info("Added SCP data movement interface for storage: {}",
storageResourceId);
+
+ // 3. Register SSH credential from Tiltfile-generated keypair
+ String privateKey = readKeyFile(privateKeyPath);
+ String publicKey = readKeyFile(publicKeyPath);
+ if (privateKey == null || publicKey == null) {
+ logger.warn("SSH keypair not found at {} / {} — run 'tilt up'
to generate", privateKeyPath, publicKeyPath);
+ return;
+ }
+ SSHCredential sshCredential = SSHCredential.newBuilder()
+ .setUsername(defaultUser)
+ .setGatewayId(gatewayId)
+ .setDescription(CREDENTIAL_DESCRIPTION)
+ .setPrivateKey(privateKey)
+ .setPublicKey(publicKey)
+ .build();
+ String credentialToken =
credentialStoreService.addSSHCredential(sshCredential);
+ logger.info("Registered SSH credential from {} for dev storage:
{}", privateKeyPath, credentialToken);
+
+ // 4. Add gateway storage preference
+ StoragePreference storagePreference =
StoragePreference.newBuilder()
+ .setStorageResourceId(storageResourceId)
+ .setLoginUserName(SFTP_LOGIN_USER)
+ .setFileSystemRootLocation(SFTP_FS_ROOT)
+ .setResourceSpecificCredentialStoreToken(credentialToken)
+ .build();
+ registryHandler.addGatewayStoragePreference(gatewayId,
storageResourceId, storagePreference);
+ logger.info("Added gateway storage preference for dev storage:
{}", storageResourceId);
+
+ logger.info("Dev storage cold-start initialization complete");
+
+ } catch (Exception e) {
+ logger.warn("Dev storage initialization failed (non-fatal): {}",
e.getMessage());
+ logger.debug("Dev storage init error details:", e);
+ }
+ }
+
+ private String readKeyFile(String path) {
+ try {
+ Path keyPath = Path.of(path);
+ if (Files.exists(keyPath)) {
+ return Files.readString(keyPath).trim();
+ }
+ } catch (IOException e) {
+ logger.warn("Failed to read key file {}: {}", path,
e.getMessage());
+ }
+ return null;
+ }
+}
diff --git
a/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/ExpCatalogDBInitConfig.java
b/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/ExpCatalogDBInitConfig.java
index 5c3783fbe2..5fbd5ddff3 100644
---
a/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/ExpCatalogDBInitConfig.java
+++
b/airavata-api/orchestration-service/src/main/java/org/apache/airavata/orchestration/util/ExpCatalogDBInitConfig.java
@@ -23,20 +23,28 @@ import org.apache.airavata.config.ServerSettings;
import org.apache.airavata.db.DBInitConfig;
import org.apache.airavata.db.JDBCConfig;
import org.apache.airavata.interfaces.GatewayRegistry;
+import org.apache.airavata.interfaces.SharingFacade;
import org.apache.airavata.model.user.proto.UserProfile;
import org.apache.airavata.model.workspace.proto.Gateway;
import org.apache.airavata.model.workspace.proto.GatewayApprovalStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ExpCatalogDBInitConfig implements DBInitConfig {
+ private static final Logger logger =
LoggerFactory.getLogger(ExpCatalogDBInitConfig.class);
+
private String dbInitScriptPrefix = "database_scripts/expcatalog";
@Autowired
private GatewayRegistry gatewayRegistry;
+ @Autowired
+ private SharingFacade sharingFacade;
+
@Override
public JDBCConfig getJDBCConfig() {
return null;
@@ -57,7 +65,44 @@ public class ExpCatalogDBInitConfig implements DBInitConfig {
return "CONFIGURATION";
}
+ private void initializeSharingForGateway(String gatewayId) {
+ // Domain
+ tryCreate("sharing domain", () ->
+ sharingFacade.createDomain(gatewayId, "Gateway " + gatewayId,
"Sharing domain for " + gatewayId));
+
+ // Entity types
+ String[] entityTypes = {"PROJECT", "EXPERIMENT", "FILE",
"APPLICATION_DEPLOYMENT", "GROUP_RESOURCE_PROFILE", "CREDENTIAL_TOKEN"};
+ for (String et : entityTypes) {
+ tryCreate("entity type " + et, () ->
+ sharingFacade.createEntityType(gatewayId + ":" + et,
gatewayId, et, et + " entity type"));
+ }
+
+ // Permission types
+ String[] permTypes = {"READ", "WRITE", "MANAGE_SHARING"};
+ for (String pt : permTypes) {
+ tryCreate("permission type " + pt, () ->
+ sharingFacade.createPermissionType(gatewayId + ":" + pt,
gatewayId, pt, pt + " permission type"));
+ }
+
+ logger.info("Sharing initialized for gateway: {}", gatewayId);
+ }
+
+ private void tryCreate(String label, ThrowingRunnable action) {
+ try {
+ action.run();
+ logger.info("Created {}", label);
+ } catch (Exception e) {
+ logger.debug("{} already exists: {}", label, e.getMessage());
+ }
+ }
+
+ @FunctionalInterface
+ private interface ThrowingRunnable {
+ void run() throws Exception;
+ }
+
@Override
+ @jakarta.annotation.PostConstruct
public void postInit() {
try {
@@ -71,8 +116,12 @@ public class ExpCatalogDBInitConfig implements DBInitConfig
{
.setOauthClientSecret(ServerSettings.getSetting("default.registry.oauth.client.secret"))
.build();
gatewayRegistry.addGateway(gateway);
+ logger.info("Created default gateway: {}", defaultGatewayId);
}
+ // Initialize sharing domain, entity types, and permission types
(idempotent)
+ initializeSharingForGateway(defaultGatewayId);
+
String defaultUsername = ServerSettings.getDefaultUser();
if (!gatewayRegistry.isUserExists(defaultGatewayId,
defaultUsername)) {
UserProfile defaultUser = UserProfile.newBuilder()
diff --git
a/airavata-api/research-service/src/main/java/org/apache/airavata/research/mapper/ResearchMapper.java
b/airavata-api/research-service/src/main/java/org/apache/airavata/research/mapper/ResearchMapper.java
index fa6efbf120..c75db099b5 100644
---
a/airavata-api/research-service/src/main/java/org/apache/airavata/research/mapper/ResearchMapper.java
+++
b/airavata-api/research-service/src/main/java/org/apache/airavata/research/mapper/ResearchMapper.java
@@ -109,8 +109,10 @@ public interface ResearchMapper extends
CommonMapperConversions {
}
// --- Project ---
+ @Mapping(target = "projectId", source = "projectID")
Project projectToModel(ProjectEntity entity);
+ @Mapping(target = "projectID", source = "projectId")
ProjectEntity projectToEntity(Project model);
// --- Notification ---
diff --git
a/airavata-api/sharing-service/src/main/java/org/apache/airavata/sharing/repository/EntityRepository.java
b/airavata-api/sharing-service/src/main/java/org/apache/airavata/sharing/repository/EntityRepository.java
index d59d617980..44436b77bf 100644
---
a/airavata-api/sharing-service/src/main/java/org/apache/airavata/sharing/repository/EntityRepository.java
+++
b/airavata-api/sharing-service/src/main/java/org/apache/airavata/sharing/repository/EntityRepository.java
@@ -155,10 +155,10 @@ public class EntityRepository extends
AbstractRepository<EntityEntity, EntityPK>
entity.setDescription((String) (rs[6]));
entity.setBinaryData((byte[]) (rs[7]));
entity.setFullText((String) (rs[8]));
- entity.setSharedCount((long) rs[9]);
- entity.setOriginalEntityCreationTime((long) (rs[10]));
- entity.setCreatedTime((long) (rs[11]));
- entity.setUpdatedTime((long) (rs[12]));
+ entity.setSharedCount(rs[9] != null ? ((Number) rs[9]).longValue()
: 0L);
+ entity.setOriginalEntityCreationTime(rs[10] != null ? ((Number)
rs[10]).longValue() : 0L);
+ entity.setCreatedTime(rs[11] != null ? ((Number)
rs[11]).longValue() : 0L);
+ entity.setUpdatedTime(rs[12] != null ? ((Number)
rs[12]).longValue() : 0L);
// Removing duplicates. Another option is to change the query to
remove duplicates.
if (!keys.containsKey(entity + domainId + "," +
entity.getEntityId())) {
diff --git
a/airavata-api/src/main/java/org/apache/airavata/grpc/GrpcStatusMapper.java
b/airavata-api/src/main/java/org/apache/airavata/grpc/GrpcStatusMapper.java
index 7b19883b1f..5dee614d2b 100644
--- a/airavata-api/src/main/java/org/apache/airavata/grpc/GrpcStatusMapper.java
+++ b/airavata-api/src/main/java/org/apache/airavata/grpc/GrpcStatusMapper.java
@@ -26,9 +26,15 @@ public final class GrpcStatusMapper {
private GrpcStatusMapper() {}
+ private static final int MAX_MESSAGE_LENGTH = 1024;
+
public static StatusRuntimeException toStatusException(Exception e) {
String className = e.getClass().getSimpleName();
- String message = e.getMessage() != null ? e.getMessage() : className;
+ String fullMessage = e.getMessage() != null ? e.getMessage() :
className;
+ // Truncate to avoid exceeding gRPC metadata size limits
+ String message = fullMessage.length() > MAX_MESSAGE_LENGTH
+ ? fullMessage.substring(0, MAX_MESSAGE_LENGTH) + "...
(truncated)"
+ : fullMessage;
Status status =
switch (className) {
diff --git
a/airavata-api/src/main/java/org/apache/airavata/task/AdaptorSupportImpl.java
b/airavata-api/src/main/java/org/apache/airavata/task/AdaptorSupportImpl.java
index 07cabc923e..383032d029 100644
---
a/airavata-api/src/main/java/org/apache/airavata/task/AdaptorSupportImpl.java
+++
b/airavata-api/src/main/java/org/apache/airavata/task/AdaptorSupportImpl.java
@@ -39,7 +39,7 @@ public class AdaptorSupportImpl implements AdaptorSupport {
private static final Logger logger =
LoggerFactory.getLogger(AdaptorSupportImpl.class);
private static final String SSHJ_AGENT_ADAPTOR_CLASS =
"org.apache.airavata.compute.util.SSHJAgentAdaptor";
- private static final String SSHJ_STORAGE_ADAPTOR_CLASS =
"org.apache.airavata.storage.util.SSHJStorageAdaptor";
+ private static final String SSHJ_STORAGE_ADAPTOR_CLASS =
"org.apache.airavata.compute.util.SSHJStorageAdaptor";
private static AdaptorSupportImpl INSTANCE;
diff --git
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
index 673a01c775..ea70d79a31 100644
---
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
+++
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/grpc/UserStorageGrpcService.java
@@ -62,17 +62,74 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
this.gatewayStoragePreferenceProvider =
gatewayStoragePreferenceProvider;
}
+ private StoragePreference resolveStoragePreference(String
storageResourceId) throws Exception {
+ RequestContext ctx = GrpcRequestContext.current();
+ var prefs =
gatewayStoragePreferenceProvider.getAllGatewayStoragePreferences(ctx.getGatewayId());
+ if (prefs == null || prefs.isEmpty()) {
+ return null;
+ }
+ String resolvedId = (storageResourceId != null &&
!storageResourceId.isEmpty())
+ ? storageResourceId : prefs.get(0).getStorageResourceId();
+ for (var pref : prefs) {
+ if (pref.getStorageResourceId().equals(resolvedId)) {
+ return pref;
+ }
+ }
+ return prefs.get(0);
+ }
+
private StorageResourceAdaptor getStorageAdaptor(String storageResourceId)
throws Exception {
RequestContext ctx = GrpcRequestContext.current();
+ String resolvedId = storageResourceId;
+ String credentialToken = ctx.getAccessToken(); // fallback
+ String loginUser = ctx.getUserId(); // fallback
+
+ // Resolve storage resource, credential, and login user from gateway
preferences
+ StoragePreference pref = resolveStoragePreference(storageResourceId);
+ if (pref != null) {
+ if (resolvedId == null || resolvedId.isEmpty()) {
+ resolvedId = pref.getStorageResourceId();
+ }
+ String csToken = pref.getResourceSpecificCredentialStoreToken();
+ if (csToken != null && !csToken.isEmpty()) {
+ credentialToken = csToken;
+ }
+ String prefUser = pref.getLoginUserName();
+ if (prefUser != null && !prefUser.isEmpty()) {
+ loginUser = prefUser;
+ }
+ }
+ if (resolvedId == null || resolvedId.isEmpty()) {
+ throw new IllegalStateException("No storage resource configured
for gateway " + ctx.getGatewayId());
+ }
return adaptorSupport.fetchStorageAdaptor(
- ctx.getGatewayId(), storageResourceId,
DataMovementProtocol.SCP, ctx.getAccessToken(), ctx.getUserId());
+ ctx.getGatewayId(), resolvedId, DataMovementProtocol.SCP,
credentialToken, loginUser);
+ }
+
+ /**
+ * Resolve paths like "~/" or "~" to the storage preference's
fileSystemRootLocation.
+ * SFTP doesn't support shell tilde expansion.
+ */
+ private String resolvePath(String path, String storageResourceId) throws
Exception {
+ if (path == null || path.isEmpty()) {
+ path = "/";
+ }
+ if (path.startsWith("~/") || path.equals("~")) {
+ StoragePreference pref =
resolveStoragePreference(storageResourceId);
+ String root = (pref != null &&
!pref.getFileSystemRootLocation().isEmpty())
+ ? pref.getFileSystemRootLocation() : "/";
+ if (!root.endsWith("/")) root += "/";
+ String suffix = path.length() > 2 ? path.substring(2) : "";
+ path = root + suffix;
+ }
+ return path;
}
@Override
public void uploadFile(UploadFileRequest request,
StreamObserver<DataProductModel> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- String remotePath = request.getPath();
+ String remotePath = resolvePath(request.getPath(),
request.getStorageResourceId());
FileMetadata metadata = new FileMetadata();
metadata.setName(request.getName());
@@ -101,7 +158,7 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void downloadFile(DownloadFileRequest request,
StreamObserver<DownloadFileResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- String remotePath = request.getPath();
+ String remotePath = resolvePath(request.getPath(),
request.getStorageResourceId());
FileMetadata metadata = adaptor.getFileMetadata(remotePath);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
@@ -121,7 +178,8 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void fileExists(FileExistsRequest request,
StreamObserver<FileExistsResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- boolean exists = adaptor.doesFileExist(request.getPath());
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
+ boolean exists = adaptor.doesFileExist(path);
observer.onNext(FileExistsResponse.newBuilder().setExists(exists).build());
observer.onCompleted();
} catch (Exception e) {
@@ -133,10 +191,11 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void dirExists(DirExistsRequest request,
StreamObserver<DirExistsResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
// Check existence via getFileMetadata — if it's a directory, it
exists
boolean exists = false;
try {
- FileMetadata metadata =
adaptor.getFileMetadata(request.getPath());
+ FileMetadata metadata = adaptor.getFileMetadata(path);
exists = metadata.isDirectory();
} catch (Exception ignored) {
// Path does not exist or is not accessible
@@ -152,7 +211,7 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void listDir(ListDirRequest request,
StreamObserver<ListDirResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- String path = request.getPath();
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
List<String> entries = adaptor.listDirectory(path);
ListDirResponse.Builder responseBuilder =
ListDirResponse.newBuilder();
@@ -179,7 +238,8 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void deleteFile(DeleteFileRequest request, StreamObserver<Empty>
observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- adaptor.executeCommand("rm -f " + request.getPath(), "/");
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
+ adaptor.executeCommand("rm -f " + path, "/");
observer.onNext(Empty.getDefaultInstance());
observer.onCompleted();
} catch (Exception e) {
@@ -191,7 +251,8 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void deleteDir(DeleteDirRequest request, StreamObserver<Empty>
observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- adaptor.deleteDirectory(request.getPath());
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
+ adaptor.deleteDirectory(path);
observer.onNext(Empty.getDefaultInstance());
observer.onCompleted();
} catch (Exception e) {
@@ -203,7 +264,9 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void moveFile(MoveFileRequest request,
StreamObserver<DataProductModel> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- adaptor.executeCommand("mv " + request.getSourcePath() + " " +
request.getDestinationPath(), "/");
+ String src = resolvePath(request.getSourcePath(),
request.getStorageResourceId());
+ String dst = resolvePath(request.getDestinationPath(),
request.getStorageResourceId());
+ adaptor.executeCommand("mv " + src + " " + dst, "/");
DataProductModel product = DataProductModel.newBuilder()
.setProductName(Paths.get(request.getDestinationPath())
@@ -221,7 +284,8 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void createDir(CreateDirRequest request,
StreamObserver<CreateDirResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- adaptor.createDirectory(request.getPath(), true);
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
+ adaptor.createDirectory(path, true);
observer.onNext(CreateDirResponse.newBuilder()
.setCreatedPath(request.getPath())
.build());
@@ -235,7 +299,9 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void createSymlink(CreateSymlinkRequest request,
StreamObserver<Empty> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- adaptor.executeCommand("ln -s " + request.getTargetPath() + " " +
request.getSourcePath(), "/");
+ String target = resolvePath(request.getTargetPath(),
request.getStorageResourceId());
+ String source = resolvePath(request.getSourcePath(),
request.getStorageResourceId());
+ adaptor.executeCommand("ln -s " + target + " " + source, "/");
observer.onNext(Empty.getDefaultInstance());
observer.onCompleted();
} catch (Exception e) {
@@ -247,8 +313,9 @@ public class UserStorageGrpcService extends
UserStorageServiceGrpc.UserStorageSe
public void getFileMetadata(GetFileMetadataRequest request,
StreamObserver<FileMetadataResponse> observer) {
try {
StorageResourceAdaptor adaptor =
getStorageAdaptor(request.getStorageResourceId());
- FileMetadata meta = adaptor.getFileMetadata(request.getPath());
- observer.onNext(toFileMetadataResponse(meta, request.getPath()));
+ String path = resolvePath(request.getPath(),
request.getStorageResourceId());
+ FileMetadata meta = adaptor.getFileMetadata(path);
+ observer.onNext(toFileMetadataResponse(meta, path));
observer.onCompleted();
} catch (Exception e) {
observer.onError(GrpcStatusMapper.toStatusException(e));
diff --git
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/mapper/StorageMapper.java
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/mapper/StorageMapper.java
index be60067e9d..89f0ded81f 100644
---
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/mapper/StorageMapper.java
+++
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/mapper/StorageMapper.java
@@ -44,7 +44,23 @@ public interface StorageMapper extends
CommonMapperConversions {
StorageMapper INSTANCE = Mappers.getMapper(StorageMapper.class);
// --- StorageResourceDescription ---
- StorageResourceDescription storageResourceToModel(StorageResourceEntity
entity);
+ // Manual mapping because MapStruct can't populate protobuf repeated
fields (addAll*)
+ default StorageResourceDescription
storageResourceToModel(StorageResourceEntity entity) {
+ if (entity == null) return null;
+ StorageResourceDescription.Builder b =
StorageResourceDescription.newBuilder();
+ if (entity.getStorageResourceId() != null)
b.setStorageResourceId(entity.getStorageResourceId());
+ if (entity.getHostName() != null) b.setHostName(entity.getHostName());
+ if (entity.getStorageResourceDescription() != null)
b.setStorageResourceDescription(entity.getStorageResourceDescription());
+ b.setEnabled(entity.isEnabled());
+ if (entity.getCreationTime() != null)
b.setCreationTime(entity.getCreationTime().getTime());
+ if (entity.getUpdateTime() != null)
b.setUpdateTime(entity.getUpdateTime().getTime());
+ if (entity.getDataMovementInterfaces() != null) {
+ for (DataMovementInterfaceEntity dm :
entity.getDataMovementInterfaces()) {
+ b.addDataMovementInterfaces(dataMovementInterfaceToModel(dm));
+ }
+ }
+ return b.build();
+ }
StorageResourceEntity storageResourceToEntity(StorageResourceDescription
model);
diff --git
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/model/DataMovementInterfaceEntity.java
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/model/DataMovementInterfaceEntity.java
index 6913d9b78c..efadb81791 100644
---
a/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/model/DataMovementInterfaceEntity.java
+++
b/airavata-api/storage-service/src/main/java/org/apache/airavata/storage/model/DataMovementInterfaceEntity.java
@@ -28,12 +28,12 @@ import
org.apache.airavata.model.data.movement.proto.DataMovementProtocol;
* The persistent class for the data_movement_interface database table.
*/
@Entity
-@Table(name = "DATA_MOVEMENT_INTERFACE")
+@Table(name = "STORAGE_INTERFACE")
@IdClass(DataMovementInterfacePK.class)
public class DataMovementInterfaceEntity implements Serializable {
private static final long serialVersionUID = 1L;
- @Column(name = "RESOURCE_ID")
+ @Column(name = "STORAGE_RESOURCE_ID")
@Id
private String resourceId;
diff --git a/airavata-python-sdk/airavata_sdk/client.py
b/airavata-python-sdk/airavata_sdk/client.py
index 632cbe521c..3d2dcdf708 100644
--- a/airavata-python-sdk/airavata_sdk/client.py
+++ b/airavata-python-sdk/airavata_sdk/client.py
@@ -16,10 +16,13 @@ class AiravataClient:
def __init__(self, host, port, token, gateway_id, secure=False,
claims=None):
target = f"{host}:{port}"
+ options = [
+ ("grpc.max_metadata_size", 64 * 1024), # 64KB for large error
messages
+ ]
if secure:
- self._channel = grpc.secure_channel(target,
grpc.ssl_channel_credentials())
+ self._channel = grpc.secure_channel(target,
grpc.ssl_channel_credentials(), options=options)
else:
- self._channel = grpc.insecure_channel(target)
+ self._channel = grpc.insecure_channel(target, options=options)
self._metadata = []
if token:
diff --git a/airavata-python-sdk/airavata_sdk/facade/compute.py
b/airavata-python-sdk/airavata_sdk/facade/compute.py
index 5ecc2dbe8f..a4c7a93aa7 100644
--- a/airavata-python-sdk/airavata_sdk/facade/compute.py
+++ b/airavata-python-sdk/airavata_sdk/facade/compute.py
@@ -29,10 +29,11 @@ class ComputeClient:
def register_compute_resource(self, compute_resource):
pb2 = self._svc("resource_service_pb2")
- return self._resource.RegisterComputeResource(
+ response = self._resource.RegisterComputeResource(
pb2.RegisterComputeResourceRequest(compute_resource=compute_resource),
metadata=self._metadata,
)
+ return response.compute_resource_id
def get_compute_resource(self, compute_resource_id):
pb2 = self._svc("resource_service_pb2")
@@ -43,10 +44,11 @@ class ComputeClient:
def get_all_compute_resource_names(self):
pb2 = self._svc("resource_service_pb2")
- return self._resource.GetAllComputeResourceNames(
+ response = self._resource.GetAllComputeResourceNames(
pb2.GetAllComputeResourceNamesRequest(),
metadata=self._metadata,
)
+ return dict(response.compute_resource_names)
def update_compute_resource(self, compute_resource_id, compute_resource):
pb2 = self._svc("resource_service_pb2")
@@ -66,10 +68,11 @@ class ComputeClient:
def add_local_submission_details(self, compute_resource_id, priority,
local_submission):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddLocalSubmission(
+ response = self._resource.AddLocalSubmission(
pb2.AddLocalSubmissionRequest(compute_resource_id=compute_resource_id,
priority=priority, local_submission=local_submission),
metadata=self._metadata,
)
+ return response.submission_id
def update_local_submission_details(self, submission_id, local_submission):
pb2 = self._svc("resource_service_pb2")
@@ -87,17 +90,19 @@ class ComputeClient:
def add_ssh_job_submission_details(self, compute_resource_id, priority,
ssh_job_submission):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddSSHJobSubmission(
+ response = self._resource.AddSSHJobSubmission(
pb2.AddSSHJobSubmissionRequest(compute_resource_id=compute_resource_id,
priority=priority, ssh_job_submission=ssh_job_submission),
metadata=self._metadata,
)
+ return response.submission_id
def add_ssh_fork_job_submission_details(self, compute_resource_id,
priority, ssh_job_submission):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddSSHForkJobSubmission(
+ response = self._resource.AddSSHForkJobSubmission(
pb2.AddSSHForkJobSubmissionRequest(compute_resource_id=compute_resource_id,
priority=priority, ssh_job_submission=ssh_job_submission),
metadata=self._metadata,
)
+ return response.submission_id
def get_ssh_job_submission(self, submission_id):
pb2 = self._svc("resource_service_pb2")
@@ -108,10 +113,11 @@ class ComputeClient:
def add_unicore_job_submission_details(self, compute_resource_id,
priority, unicore_job_submission):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddUnicoreJobSubmission(
+ response = self._resource.AddUnicoreJobSubmission(
pb2.AddUnicoreJobSubmissionRequest(compute_resource_id=compute_resource_id,
priority=priority, unicore_job_submission=unicore_job_submission),
metadata=self._metadata,
)
+ return response.submission_id
def get_unicore_job_submission(self, submission_id):
pb2 = self._svc("resource_service_pb2")
@@ -122,10 +128,11 @@ class ComputeClient:
def add_cloud_job_submission_details(self, compute_resource_id, priority,
cloud_job_submission):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddCloudJobSubmission(
+ response = self._resource.AddCloudJobSubmission(
pb2.AddCloudJobSubmissionRequest(compute_resource_id=compute_resource_id,
priority=priority, cloud_job_submission=cloud_job_submission),
metadata=self._metadata,
)
+ return response.submission_id
def get_cloud_job_submission(self, submission_id):
pb2 = self._svc("resource_service_pb2")
@@ -175,10 +182,11 @@ class ComputeClient:
def register_gateway_resource_profile(self, gateway_resource_profile):
pb2 = self._svc("gateway_resource_profile_service_pb2")
- return self._gw_profile.RegisterGatewayResourceProfile(
+ response = self._gw_profile.RegisterGatewayResourceProfile(
pb2.RegisterGatewayResourceProfileRequest(gateway_resource_profile=gateway_resource_profile),
metadata=self._metadata,
)
+ return response.gateway_id
def get_gateway_resource_profile(self, gateway_id):
pb2 = self._svc("gateway_resource_profile_service_pb2")
@@ -231,24 +239,27 @@ class ComputeClient:
def get_all_gateway_compute_resource_preferences(self, gateway_id):
pb2 = self._svc("gateway_resource_profile_service_pb2")
- return self._gw_profile.GetAllComputePreferences(
+ response = self._gw_profile.GetAllComputePreferences(
pb2.GetAllComputePreferencesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.compute_resource_preferences)
def get_all_gateway_storage_preferences(self, gateway_id):
pb2 = self._svc("gateway_resource_profile_service_pb2")
- return self._gw_profile.GetAllStoragePreferences(
+ response = self._gw_profile.GetAllStoragePreferences(
pb2.GetAllStoragePreferencesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.storage_preferences)
def get_all_gateway_resource_profiles(self):
pb2 = self._svc("gateway_resource_profile_service_pb2")
- return self._gw_profile.GetAllGatewayResourceProfiles(
+ response = self._gw_profile.GetAllGatewayResourceProfiles(
pb2.GetAllGatewayResourceProfilesRequest(),
metadata=self._metadata,
)
+ return list(response.gateway_resource_profiles)
def update_gateway_compute_resource_preference(self, gateway_id,
compute_resource_id, compute_resource_preference):
pb2 = self._svc("gateway_resource_profile_service_pb2")
@@ -280,10 +291,11 @@ class ComputeClient:
def get_ssh_account_provisioners(self):
pb2 = self._svc("gateway_resource_profile_service_pb2")
- return self._gw_profile.GetSSHAccountProvisioners(
+ response = self._gw_profile.GetSSHAccountProvisioners(
pb2.GetSSHAccountProvisionersRequest(),
metadata=self._metadata,
)
+ return list(response.ssh_account_provisioners)
# ================================================================
# Group Resource Profile Service
@@ -319,10 +331,11 @@ class ComputeClient:
def get_group_resource_list(self):
pb2 = self._svc("group_resource_profile_service_pb2")
- return self._grp_profile.GetGroupResourceList(
+ response = self._grp_profile.GetGroupResourceList(
pb2.GetGroupResourceListRequest(),
metadata=self._metadata,
)
+ return list(response.group_resource_profiles)
def remove_group_compute_prefs(self, group_resource_profile_id,
compute_resource_id):
pb2 = self._svc("group_resource_profile_service_pb2")
@@ -368,24 +381,27 @@ class ComputeClient:
def get_group_compute_resource_pref_list(self, group_resource_profile_id):
pb2 = self._svc("group_resource_profile_service_pb2")
- return self._grp_profile.GetGroupComputePrefList(
+ response = self._grp_profile.GetGroupComputePrefList(
pb2.GetGroupComputePrefListRequest(group_resource_profile_id=group_resource_profile_id),
metadata=self._metadata,
)
+ return list(response.group_compute_resource_preferences)
def get_group_batch_queue_resource_policy_list(self,
group_resource_profile_id):
pb2 = self._svc("group_resource_profile_service_pb2")
- return self._grp_profile.GetGroupBatchQueuePolicyList(
+ response = self._grp_profile.GetGroupBatchQueuePolicyList(
pb2.GetGroupBatchQueuePolicyListRequest(group_resource_profile_id=group_resource_profile_id),
metadata=self._metadata,
)
+ return list(response.batch_queue_resource_policies)
def get_group_compute_resource_policy_list(self,
group_resource_profile_id):
pb2 = self._svc("group_resource_profile_service_pb2")
- return self._grp_profile.GetGroupComputeResourcePolicyList(
+ response = self._grp_profile.GetGroupComputeResourcePolicyList(
pb2.GetGroupComputeResourcePolicyListRequest(group_resource_profile_id=group_resource_profile_id),
metadata=self._metadata,
)
+ return list(response.compute_resource_policies)
def get_gateway_groups(self):
pb2 = self._svc("group_resource_profile_service_pb2")
@@ -400,17 +416,19 @@ class ComputeClient:
def register_user_resource_profile(self, user_resource_profile):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.RegisterUserResourceProfile(
+ response = self._user_profile.RegisterUserResourceProfile(
pb2.RegisterUserResourceProfileRequest(user_resource_profile=user_resource_profile),
metadata=self._metadata,
)
+ return response.user_id
def is_user_resource_profile_exists(self, user_id, gateway_id):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.IsUserResourceProfileExists(
+ response = self._user_profile.IsUserResourceProfileExists(
pb2.IsUserResourceProfileExistsRequest(user_id=user_id,
gateway_id=gateway_id),
metadata=self._metadata,
)
+ return response.exists
def get_user_resource_profile(self, user_id, gateway_id):
pb2 = self._svc("user_resource_profile_service_pb2")
@@ -463,24 +481,27 @@ class ComputeClient:
def get_all_user_compute_resource_preferences(self, user_id, gateway_id):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.GetAllUserComputePreferences(
+ response = self._user_profile.GetAllUserComputePreferences(
pb2.GetAllUserComputePreferencesRequest(user_id=user_id,
gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.user_compute_resource_preferences)
def get_all_user_storage_preferences(self, user_id, gateway_id):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.GetAllUserStoragePreferences(
+ response = self._user_profile.GetAllUserStoragePreferences(
pb2.GetAllUserStoragePreferencesRequest(user_id=user_id,
gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.user_storage_preferences)
def get_all_user_resource_profiles(self):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.GetAllUserResourceProfiles(
+ response = self._user_profile.GetAllUserResourceProfiles(
pb2.GetAllUserResourceProfilesRequest(),
metadata=self._metadata,
)
+ return list(response.user_resource_profiles)
def update_user_compute_resource_preference(self, user_id, gateway_id,
compute_resource_id, user_compute_resource_preference):
pb2 = self._svc("user_resource_profile_service_pb2")
@@ -512,7 +533,8 @@ class ComputeClient:
def get_latest_queue_statuses(self):
pb2 = self._svc("user_resource_profile_service_pb2")
- return self._user_profile.GetLatestQueueStatuses(
+ response = self._user_profile.GetLatestQueueStatuses(
pb2.GetLatestQueueStatusesRequest(),
metadata=self._metadata,
)
+ return list(response.queue_statuses)
diff --git a/airavata-python-sdk/airavata_sdk/facade/credential.py
b/airavata-python-sdk/airavata_sdk/facade/credential.py
index b2a9628f4f..bdaf7a4f19 100644
--- a/airavata-python-sdk/airavata_sdk/facade/credential.py
+++ b/airavata-python-sdk/airavata_sdk/facade/credential.py
@@ -24,17 +24,19 @@ class CredentialClient:
def generate_and_register_ssh_keys(self, gateway_id, username,
description=""):
pb2 = self._svc("credential_service_pb2")
- return self._stub.GenerateAndRegisterSSHKeys(
+ response = self._stub.GenerateAndRegisterSSHKeys(
pb2.GenerateAndRegisterSSHKeysRequest(gateway_id=gateway_id,
username=username, description=description),
metadata=self._metadata,
)
+ return response.token
def register_pwd_credential(self, gateway_id, password_credential):
pb2 = self._svc("credential_service_pb2")
- return self._stub.RegisterPwdCredential(
+ response = self._stub.RegisterPwdCredential(
pb2.RegisterPwdCredentialRequest(gateway_id=gateway_id,
password_credential=password_credential),
metadata=self._metadata,
)
+ return response.token
def get_credential_summary(self, token_id, gateway_id):
pb2 = self._svc("credential_service_pb2")
@@ -45,10 +47,11 @@ class CredentialClient:
def get_all_credential_summaries(self, gateway_id, type):
pb2 = self._svc("credential_service_pb2")
- return self._stub.GetAllCredentialSummaries(
+ response = self._stub.GetAllCredentialSummaries(
pb2.GetAllCredentialSummariesRequest(gateway_id=gateway_id,
type=type),
metadata=self._metadata,
)
+ return list(response.credential_summaries)
def delete_ssh_pub_key(self, token_id, gateway_id):
pb2 = self._svc("credential_service_pb2")
@@ -66,21 +69,24 @@ class CredentialClient:
def is_ssh_setup_complete(self, compute_resource_id, gateway_id, username):
pb2 = self._svc("credential_service_pb2")
- return self._stub.IsSSHSetupComplete(
+ response = self._stub.IsSSHSetupComplete(
pb2.IsSSHSetupCompleteRequest(compute_resource_id=compute_resource_id,
gateway_id=gateway_id, username=username),
metadata=self._metadata,
)
+ return response.is_complete
def setup_ssh_account(self, compute_resource_id, gateway_id, username):
pb2 = self._svc("credential_service_pb2")
- return self._stub.SetupSSHAccount(
+ response = self._stub.SetupSSHAccount(
pb2.SetupSSHAccountRequest(compute_resource_id=compute_resource_id,
gateway_id=gateway_id, username=username),
metadata=self._metadata,
)
+ return response.success
def does_user_have_ssh_account(self, compute_resource_id, gateway_id,
username):
pb2 = self._svc("credential_service_pb2")
- return self._stub.DoesUserHaveSSHAccount(
+ response = self._stub.DoesUserHaveSSHAccount(
pb2.DoesUserHaveSSHAccountRequest(compute_resource_id=compute_resource_id,
gateway_id=gateway_id, username=username),
metadata=self._metadata,
)
+ return response.has_account
diff --git a/airavata-python-sdk/airavata_sdk/facade/iam.py
b/airavata-python-sdk/airavata_sdk/facade/iam.py
index 97f4c14bf4..71d2341986 100644
--- a/airavata-python-sdk/airavata_sdk/facade/iam.py
+++ b/airavata-python-sdk/airavata_sdk/facade/iam.py
@@ -27,24 +27,27 @@ class IamClient:
def is_user_exists(self, gateway_id, user_name):
pb2 = self._svc("gateway_service_pb2")
- return self._gateway.IsUserExists(
+ response = self._gateway.IsUserExists(
pb2.IsUserExistsRequest(gateway_id=gateway_id,
user_name=user_name),
metadata=self._metadata,
)
+ return response.exists
def add_gateway(self, gateway):
pb2 = self._svc("gateway_service_pb2")
- return self._gateway.AddGateway(
+ response = self._gateway.AddGateway(
pb2.AddGatewayRequest(gateway=gateway),
metadata=self._metadata,
)
+ return response.gateway_id
def get_all_users_in_gateway(self, gateway_id):
pb2 = self._svc("gateway_service_pb2")
- return self._gateway.GetAllUsersInGateway(
+ response = self._gateway.GetAllUsersInGateway(
pb2.GetAllUsersInGatewayRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.user_names)
def update_gateway(self, gateway_id, gateway):
pb2 = self._svc("gateway_service_pb2")
@@ -69,17 +72,19 @@ class IamClient:
def get_all_gateways(self):
pb2 = self._svc("gateway_service_pb2")
- return self._gateway.GetAllGateways(
+ response = self._gateway.GetAllGateways(
pb2.GetAllGatewaysRequest(),
metadata=self._metadata,
)
+ return list(response.gateways)
def is_gateway_exist(self, gateway_id):
pb2 = self._svc("gateway_service_pb2")
- return self._gateway.IsGatewayExist(
+ response = self._gateway.IsGatewayExist(
pb2.IsGatewayExistRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return response.exists
# ================================================================
# IAM Admin Service
diff --git a/airavata-python-sdk/airavata_sdk/facade/research.py
b/airavata-python-sdk/airavata_sdk/facade/research.py
index 706bcae4f3..fd610e7a37 100644
--- a/airavata-python-sdk/airavata_sdk/facade/research.py
+++ b/airavata-python-sdk/airavata_sdk/facade/research.py
@@ -46,10 +46,11 @@ class ResearchClient:
def search_experiments(self, gateway_id, user_name, filters=None,
limit=-1, offset=0):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.SearchExperiments(
+ response = self._experiment.SearchExperiments(
pb2.SearchExperimentsRequest(gateway_id=gateway_id,
user_name=user_name, filters=filters or {}, limit=limit, offset=offset),
metadata=self._metadata,
)
+ return list(response.experiments)
def get_experiment_statistics(self, gateway_id, from_time, to_time,
user_name="", application_name="", resource_host_name="", limit=0, offset=0):
pb2 = self._svc("experiment_service_pb2")
@@ -64,24 +65,27 @@ class ResearchClient:
def get_experiments_in_project(self, project_id, limit=-1, offset=0):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.GetExperimentsInProject(
+ response = self._experiment.GetExperimentsInProject(
pb2.GetExperimentsInProjectRequest(project_id=project_id,
limit=limit, offset=offset),
metadata=self._metadata,
)
+ return list(response.experiments)
def get_user_experiments(self, gateway_id, user_name, limit=-1, offset=0):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.GetUserExperiments(
+ response = self._experiment.GetUserExperiments(
pb2.GetUserExperimentsRequest(gateway_id=gateway_id,
user_name=user_name, limit=limit, offset=offset),
metadata=self._metadata,
)
+ return list(response.experiments)
def create_experiment(self, gateway_id, experiment):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.CreateExperiment(
+ response = self._experiment.CreateExperiment(
pb2.CreateExperimentRequest(gateway_id=gateway_id,
experiment=experiment),
metadata=self._metadata,
)
+ return response.experiment_id
def delete_experiment(self, experiment_id):
pb2 = self._svc("experiment_service_pb2")
@@ -155,10 +159,11 @@ class ResearchClient:
def get_experiment_outputs(self, experiment_id):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.GetExperimentOutputs(
+ response = self._experiment.GetExperimentOutputs(
pb2.GetExperimentOutputsRequest(experiment_id=experiment_id),
metadata=self._metadata,
)
+ return list(response.outputs)
def get_intermediate_outputs(self, experiment_id, output_names=None):
pb2 = self._svc("experiment_service_pb2")
@@ -183,14 +188,15 @@ class ResearchClient:
def get_job_details(self, experiment_id):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.GetJobDetails(
+ response = self._experiment.GetJobDetails(
pb2.GetJobDetailsRequest(experiment_id=experiment_id),
metadata=self._metadata,
)
+ return list(response.jobs)
def clone_experiment(self, experiment_id, new_experiment_name="",
new_experiment_project_id=""):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.CloneExperiment(
+ response = self._experiment.CloneExperiment(
pb2.CloneExperimentRequest(
experiment_id=experiment_id,
new_experiment_name=new_experiment_name,
@@ -198,10 +204,11 @@ class ResearchClient:
),
metadata=self._metadata,
)
+ return response.experiment_id
def clone_experiment_by_admin(self, experiment_id, new_experiment_name="",
new_experiment_project_id=""):
pb2 = self._svc("experiment_service_pb2")
- return self._experiment.CloneExperiment(
+ response = self._experiment.CloneExperiment(
pb2.CloneExperimentRequest(
experiment_id=experiment_id,
new_experiment_name=new_experiment_name,
@@ -209,6 +216,7 @@ class ResearchClient:
),
metadata=self._metadata,
)
+ return response.experiment_id
def terminate_experiment(self, experiment_id, gateway_id):
pb2 = self._svc("experiment_service_pb2")
@@ -223,10 +231,11 @@ class ResearchClient:
def create_project(self, gateway_id, project):
pb2 = self._svc("project_service_pb2")
- return self._project.CreateProject(
+ response = self._project.CreateProject(
pb2.CreateProjectRequest(gateway_id=gateway_id, project=project),
metadata=self._metadata,
)
+ return response.project_id
def update_project(self, project_id, project):
pb2 = self._svc("project_service_pb2")
@@ -251,17 +260,19 @@ class ResearchClient:
def get_user_projects(self, gateway_id, user_name, limit=-1, offset=0):
pb2 = self._svc("project_service_pb2")
- return self._project.GetUserProjects(
+ response = self._project.GetUserProjects(
pb2.GetUserProjectsRequest(gateway_id=gateway_id,
user_name=user_name, limit=limit, offset=offset),
metadata=self._metadata,
)
+ return list(response.projects)
def search_projects(self, gateway_id, user_name, filters=None, limit=-1,
offset=0):
pb2 = self._svc("project_service_pb2")
- return self._project.SearchProjects(
+ response = self._project.SearchProjects(
pb2.SearchProjectsRequest(gateway_id=gateway_id,
user_name=user_name, filters=filters or {}, limit=limit, offset=offset),
metadata=self._metadata,
)
+ return list(response.projects)
# ================================================================
# Application Catalog Service
@@ -269,10 +280,11 @@ class ResearchClient:
def register_application_module(self, gateway_id, application_module):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.RegisterApplicationModule(
+ response = self._app_catalog.RegisterApplicationModule(
pb2.RegisterApplicationModuleRequest(gateway_id=gateway_id,
application_module=application_module),
metadata=self._metadata,
)
+ return response.app_module_id
def get_application_module(self, app_module_id):
pb2 = self._svc("application_catalog_service_pb2")
@@ -290,17 +302,19 @@ class ResearchClient:
def get_all_app_modules(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAllAppModules(
+ response = self._app_catalog.GetAllAppModules(
pb2.GetAllAppModulesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.application_modules)
def get_accessible_app_modules(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAccessibleAppModules(
+ response = self._app_catalog.GetAccessibleAppModules(
pb2.GetAccessibleAppModulesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.application_modules)
def delete_application_module(self, app_module_id):
pb2 = self._svc("application_catalog_service_pb2")
@@ -311,10 +325,11 @@ class ResearchClient:
def register_application_deployment(self, gateway_id,
application_deployment):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.RegisterApplicationDeployment(
+ response = self._app_catalog.RegisterApplicationDeployment(
pb2.RegisterApplicationDeploymentRequest(gateway_id=gateway_id,
application_deployment=application_deployment),
metadata=self._metadata,
)
+ return response.app_deployment_id
def get_application_deployment(self, app_deployment_id):
pb2 = self._svc("application_catalog_service_pb2")
@@ -339,42 +354,47 @@ class ResearchClient:
def get_all_application_deployments(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAllApplicationDeployments(
+ response = self._app_catalog.GetAllApplicationDeployments(
pb2.GetAllApplicationDeploymentsRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.application_deployments)
def get_accessible_application_deployments(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAccessibleApplicationDeployments(
+ response = self._app_catalog.GetAccessibleApplicationDeployments(
pb2.GetAccessibleApplicationDeploymentsRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.application_deployments)
def get_app_module_deployed_resources(self, app_module_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAppModuleDeployedResources(
+ response = self._app_catalog.GetAppModuleDeployedResources(
pb2.GetAppModuleDeployedResourcesRequest(app_module_id=app_module_id),
metadata=self._metadata,
)
+ return list(response.compute_resource_ids)
def
get_application_deployments_for_app_module_and_group_resource_profile(self,
app_module_id, group_resource_profile_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetDeploymentsForModuleAndProfile(
+ response = self._app_catalog.GetDeploymentsForModuleAndProfile(
pb2.GetDeploymentsForModuleAndProfileRequest(app_module_id=app_module_id,
group_resource_profile_id=group_resource_profile_id),
metadata=self._metadata,
)
+ return list(response.application_deployments)
def register_application_interface(self, gateway_id,
application_interface):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.RegisterApplicationInterface(
+ response = self._app_catalog.RegisterApplicationInterface(
pb2.RegisterApplicationInterfaceRequest(gateway_id=gateway_id,
application_interface=application_interface),
metadata=self._metadata,
)
+ return response.app_interface_id
def clone_application_interface(self, existing_app_interface_id,
new_app_module_name, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.CloneApplicationInterface(
+ response = self._app_catalog.CloneApplicationInterface(
pb2.CloneApplicationInterfaceRequest(
existing_app_interface_id=existing_app_interface_id,
new_app_module_name=new_app_module_name,
@@ -382,6 +402,7 @@ class ResearchClient:
),
metadata=self._metadata,
)
+ return response.app_interface_id
def get_application_interface(self, app_interface_id):
pb2 = self._svc("application_catalog_service_pb2")
@@ -406,38 +427,43 @@ class ResearchClient:
def get_all_application_interface_names(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAllApplicationInterfaceNames(
+ response = self._app_catalog.GetAllApplicationInterfaceNames(
pb2.GetAllApplicationInterfaceNamesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return dict(response.application_interface_names)
def get_all_application_interfaces(self, gateway_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAllApplicationInterfaces(
+ response = self._app_catalog.GetAllApplicationInterfaces(
pb2.GetAllApplicationInterfacesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.application_interfaces)
def get_application_inputs(self, app_interface_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetApplicationInputs(
+ response = self._app_catalog.GetApplicationInputs(
pb2.GetApplicationInputsRequest(app_interface_id=app_interface_id),
metadata=self._metadata,
)
+ return list(response.application_inputs)
def get_application_outputs(self, app_interface_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetApplicationOutputs(
+ response = self._app_catalog.GetApplicationOutputs(
pb2.GetApplicationOutputsRequest(app_interface_id=app_interface_id),
metadata=self._metadata,
)
+ return list(response.application_outputs)
def get_available_app_interface_compute_resources(self, app_interface_id):
pb2 = self._svc("application_catalog_service_pb2")
- return self._app_catalog.GetAvailableComputeResources(
+ response = self._app_catalog.GetAvailableComputeResources(
pb2.GetAvailableComputeResourcesRequest(app_interface_id=app_interface_id),
metadata=self._metadata,
)
+ return dict(response.compute_resource_names)
# ================================================================
# Parser Service
@@ -452,17 +478,19 @@ class ResearchClient:
def save_parser(self, parser):
pb2 = self._svc("parser_service_pb2")
- return self._parser.SaveParser(
+ response = self._parser.SaveParser(
pb2.SaveParserRequest(parser=parser),
metadata=self._metadata,
)
+ return response.parser_id
def list_all_parsers(self, gateway_id):
pb2 = self._svc("parser_service_pb2")
- return self._parser.ListAllParsers(
+ response = self._parser.ListAllParsers(
pb2.ListAllParsersRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.parsers)
def remove_parser(self, parser_id, gateway_id):
pb2 = self._svc("parser_service_pb2")
@@ -480,17 +508,19 @@ class ResearchClient:
def get_parsing_templates_for_experiment(self, experiment_id, gateway_id):
pb2 = self._svc("parser_service_pb2")
- return self._parser.GetParsingTemplatesForExperiment(
+ response = self._parser.GetParsingTemplatesForExperiment(
pb2.GetParsingTemplatesForExperimentRequest(experiment_id=experiment_id,
gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.parsing_templates)
def save_parsing_template(self, parsing_template):
pb2 = self._svc("parser_service_pb2")
- return self._parser.SaveParsingTemplate(
+ response = self._parser.SaveParsingTemplate(
pb2.SaveParsingTemplateRequest(parsing_template=parsing_template),
metadata=self._metadata,
)
+ return response.template_id
def remove_parsing_template(self, template_id, gateway_id):
pb2 = self._svc("parser_service_pb2")
@@ -501,10 +531,11 @@ class ResearchClient:
def list_all_parsing_templates(self, gateway_id):
pb2 = self._svc("parser_service_pb2")
- return self._parser.ListAllParsingTemplates(
+ response = self._parser.ListAllParsingTemplates(
pb2.ListAllParsingTemplatesRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.parsing_templates)
# ================================================================
# Data Product Service
@@ -512,10 +543,11 @@ class ResearchClient:
def register_data_product(self, data_product):
pb2 = self._svc("data_product_service_pb2")
- return self._data_product.RegisterDataProduct(
+ response = self._data_product.RegisterDataProduct(
pb2.RegisterDataProductRequest(data_product=data_product),
metadata=self._metadata,
)
+ return response.product_uri
def get_data_product(self, data_product_uri):
pb2 = self._svc("data_product_service_pb2")
@@ -526,10 +558,11 @@ class ResearchClient:
def register_replica_location(self, replica_location):
pb2 = self._svc("data_product_service_pb2")
- return self._data_product.RegisterReplicaLocation(
+ response = self._data_product.RegisterReplicaLocation(
pb2.RegisterReplicaLocationRequest(replica_location=replica_location),
metadata=self._metadata,
)
+ return response.replica_id
def get_parent_data_product(self, data_product_uri):
pb2 = self._svc("data_product_service_pb2")
@@ -540,10 +573,11 @@ class ResearchClient:
def get_child_data_products(self, data_product_uri):
pb2 = self._svc("data_product_service_pb2")
- return self._data_product.GetChildDataProducts(
+ response = self._data_product.GetChildDataProducts(
pb2.GetChildDataProductsRequest(data_product_uri=data_product_uri),
metadata=self._metadata,
)
+ return list(response.data_products)
def update_data_product(self, product_uri, data_product):
pb2 = self._svc("data_product_service_pb2")
@@ -586,10 +620,11 @@ class ResearchClient:
def create_notification(self, notification):
pb2 = self._svc("notification_service_pb2")
- return self._notification.CreateNotification(
+ response = self._notification.CreateNotification(
pb2.CreateNotificationRequest(notification=notification),
metadata=self._metadata,
)
+ return response.notification_id
def update_notification(self, notification):
pb2 = self._svc("notification_service_pb2")
@@ -614,10 +649,11 @@ class ResearchClient:
def get_all_notifications(self, gateway_id):
pb2 = self._svc("notification_service_pb2")
- return self._notification.GetAllNotifications(
+ response = self._notification.GetAllNotifications(
pb2.GetAllNotificationsRequest(gateway_id=gateway_id),
metadata=self._metadata,
)
+ return list(response.notifications)
# ================================================================
# Experiment Management Service
diff --git a/airavata-python-sdk/airavata_sdk/facade/sharing.py
b/airavata-python-sdk/airavata_sdk/facade/sharing.py
index 1ee527b338..ea5bb31a56 100644
--- a/airavata-python-sdk/airavata_sdk/facade/sharing.py
+++ b/airavata-python-sdk/airavata_sdk/facade/sharing.py
@@ -68,38 +68,43 @@ class SharingClient:
def get_all_accessible_users(self, resource_id, permission_type):
pb2 = self._pb2()
- return self._sharing.GetAllAccessibleUsers(
+ response = self._sharing.GetAllAccessibleUsers(
pb2.GetAllAccessibleUsersRequest(resource_id=resource_id,
permission_type=permission_type),
metadata=self._metadata,
)
+ return list(response.user_ids)
def get_all_directly_accessible_users(self, resource_id, permission_type):
pb2 = self._pb2()
- return self._sharing.GetAllDirectlyAccessibleUsers(
+ response = self._sharing.GetAllDirectlyAccessibleUsers(
pb2.GetAllDirectlyAccessibleUsersRequest(resource_id=resource_id,
permission_type=permission_type),
metadata=self._metadata,
)
+ return list(response.user_ids)
def get_all_accessible_groups(self, resource_id, permission_type):
pb2 = self._pb2()
- return self._sharing.GetAllAccessibleGroups(
+ response = self._sharing.GetAllAccessibleGroups(
pb2.GetAllAccessibleGroupsRequest(resource_id=resource_id,
permission_type=permission_type),
metadata=self._metadata,
)
+ return list(response.group_ids)
def get_all_directly_accessible_groups(self, resource_id, permission_type):
pb2 = self._pb2()
- return self._sharing.GetAllDirectlyAccessibleGroups(
+ response = self._sharing.GetAllDirectlyAccessibleGroups(
pb2.GetAllDirectlyAccessibleGroupsRequest(resource_id=resource_id,
permission_type=permission_type),
metadata=self._metadata,
)
+ return list(response.group_ids)
def user_has_access(self, resource_id, user_id, permission_type):
pb2 = self._pb2()
- return self._sharing.UserHasAccess(
+ response = self._sharing.UserHasAccess(
pb2.UserHasAccessRequest(resource_id=resource_id, user_id=user_id,
permission_type=permission_type),
metadata=self._metadata,
)
+ return response.has_access
def revoke_from_users(self, resource_id, user_permissions):
pb2 = self._pb2()
diff --git a/airavata-python-sdk/airavata_sdk/facade/storage.py
b/airavata-python-sdk/airavata_sdk/facade/storage.py
index bec3fda26e..b6a1ca80e7 100644
--- a/airavata-python-sdk/airavata_sdk/facade/storage.py
+++ b/airavata-python-sdk/airavata_sdk/facade/storage.py
@@ -22,10 +22,11 @@ class StorageClient:
def register_storage_resource(self, storage_resource):
pb2 = self._svc("resource_service_pb2")
- return self._resource.RegisterStorageResource(
+ response = self._resource.RegisterStorageResource(
pb2.RegisterStorageResourceRequest(storage_resource=storage_resource),
metadata=self._metadata,
)
+ return response.storage_resource_id
def get_storage_resource(self, storage_resource_id):
pb2 = self._svc("resource_service_pb2")
@@ -50,10 +51,11 @@ class StorageClient:
def get_all_storage_resource_names(self):
pb2 = self._svc("resource_service_pb2")
- return self._resource.GetAllStorageResourceNames(
+ response = self._resource.GetAllStorageResourceNames(
pb2.GetAllStorageResourceNamesRequest(),
metadata=self._metadata,
)
+ return dict(response.storage_resource_names)
# ================================================================
# Data Movement
@@ -61,10 +63,11 @@ class StorageClient:
def add_local_data_movement(self, compute_resource_id, priority, dm_type,
local_data_movement):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddLocalDataMovement(
+ response = self._resource.AddLocalDataMovement(
pb2.AddLocalDataMovementRequest(compute_resource_id=compute_resource_id,
priority=priority, dm_type=dm_type, local_data_movement=local_data_movement),
metadata=self._metadata,
)
+ return response.data_movement_id
def update_local_data_movement(self, data_movement_id,
local_data_movement):
pb2 = self._svc("resource_service_pb2")
@@ -82,10 +85,11 @@ class StorageClient:
def add_scp_data_movement(self, compute_resource_id, priority, dm_type,
scp_data_movement):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddSCPDataMovement(
+ response = self._resource.AddSCPDataMovement(
pb2.AddSCPDataMovementRequest(compute_resource_id=compute_resource_id,
priority=priority, dm_type=dm_type, scp_data_movement=scp_data_movement),
metadata=self._metadata,
)
+ return response.data_movement_id
def update_scp_data_movement(self, data_movement_id, scp_data_movement):
pb2 = self._svc("resource_service_pb2")
@@ -103,10 +107,11 @@ class StorageClient:
def add_grid_ftp_data_movement(self, compute_resource_id, priority,
dm_type, gridftp_data_movement):
pb2 = self._svc("resource_service_pb2")
- return self._resource.AddGridFTPDataMovement(
+ response = self._resource.AddGridFTPDataMovement(
pb2.AddGridFTPDataMovementRequest(compute_resource_id=compute_resource_id,
priority=priority, dm_type=dm_type,
gridftp_data_movement=gridftp_data_movement),
metadata=self._metadata,
)
+ return response.data_movement_id
def update_grid_ftp_data_movement(self, data_movement_id,
gridftp_data_movement):
pb2 = self._svc("resource_service_pb2")
diff --git a/airavata-server/src/main/resources/application.properties
b/airavata-server/src/main/resources/application.properties
index a008a1839a..371bda938f 100644
--- a/airavata-server/src/main/resources/application.properties
+++ b/airavata-server/src/main/resources/application.properties
@@ -28,7 +28,7 @@ spring.datasource.hikari.leak-detection-threshold=20000
validationQuery=SELECT 1
# --- JPA ---
-spring.jpa.hibernate.ddl-auto=none
+spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
@@ -62,7 +62,7 @@ credential.store.keystore.alias=airavata
# --- Gateway ---
default.registry.gateway=default
default.registry.oauth.client.id=pga
-default.registry.oauth.client.secret=upCMVu2RZcAXUqpr9V7phAbz6hhF9cbl
+default.registry.oauth.client.secret=m36BXQIxX3j3VILadeHMK5IvbOeRlCCc
default.registry.user=default-admin
default.registry.password=ade4#21242ftfd
super.tenant.gatewayId=default
diff --git
a/airavata-server/src/main/resources/db/migration/airavata/V1__Baseline_schema.sql
b/airavata-server/src/main/resources/db/migration/airavata/V1__Baseline_schema.sql
index f6d9c20229..6efa3ee192 100644
---
a/airavata-server/src/main/resources/db/migration/airavata/V1__Baseline_schema.sql
+++
b/airavata-server/src/main/resources/db/migration/airavata/V1__Baseline_schema.sql
@@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS GATEWAY (
OAUTH_CLIENT_SECRET VARCHAR(255),
REQUEST_CREATION_TIME TIMESTAMP NULL,
REQUESTER_USERNAME VARCHAR(255),
+ AIRAVATA_INTERNAL_GATEWAY_ID VARCHAR(255),
PRIMARY KEY (GATEWAY_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/compose.yml b/compose.yml
index 199d309378..9123c0ab33 100644
--- a/compose.yml
+++ b/compose.yml
@@ -4,6 +4,7 @@ services:
image: mariadb:11.8
container_name: airavata-db
restart: unless-stopped
+ command: --lower-case-table-names=1
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: airavata
@@ -90,6 +91,7 @@ services:
KEYCLOAK_ADMIN_PASSWORD: admin
volumes:
-
./conf/keycloak/realm-default.json:/opt/keycloak/data/import/realm-default.json:ro
+ - ./conf/keycloak/themes/airavata:/opt/keycloak/themes/airavata:ro
command:
- start-dev
- --import-realm
@@ -100,6 +102,7 @@ services:
- --health-enabled=true
- --metrics-enabled=true
- --log-level=INFO
+ - --spi-theme-default=airavata
ports:
- "18080:18080"
healthcheck:
@@ -109,6 +112,23 @@ services:
retries: 10
start_period: 60s
+ sftp:
+ image: atmoz/sftp:latest
+ container_name: airavata-sftp
+ restart: unless-stopped
+ volumes:
+ - ./conf/sftp/id_rsa.pub:/home/airavata/.ssh/keys/id_rsa.pub:ro
+ - sftp_data:/home/airavata/storage
+ ports:
+ - "2222:22"
+ command: "airavata::1000"
+ healthcheck:
+ test: ["CMD-SHELL", "cat /proc/1/cmdline | tr '\\0' ' ' | grep -q sshd
|| exit 1"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 10s
+
# Dev tools (start with: docker compose --profile tools up -d)
adminer:
@@ -128,3 +148,4 @@ volumes:
zk_data:
zk_logs:
kafka_data:
+ sftp_data:
diff --git a/conf/keycloak/realm-default.json b/conf/keycloak/realm-default.json
index d8bac079b0..3eb295fcb3 100644
--- a/conf/keycloak/realm-default.json
+++ b/conf/keycloak/realm-default.json
@@ -1395,8 +1395,8 @@
{
"id": "5e2398e0-3498-4da3-9262-4f2dcc7448fa",
"clientId": "pga",
- "name": "Cybeshuttle Client",
- "description": "Client For Cybershuttle Services",
+ "name": "Airavata Portal",
+ "description": "Client for Airavata Portal",
"rootUrl": "",
"adminUrl": "",
"baseUrl": "",
@@ -1406,8 +1406,7 @@
"clientAuthenticatorType": "client-secret",
"secret": "m36BXQIxX3j3VILadeHMK5IvbOeRlCCc",
"redirectUris": [
- "http://airavata.localhost:8008/callback*",
- "http://airavata.localhost:8009/auth/callback*"
+ "http://localhost:8000/*"
],
"webOrigins": [
"+"
@@ -2297,7 +2296,7 @@
"strictTransportSecurity": ""
},
"smtpServer": {},
- "loginTheme": "custom-theme",
+ "loginTheme": "airavata",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
@@ -2578,14 +2577,12 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "ALTERNATIVE",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "Verify Existing Account by Re-authentication",
"userSetupAllowed": false
}
@@ -2604,7 +2601,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2612,7 +2608,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2630,7 +2625,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2638,7 +2632,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2656,7 +2649,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2664,7 +2656,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2682,14 +2673,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "Account verification options",
"userSetupAllowed": false
}
@@ -2708,7 +2697,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2716,7 +2704,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2735,14 +2722,12 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "ALTERNATIVE",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "Handle Existing Account",
"userSetupAllowed": false
}
@@ -2761,14 +2746,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "CONDITIONAL",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "First broker login - Conditional OTP",
"userSetupAllowed": false
}
@@ -2787,7 +2770,6 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2795,23 +2777,20 @@
"authenticatorFlow": false,
"requirement": "DISABLED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorConfig": "oidc",
"authenticator": "identity-provider-redirector",
"authenticatorFlow": false,
- "requirement": "ALTERNATIVE",
+ "requirement": "DISABLED",
"priority": 25,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "ALTERNATIVE",
"priority": 30,
- "autheticatorFlow": true,
"flowAlias": "forms",
"userSetupAllowed": false
}
@@ -2830,7 +2809,6 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2838,7 +2816,6 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2846,7 +2823,6 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 30,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2854,7 +2830,6 @@
"authenticatorFlow": false,
"requirement": "ALTERNATIVE",
"priority": 40,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2872,7 +2847,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -2880,14 +2854,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "CONDITIONAL",
"priority": 30,
- "autheticatorFlow": true,
"flowAlias": "Direct Grant - Conditional OTP",
"userSetupAllowed": false
}
@@ -2906,7 +2878,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -2925,14 +2896,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "User creation or linking",
"userSetupAllowed": false
}
@@ -2951,14 +2920,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "CONDITIONAL",
"priority": 20,
- "autheticatorFlow": true,
"flowAlias": "Browser - Conditional OTP",
"userSetupAllowed": false
}
@@ -2977,7 +2944,6 @@
"authenticatorFlow": true,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": true,
"flowAlias": "registration form",
"userSetupAllowed": false
}
@@ -2996,7 +2962,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -3004,7 +2969,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 50,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -3012,7 +2976,6 @@
"authenticatorFlow": false,
"requirement": "DISABLED",
"priority": 60,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -3020,7 +2983,6 @@
"authenticatorFlow": false,
"requirement": "DISABLED",
"priority": 70,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
@@ -3038,7 +3000,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -3046,7 +3007,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 20,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
@@ -3054,14 +3014,12 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 30,
- "autheticatorFlow": false,
"userSetupAllowed": false
},
{
"authenticatorFlow": true,
"requirement": "CONDITIONAL",
"priority": 40,
- "autheticatorFlow": true,
"flowAlias": "Reset - Conditional OTP",
"userSetupAllowed": false
}
@@ -3080,7 +3038,6 @@
"authenticatorFlow": false,
"requirement": "REQUIRED",
"priority": 10,
- "autheticatorFlow": false,
"userSetupAllowed": false
}
]
diff --git a/conf/keycloak/themes/airavata/login/resources/css/custom.css
b/conf/keycloak/themes/airavata/login/resources/css/custom.css
new file mode 100644
index 0000000000..7acffbf572
--- /dev/null
+++ b/conf/keycloak/themes/airavata/login/resources/css/custom.css
@@ -0,0 +1,297 @@
+/* Airavata Portal Keycloak Theme — pixel-matched to portal landing page */
+
+/* ── NUCLEAR: Force all card elements to 480px width ── */
+.pf-v5-c-login__container,
+.pf-v5-c-login__container > *,
+.pf-v5-c-login__header,
+.pf-v5-c-login__main,
+.pf-v5-c-login__main-header,
+.pf-v5-c-login__main-footer {
+ width: 480px !important;
+ min-width: 480px !important;
+ max-width: 480px !important;
+ box-sizing: border-box !important;
+}
+
+/* ── Background ── */
+html, body, .login-pf, .pf-v5-c-login {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
+ min-height: 100vh !important;
+}
+
+/* ── Center everything ── */
+.pf-v5-c-login {
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ padding: 2rem 0 !important;
+}
+
+/* Kill any grid/flex that shifts the container off-center */
+.pf-v5-c-login > * {
+ margin: 0 auto !important;
+}
+
+/* Absolute center the container */
+.pf-v5-c-login__container {
+ position: absolute !important;
+ left: 50% !important;
+ top: 50% !important;
+ transform: translate(-50%, -50%) !important;
+}
+
+/* ── Container: single column, card width ── */
+.pf-v5-c-login__container {
+ display: flex !important;
+ flex-direction: column !important;
+ align-items: stretch !important;
+ width: 480px !important;
+ min-width: 480px !important;
+ max-width: 480px !important;
+ grid-template-columns: unset !important;
+ grid-template-rows: unset !important;
+ gap: 0 !important;
+}
+
+/* ── Realm header: INSIDE the card as top section ── */
+.pf-v5-c-login__header {
+ background: rgba(255, 255, 255, 0.95) !important;
+ border-radius: 16px 16px 0 0 !important;
+ text-align: center !important;
+ padding: 1.75rem 2rem 0 !important;
+ margin: 0 !important;
+ grid-area: unset !important;
+ box-shadow: none !important;
+ border-bottom: none !important;
+ width: 100% !important;
+ align-self: stretch !important;
+}
+
+#kc-header-wrapper {
+ color: #1a1a2e !important;
+ font-weight: 700 !important;
+ letter-spacing: 0.02em !important;
+ text-transform: none !important;
+ font-size: 1.75rem !important;
+ margin-bottom: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+/* Logo above realm name — same as portal landing page */
+#kc-header-wrapper::before {
+ content: '' !important;
+ display: block !important;
+ width: 80px !important;
+ height: 80px !important;
+ margin: 0 auto 0.5rem !important;
+ background: url('../img/logo.png') center/contain no-repeat !important;
+}
+
+/* ── Card body ── */
+.pf-v5-c-login__main {
+ background: rgba(255, 255, 255, 0.95) !important;
+ backdrop-filter: blur(10px) !important;
+ border-radius: 0 0 16px 16px !important;
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15) !important;
+ border-top: none !important;
+ overflow: hidden !important;
+ width: 100% !important;
+ align-self: stretch !important;
+ grid-area: unset !important;
+}
+
+/* ── Card header (title) ── */
+.pf-v5-c-login__main-header {
+ padding: 0.25rem 2rem 0 !important;
+ background: transparent !important;
+ text-align: center !important;
+}
+
+.pf-v5-c-login__main-header .pf-v5-c-title,
+#kc-page-title {
+ color: #666 !important;
+ font-weight: 400 !important;
+ font-size: 1rem !important;
+ text-align: center !important;
+ margin-bottom: 1rem !important;
+}
+
+/* ── Card body (form) ── */
+.pf-v5-c-login__main-body {
+ padding: 0 2rem 1rem !important;
+ background: transparent !important;
+}
+
+/* Tight form spacing */
+.pf-v5-c-login__main-body .pf-v5-c-form {
+ gap: 0 !important;
+}
+.pf-v5-c-login__main-body .pf-v5-c-form__group {
+ margin-top: 0 !important;
+ margin-bottom: 0.5rem !important;
+}
+.pf-v5-c-login__main-body .pf-v5-c-form__group:last-child {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+/* ── Labels ── */
+.pf-v5-c-form__label-text, label {
+ color: #333 !important;
+ font-weight: 500 !important;
+ font-size: 0.9rem !important;
+}
+
+/* ── Inputs ── */
+.pf-v5-c-form-control,
+.pf-v5-c-input-group,
+.pf-v5-c-text-input-group {
+ border: 1px solid #dee2e6 !important;
+ border-radius: 8px !important;
+ background: #fff !important;
+ overflow: hidden !important;
+}
+
+.pf-v5-c-form-control:focus-within,
+.pf-v5-c-text-input-group:focus-within,
+.pf-v5-c-input-group:focus-within {
+ border-color: #667eea !important;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2) !important;
+}
+
+input[type="text"], input[type="password"], input[type="email"] {
+ color: #333 !important;
+ padding: 0.625rem 0.75rem !important;
+}
+
+/* ── Password toggle ── */
+.pf-v5-c-button.pf-m-control {
+ background: #fff !important;
+ color: #666 !important;
+ border: none !important;
+ border-left: 1px solid #dee2e6 !important;
+ border-radius: 0 !important;
+ padding: 0.5rem 0.75rem !important;
+}
+
+.pf-v5-c-button.pf-m-control:hover {
+ background: #f0f0f0 !important;
+ color: #333 !important;
+}
+
+/* ── Primary button ── */
+.pf-v5-c-button.pf-m-primary,
+#kc-login {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
+ border: none !important;
+ border-radius: 8px !important;
+ padding: 0.875rem 2.5rem !important;
+ font-weight: 600 !important;
+ font-size: 1rem !important;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
+ color: #fff !important;
+ transition: transform 0.15s ease, box-shadow 0.15s ease !important;
+ width: 100% !important;
+ display: block !important;
+ text-align: center !important;
+ margin-top: 0.5rem !important;
+}
+
+.pf-v5-c-button.pf-m-primary:hover,
+#kc-login:hover {
+ transform: translateY(-2px) !important;
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important;
+ color: #fff !important;
+}
+
+/* ── Footer (social/IdP) ── */
+.pf-v5-c-login__main-footer {
+ padding: 0.25rem 2rem 1rem !important;
+ background: transparent !important;
+ grid-area: unset !important;
+}
+
+/* Make footer content same width as form body */
+.pf-v5-c-login__main-footer-links-item {
+ padding: 0 !important;
+}
+
+.pf-v5-c-login__main-footer-band {
+ border-top: none !important;
+ padding-top: 0 !important;
+ margin-top: 0 !important;
+ background: transparent !important;
+}
+
+.pf-v5-c-login__main-footer-band-item:first-child {
+ color: #999 !important;
+ font-size: 0.85rem !important;
+ text-align: center !important;
+ margin-bottom: 0 !important;
+ padding: 0 !important;
+ line-height: 1.2 !important;
+}
+
+/* IdP buttons — muted outlined style */
+.pf-v5-c-login__main-footer-links {
+ list-style: none !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ margin-top: 0 !important;
+ display: flex !important;
+ flex-direction: column !important;
+ gap: 0.5rem !important;
+}
+
+.pf-v5-c-login__main-footer-links-item {
+ width: 100% !important;
+}
+
+.pf-v5-c-login__main-footer-links-item-link {
+ border-radius: 8px !important;
+ border: 2px solid #667eea !important;
+ background: transparent !important;
+ color: #667eea !important;
+ padding: 0.625rem 1rem !important;
+ width: 100% !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ gap: 0.5rem !important;
+ font-weight: 600 !important;
+ font-size: 0.95rem !important;
+ line-height: 1.4 !important;
+ transition: all 0.15s ease !important;
+ text-decoration: none !important;
+ box-sizing: border-box !important;
+}
+
+.pf-v5-c-login__main-footer-links-item-link img,
+.pf-v5-c-login__main-footer-links-item-link svg {
+ width: 20px !important;
+ height: 20px !important;
+ vertical-align: middle !important;
+ flex-shrink: 0 !important;
+ filter: brightness(0) saturate(100%) invert(40%) sepia(85%) saturate(800%)
hue-rotate(210deg) !important;
+}
+
+.pf-v5-c-login__main-footer-links-item-link span {
+ vertical-align: middle !important;
+}
+
+.pf-v5-c-login__main-footer-links-item-link:hover {
+ background: rgba(102, 126, 234, 0.08) !important;
+ border-color: #764ba2 !important;
+ color: #764ba2 !important;
+ text-decoration: none !important;
+}
+
+/* ── Links ── */
+.pf-v5-c-login__main-body a { color: #667eea !important; }
+.pf-v5-c-login__main-body a:hover { color: #764ba2 !important; }
+
+/* ── Alerts ── */
+.pf-v5-c-alert { border-radius: 8px !important; }
+
+/* ── Hide Keycloak footer ── */
+.pf-v5-c-login__footer { display: none !important; }
diff --git a/conf/keycloak/themes/airavata/login/resources/img/logo.png
b/conf/keycloak/themes/airavata/login/resources/img/logo.png
new file mode 100644
index 0000000000..65b39992c6
Binary files /dev/null and
b/conf/keycloak/themes/airavata/login/resources/img/logo.png differ
diff --git a/conf/keycloak/themes/airavata/login/theme.properties
b/conf/keycloak/themes/airavata/login/theme.properties
new file mode 100644
index 0000000000..7349d78df7
--- /dev/null
+++ b/conf/keycloak/themes/airavata/login/theme.properties
@@ -0,0 +1,4 @@
+parent=keycloak.v2
+import=common/keycloak
+
+styles=css/login.css css/custom.css
diff --git a/pom.xml b/pom.xml
index 8a57cd9ab2..85626ced23 100644
--- a/pom.xml
+++ b/pom.xml
@@ -493,6 +493,13 @@ under the License.
<configuration>
<release>17</release>
<fork>true</fork>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>org.mapstruct</groupId>
+ <artifactId>mapstruct-processor</artifactId>
+ <version>1.6.3</version>
+ </path>
+ </annotationProcessorPaths>
</configuration>
</plugin>
<plugin>