diff --git a/contracts/ripple/backend/backend.yml.tftpl b/contracts/ripple/backend/backend.yml.tftpl
index c4df6a7..c1b7fbe 100644
--- a/contracts/ripple/backend/backend.yml.tftpl
+++ b/contracts/ripple/backend/backend.yml.tftpl
@@ -1,3 +1,51 @@
+%{ for i, vault in tpl.vaults ~}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: vault-${i}-supervisord-config
+data:
+ supervisord.conf: |
+ [supervisord]
+ nodaemon=true
+ logfile=/dev/null
+ logfile_maxbytes=0
+ pidfile=/tmp/supervisord.pid
+
+ [program:vault-core]
+ command=/opt/vault-core/bin/vault-core-omnibus-s390x-g++ -p ${vault.platform} -u 0.0.0.0:${vault.grpc_port}
+ directory=/opt/vault-core
+ autostart=true
+ autorestart=true
+ autorestart_delay=2
+ startretries=5
+ stdout_logfile=/dev/fd/1
+ stdout_logfile_maxbytes=0
+ stderr_logfile=/dev/fd/2
+ stderr_logfile_maxbytes=0
+
+ [program:vault-bridge]
+ command=/opt/vault-bridge/bin/vault-bridge
+ directory=/opt/vault-bridge
+ priority=3
+ stdout_logfile=/dev/fd/1
+ stdout_logfile_maxbytes=0
+ stderr_logfile=/dev/fd/2
+ stderr_logfile_maxbytes=0
+ startsecs=0
+ stopasgroup=true
+
+ [program:healthcheck]
+ command=/opt/vault-bridge-health-check/bin/vault-bridge-health-check
+ directory=/opt/vault-bridge-health-check
+ priority=4
+ stdout_logfile=/dev/fd/1
+ stdout_logfile_maxbytes=0
+ stderr_logfile=/dev/fd/2
+ stderr_logfile_maxbytes=0
+ startsecs=0
+ stopasgroup=true
+%{ endfor ~}
---
apiVersion: v1
kind: Pod
@@ -11,12 +59,14 @@ spec:
- name: cold-bridge
image: ${tpl.cold_bridge_image}
env:
- # This must be same as VAULT_ID below
- - name: Vault__Ids__0
- value: "${tpl.vault_id}"
+ # Dynamic vault IDs - supports 1 to N vaults
+%{ for i, vault in tpl.vaults ~}
+ - name: Vault__Ids__${i}
+ value: "${vault.vault_id}"
+%{ endfor ~}
# Passphrase: "{{EMPTY}}" is needed so that the bridge data is not cyphered and can be managed by OSO
- name: Passphrase
- value: "${tpl.passphrase}"
+ value: "{{EMPTY}}"
volumeMounts:
- name: app-data
mountPath: /App_Data
@@ -26,15 +76,32 @@ spec:
volumeMounts:
- name: ibm-cfg
mountPath: /opt/kms/cfg/ibm.cfg
+ - name: ibm-cfg
+ mountPath: /app/cfg/ibm.cfg
- name: cert
mountPath: /data/cert
readOnly: true
- - name: cold-vault
+ # KMS vault containers - one per vault
+ # supervisord.conf is mounted from a ConfigMap per vault with unique -u
+ # so all vault instances can coexist in the shared pod network namespace
+%{ for i, vault in tpl.vaults ~}
+ - name: vault-${i}
image: ${tpl.cold_vault_image}
+ command:
+ - /bin/bash
+ - -c
+ args:
+ - |
+ until (echo > /dev/tcp/localhost/10000) 2>/dev/null; do
+ sleep 2
+ done
+ exec /opt/entrypoint.sh
env:
+ - name: VAULT_CORE_ADDRESS
+ value: "0.0.0.0:${vault.grpc_port}"
- name: PLATFORM
- value: "kms"
+ value: "${vault.platform}"
- name: VAULT_BRIDGE_LOGLEVEL
value: "7"
- name: VAULT_CORE_LOGLEVEL
@@ -44,10 +111,24 @@ spec:
- name: VAULT_TRUSTED_SIG
value: "pem:${tpl.notary_messaging_public_key}"
- name: VAULT_ID
- value: "${tpl.vault_id}"
- # This comes from the docker container that is spun up above. If it's normal vault, then it would be API end point.
+ value: "${vault.vault_id}"
- name: HARMONIZE_CORE_ENDPOINT
value: "http://localhost:8080/internal/v1"
+ - name: HMZ_FEATURE_OPTIONAL_MAXIMUM_FEE
+ value: "true"
+%{ if vault.log_level != "" ~}
+ - name: HMZ_LOG_LEVEL
+ value: "${vault.log_level}"
+%{ endif ~}
+%{ if vault.vault_log_level != "" ~}
+ - name: VAULT_LOG_LEVEL
+ value: "${vault.vault_log_level}"
+%{ endif ~}
+ volumeMounts:
+ - name: vault-${i}-supervisord-config
+ mountPath: /opt/supervisord/supervisord.conf
+ subPath: supervisord.conf
+%{ endfor ~}
- name: backend-plugin
image: ${tpl.backend_plugin_image}
@@ -58,16 +139,34 @@ spec:
env:
- name: COMPONENT
value: "backend_plugin"
- - name: SEED
+ - name: OSOENCRYPTIONPASS
value: "${tpl.seed}"
+ - name: COLD_BRIDGE_ENDPOINT
+ value: "http://localhost:8080"
- name: PORT
value: "4000"
- - name: COLD_BRIDGE_ENDPOINT
- value: "${tpl.cold_bridge_endpoint}"
+ - name: DEBUG
+ value: "${tpl.debug}"
+%{ if tpl.debug == "true" ~}
+ - name: SSH_PORT
+ value: "${tpl.ssh_port}"
+ - name: OSO_SSH_PUBKEY
+ value: "${tpl.ssh_pubkey}"
+ - name: OSO_SSH_PASSWORD
+ value: "${tpl.ssh_password}"
+%{ endif ~}
+%{ if tpl.debug == "true" ~}
+ volumeMounts:
+ - name: debug
+ mountPath: /debug
+%{ endif ~}
ports:
- containerPort: 4000
hostPort: 4000
-
+%{ if tpl.debug == "true" ~}
+ - containerPort: ${tpl.ssh_port}
+ hostPort: ${tpl.ssh_port}
+%{ endif ~}
%{ if tpl.enable_ep11server }
- name: ep11server
@@ -135,3 +234,13 @@ spec:
type: CharDevice
%{~ endif ~}
+%{ for i, vault in tpl.vaults ~}
+ - name: vault-${i}-supervisord-config
+ configMap:
+ name: vault-${i}-supervisord-config
+%{ endfor ~}
+
+%{ if tpl.debug == "true" ~}
+ - name: debug
+ emptyDir: {}
+%{ endif ~}
\ No newline at end of file
diff --git a/contracts/ripple/backend/terraform.tfvars.template b/contracts/ripple/backend/terraform.tfvars.template
index b99bd05..16dfbf4 100644
--- a/contracts/ripple/backend/terraform.tfvars.template
+++ b/contracts/ripple/backend/terraform.tfvars.template
@@ -9,6 +9,13 @@ COLD_BRIDGE_IMAGE="registry.control23.dap.local/metaco-ripple/vault-cold-bridge@
COLD_VAULT_IMAGE="registry.control23.dap.local/metaco-ripple/vault-releases@sha256:"
KMSCONNECT_IMAGE="registry.control23.dap.local/metaco-ripple/kms-ibm@sha256:"
+MOCK_VAULTS = [
+ {
+ vault_id = ""
+ mock_phrase = ""
+ }
+]
+
# Vault Configuration
VAULT_ID=""
NOTARY_MESSAGING_PUBLIC_KEY=""
@@ -19,6 +26,13 @@ WORKLOAD_VOLUME_PREV_SEED=""
# Default value points to the cold-bridge service running on localhost
# COLD_BRIDGE_ENDPOINT="http://localhost:8080"
+# Multi-Vault Configuration (use VAULTS instead of VAULT_ID for multiple vaults)
+# Each vault container will be assigned KMS_URL="0.0.0.0:10001", "0.0.0.0:10002", etc.
+# VAULTS = [
+# { vault_id = "", log_level = "", vault_log_level = "" },
+# { vault_id = "", log_level = "", vault_log_level = "" },
+# ]
+
# Update volume name if it is different from default value vault_vol
# VOLUME_NAME = ""
diff --git a/contracts/ripple/backend/user_data_backend.tf b/contracts/ripple/backend/user_data_backend.tf
index 8b7d1c3..1bcf0b6 100644
--- a/contracts/ripple/backend/user_data_backend.tf
+++ b/contracts/ripple/backend/user_data_backend.tf
@@ -38,6 +38,30 @@ resource "local_file" "grep_client_cert" {
}
+# Local variable to handle both single vault (VAULT_ID) and multi-vault (VAULTS) configurations
+# Normalizes all vault configurations to KMS-only platform with required runtime settings
+locals {
+ # If VAULT_ID is provided, create a single-vault list with default KMS values
+ # Otherwise, use the VAULTS list (which is already KMS-only)
+ resolved_vaults_raw = var.VAULT_ID != "" ? [
+ {
+ vault_id = var.VAULT_ID
+ log_level = ""
+ vault_log_level = ""
+ }
+ ] : var.VAULTS
+
+ # Inject grpc_port per vault (10001, 10002, 10003, ...) so the template
+ # can reference vault.grpc_port without inline arithmetic.
+ # KMS_URL is set to "0.0.0.0:" on each vault container so that
+ # each vault instance binds its gRPC listener on a unique port within the
+ # shared pod network namespace. cold-bridge is told the matching endpoint
+ # via Vault__GrpcEndpoints__N. See: DAPCS-1965.
+ resolved_vaults = [
+ for i, v in local.resolved_vaults_raw : merge(v, { grpc_port = 10001 + i, platform = "kms" })
+ ]
+}
+
resource "local_file" "podman-play" {
content = templatefile(
"${path.module}/backend.yml.tftpl",
@@ -46,14 +70,18 @@ resource "local_file" "podman-play" {
cold_bridge_image = var.COLD_BRIDGE_IMAGE,
cold_vault_image = var.COLD_VAULT_IMAGE,
kmsconnect_image = var.KMSCONNECT_IMAGE,
- vault_id = var.VAULT_ID,
+ vaults = local.resolved_vaults,
passphrase = var.PASSPHRASE,
notary_messaging_public_key = var.NOTARY_MESSAGING_PUBLIC_KEY,
- seed = var.SEED,
cold_bridge_endpoint = var.COLD_BRIDGE_ENDPOINT,
+ seed = var.OSOENCRYPTIONPASS,
enable_ep11server = var.INTERNAL_GREP11,
crypto_pass_enable = var.CRYPTO_PASSTHROUGH_ENABLEMENT,
grep11_image = var.GREP11_IMAGE,
+ debug = var.DEBUG ? "true" : "false",
+ ssh_pubkey = var.SSH_PUBKEY,
+ ssh_port = var.SSH_PORT,
+ ssh_password = var.SSH_PASSWORD,
} },
)
filename = "podman-play/play.yml"
@@ -81,8 +109,8 @@ resource "null_resource" "crypto_deps" {
]
}
-# archive of the folder containing docker-compose file. This folder could create additional resources such as files
-# to be mounted into containers, environment files etc. This is why all of these files get bundled in a tgz file (base64 encoded)
+# archive of the folder containing the podman-play pod YAML and supporting files (ibm.cfg, certs, etc.)
+# All of these files get bundled into a tgz (base64 encoded) for the HPCR workload contract.
resource "hpcr_tgz" "workload" {
depends_on = [local_file.podman-play]
folder = "podman-play"
diff --git a/contracts/ripple/backend/variables.tf b/contracts/ripple/backend/variables.tf
index 74f42d2..af93171 100644
--- a/contracts/ripple/backend/variables.tf
+++ b/contracts/ripple/backend/variables.tf
@@ -20,10 +20,10 @@ variable "PREFIX" {
variable "DEBUG" {
type = bool
description = "Create debug contracts, plaintext"
- default = false
+ default = true
}
-variable "SEED" {
+variable "OSOENCRYPTIONPASS" {
type = string
description = "Encrypt data through the iteration pipeline (should be the same value as frontend plugin)"
default = ""
@@ -34,6 +34,12 @@ variable "BACKEND_PLUGIN_IMAGE" {
description = "Backend plugin image containing registry"
}
+variable "BACKEND_ENDPOINT" {
+ type = string
+ description = "Backend plugin endpoint URL (required by backend_plugin_manager.py)"
+ default = "http://localhost:4000"
+}
+
variable "COLD_BRIDGE_ENDPOINT" {
type = string
description = "Cold bridge endpoint URL for the cold bridge service"
@@ -56,16 +62,28 @@ variable "KMSCONNECT_IMAGE" {
}
variable "VAULT_ID" {
- type = string
- description = "Vault ID"
+ type = string
+ description = "Vault ID (single vault, use VAULTS for multi-vault)"
+ default = ""
+}
+
+variable "VAULTS" {
+ type = list(object({
+ vault_id = string
+ log_level = optional(string, "")
+ vault_log_level = optional(string, "")
+ }))
+ description = "List of KMS vault configurations (supports 1 to N vaults). Use instead of VAULT_ID for multi-vault setups."
+ default = []
}
variable "PASSPHRASE" {
- type = string
- default = "{{EMPTY}}"
- description = "Required to enable plugin to view content within a JSON format"
+ type = string
+ description = "Passphrase for cold-bridge. Use '{{EMPTY}}' so that the bridge data is not cyphered and can be managed by OSO"
+ default = "{{EMPTY}}"
}
+
variable "NOTARY_MESSAGING_PUBLIC_KEY" {
type = string
description = "Notary messaging public key after performing genesis"
@@ -199,3 +217,22 @@ variable "CRYPTO_PASSTHROUGH_ENABLEMENT" {
default = true
description = "Crypto passthrough enablement configuration"
}
+
+variable "SSH_PUBKEY" {
+ type = string
+ description = "SSH public key for debug access"
+ default = ""
+}
+
+variable "SSH_PORT" {
+ type = string
+ description = "SSH port for debug access"
+ default = "5000"
+}
+
+variable "SSH_PASSWORD" {
+ type = string
+ description = "SSH password for debug access (fallback when publickey auth fails). Only active when DEBUG=true."
+ default = ""
+ sensitive = true
+}
diff --git a/contracts/ripple/frontend_plugin/frontend_plugin.yml.tftpl b/contracts/ripple/frontend_plugin/frontend_plugin.yml.tftpl
index 33477e2..c61d983 100644
--- a/contracts/ripple/frontend_plugin/frontend_plugin.yml.tftpl
+++ b/contracts/ripple/frontend_plugin/frontend_plugin.yml.tftpl
@@ -1,10 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
- name: frontend_plugin
+ name: frontend-plugin-pod
spec:
containers:
- - name: frontend_plugin
+ - name: frontend-plugin
image: ${tpl.image}
envFrom:
- configMapRef:
@@ -25,10 +25,12 @@ spec:
value: "${tpl.HMZ_API_HOSTNAME}"
- name: ROOTCERT
value: "${tpl.ROOTCERT}"
- - name: SEED
+ - name: SEED
value: "${tpl.SEED}"
- name: TOKEN_EXP
value: "${tpl.TOKEN_EXP}"
ports:
- - containerPort: 4000
+ - containerPort: 4000
hostPort: 4000
+
+
diff --git a/contracts/ripple/frontend_plugin/terraform.tfvars.template b/contracts/ripple/frontend_plugin/terraform.tfvars.template
index 49984d2..41b02d6 100644
--- a/contracts/ripple/frontend_plugin/terraform.tfvars.template
+++ b/contracts/ripple/frontend_plugin/terraform.tfvars.template
@@ -6,3 +6,4 @@ HMZ_AUTH_HOSTNAME=""
HMZ_API_HOSTNAME=""
VAULT_ID=""
SK=""
+HMZ_USER_SK=""
diff --git a/contracts/ripple/frontend_plugin/user_data_frontend_plugin.tf b/contracts/ripple/frontend_plugin/user_data_frontend_plugin.tf
index 84a1039..3d2b906 100644
--- a/contracts/ripple/frontend_plugin/user_data_frontend_plugin.tf
+++ b/contracts/ripple/frontend_plugin/user_data_frontend_plugin.tf
@@ -19,9 +19,11 @@ resource "local_file" "frontend_plugin_podman_play" {
{ tpl = {
image = var.FRONTEND_PLUGIN_IMAGE,
SK = var.SK,
- VAULTID = var.VAULT_ID,
+ VAULTID = join(" ", var.VAULT_IDS),
HMZ_AUTH_HOSTNAME = var.HMZ_AUTH_HOSTNAME,
HMZ_API_HOSTNAME = var.HMZ_API_HOSTNAME,
+ //HMZ_AUTH_PATH = var.HMZ_AUTH_PATH,
+ //HMZ_AUTH_CUSTOMERID = var.HMZ_AUTH_CUSTOMERID,
ROOTCERT = var.ROOTCERT,
SEED = var.SEED,
TOKEN_EXP = var.TOKEN_EXP
diff --git a/contracts/ripple/frontend_plugin/variables.tf b/contracts/ripple/frontend_plugin/variables.tf
index 1ec565e..70979e9 100644
--- a/contracts/ripple/frontend_plugin/variables.tf
+++ b/contracts/ripple/frontend_plugin/variables.tf
@@ -31,21 +31,9 @@ variable "FRONTEND_PLUGIN_IMAGE" {
description = "Frontend plugin image name"
}
-variable "SEED" {
- type = string
- description = "Encrypt data through the iteration pipeline (should be same value as backend plugin)"
- default = ""
-}
-
-# Ripple
-variable "SK" {
- type = string
- description = "Private (secret) key of a registered user used to login to Ripple"
-}
-
-variable "VAULT_ID" {
- type = string
- description = "Ripple vault id"
+variable "VAULT_IDS" {
+ type = list(string)
+ description = "List of Ripple vault IDs (supports 1 to N vaults)"
}
variable "HMZ_AUTH_HOSTNAME" {
@@ -69,3 +57,26 @@ variable "TOKEN_EXP" {
description = "Ripple configured bearer token expiration (#h#m#s format)"
default = "4h0m0s"
}
+
+variable "SEED" {
+ type = string
+ description = "Encrypt data through the iteration pipeline (should be same value as backend plugin)"
+ default = ""
+ sensitive = true
+}
+
+variable "SK" {
+ type = string
+ description = "Private (secret) key of a registered user used to login to Ripple"
+}
+
+#variable "HMZ_AUTH_PATH" {
+# type = string
+# description = "Harmonize path to get auth token"
+#}
+
+#variable "HMZ_AUTH_CUSTOMERID" {
+# type = string
+# description = "Harmonize customer id used to authenticate"
+# default = "customer_api"
+#}
diff --git a/contracts/ripple/get_workloads.sh b/contracts/ripple/get_workloads.sh
index 588ad49..6ba0c54 100755
--- a/contracts/ripple/get_workloads.sh
+++ b/contracts/ripple/get_workloads.sh
@@ -44,10 +44,10 @@ BACKEND_WORKLOADS=[
hipersocket34: false,
workload: "$BACKEND",
persistent_vol: {
- volume_name = "vault_vol",
- env_seed = "vaultseed2",
+ volume_name = "vault_vol_lpar3",
+ env_seed = "vaultseed1I23456780",
prev_seed = "",
- volume_path = "/var/lib/libvirt/images/oso/vault-data.qcow2"
+ volume_path = "/var/lib/libvirt/images/oso/vault_vol_lpar3.qcow2"
}
}
]
diff --git a/ripple-plugin/Dockerfile b/ripple-plugin/Dockerfile
index ac3512c..2562643 100644
--- a/ripple-plugin/Dockerfile
+++ b/ripple-plugin/Dockerfile
@@ -17,6 +17,7 @@
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest AS live
ENV HOME=/app-root
+
RUN microdnf --assumeyes module enable nginx:1.24 \
&& microdnf --assumeyes \
--setopt=install_weak_deps=0 \
@@ -25,12 +26,27 @@ RUN microdnf --assumeyes module enable nginx:1.24 \
--enablerepo=ubi-9-appstream-rpms \
install \
python3.12 \
+ openssh-server \
+ shadow-utils \
gettext nginx findutils \
- && microdnf clean all
+ procps-ng \
+ && microdnf clean all \
+ && useradd --uid 1001 --gid 0 --home-dir ${HOME} --no-create-home appuser
+
+# SSH_PASSWORD is injected via --secret at build time (debug builds only).
+# Using a secret avoids baking the password into image layers or docker history.
+RUN --mount=type=secret,id=ssh_password,required=false \
+ if [ -f /run/secrets/ssh_password ]; then \
+ echo "appuser:$(cat /run/secrets/ssh_password)" | chpasswd ; \
+ fi
RUN install --directory --mode 0700 --owner 1001 --group 0 \
"${HOME}" \
"${HOME}/.ssh" \
+ && ssh-keygen -A \
+ && chown -R 1001:0 /etc/ssh \
+ && chmod 0755 /etc/ssh/sshd_config \
+ && install --mode 0644 --owner 1001 --group 0 $(mktemp) ${HOME}/.ssh/authorized_keys \
&& chown -R 1001:0 /var/run \
&& chmod -R ug+rwX /var/run \
&& chown -R 1001:0 /var/lib/nginx \
@@ -51,6 +67,7 @@ RUN microdnf --assumeyes \
--enablerepo=ubi-9-baseos-rpms \
--enablerepo=ubi-9-appstream-rpms \
install \
+ openssh-server \
openssl-devel \
gcc cargo rustc \
python3.12-pip python3.12-devel \
@@ -94,8 +111,10 @@ RUN microdnf --assumeyes \
make \
python3.12-devel \
openssl-devel \
+ openssh-server \
&& microdnf clean all
COPY --from=compile /opt/venv /opt/venv
COPY --chown=1001:0 /unit-tests /pyproject.toml /tests/
RUN pip3.12 --python /opt/venv install --requirement /tmp/requirements.constraints.txt
-CMD ["pytest", "-svvv"]
\ No newline at end of file
+CMD ["pytest", "-svvv"]
+
diff --git a/ripple-plugin/Makefile b/ripple-plugin/Makefile
index 0d891ba..8924220 100644
--- a/ripple-plugin/Makefile
+++ b/ripple-plugin/Makefile
@@ -16,6 +16,8 @@
.PHONY : build debug test
+SSH_PASSWORD ?=
+
REGISTRY ?= us.icr.io
NAMESPACE ?= dap-osc-dev
TAG ?= latest
@@ -60,6 +62,18 @@ build :
docker build \
. --target release -t oso-ripple-plugins:latest -t $(REGISTRY)/$(NAMESPACE)/oso-ripple-plugins:$(TAG) -f Dockerfile --platform linux/s390x --provenance=false
+debug :
+ @if [ -z "$(SSH_PASSWORD)" ]; then \
+ echo "ERROR: SSH_PASSWORD is required for debug build. Use: make debug SSH_PASSWORD="; \
+ exit 1; \
+ fi
+ @printf '%s' "$(SSH_PASSWORD)" > /tmp/.ssh_password_secret
+ docker build \
+ --secret id=ssh_password,src=/tmp/.ssh_password_secret \
+ --label debug=true \
+ . --target release -t oso-ripple-plugins:latest -t $(REGISTRY)/$(NAMESPACE)/oso-ripple-plugins:$(TAG) -f Dockerfile --platform linux/s390x --provenance=false
+ @rm -f /tmp/.ssh_password_secret
+
ifdef RIPPLE_PLUGINS_TEST_RESULTS
VOL_OPTS ::= -v $(RIPPLE_PLUGINS_TEST_RESULTS):/tests/results:rw,z
endif
@@ -76,3 +90,4 @@ test: test-build
--platform linux/s390x \
--entrypoint pytest \
$(REGISTRY)/$(NAMESPACE)/oso-ripple-plugins-test:$(TAG)
+
diff --git a/ripple-plugin/src/app-root/backend_plugin/entrypoints/supervisord-backend_plugin.conf b/ripple-plugin/src/app-root/backend_plugin/entrypoints/supervisord-backend_plugin.conf
index de18949..c3c342e 100644
--- a/ripple-plugin/src/app-root/backend_plugin/entrypoints/supervisord-backend_plugin.conf
+++ b/ripple-plugin/src/app-root/backend_plugin/entrypoints/supervisord-backend_plugin.conf
@@ -37,7 +37,7 @@ redirect_stderr=true
stopsignal=QUIT
[program:sshd]
-command=sh -c '/usr/sbin/sshd -D'
+command=sh -c '/usr/sbin/sshd -D -f %(ENV_SSHD_CONFIG)s -E %(ENV_SSHD_LOG)s'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
diff --git a/ripple-plugin/src/app-root/common/entrypoints/entrypoint.sh b/ripple-plugin/src/app-root/common/entrypoints/entrypoint.sh
index f7fb706..e85f077 100755
--- a/ripple-plugin/src/app-root/common/entrypoints/entrypoint.sh
+++ b/ripple-plugin/src/app-root/common/entrypoints/entrypoint.sh
@@ -18,11 +18,88 @@
cp -r /oso-root/"${COMPONENT}"/* /app-root
if [ "${DEBUG}" == "true" ]; then
- echo "${SSH_PUBKEY}" >"${HOME}/.ssh/authorized_keys"
- sed -ie 's/#Port 22/Port '"$SSH_PORT"'/g' /etc/ssh/sshd_config
+ if [ -z "${SSH_PORT}" ]; then
+ echo "ERROR: DEBUG=true but SSH_PORT is not set" >&2
+ exit 1
+ fi
+ # Support both OSO_SSH_PUBKEY (new) and SSH_PUBKEY (legacy)
+ _SSH_PUBKEY="${OSO_SSH_PUBKEY:-${SSH_PUBKEY}}"
+ # Support both OSO_SSH_PASSWORD (new) and SSH_PASSWORD (legacy)
+ _SSH_PASSWORD="${OSO_SSH_PASSWORD:-${SSH_PASSWORD}}"
+
+ if [ -z "${_SSH_PUBKEY}" ] && [ -z "${_SSH_PASSWORD}" ]; then
+ echo "ERROR: DEBUG=true but neither OSO_SSH_PUBKEY nor OSO_SSH_PASSWORD is set" >&2
+ exit 1
+ fi
+
+ # ── authorized_keys ────────────────────────────────────────────────────────
+ if [ -n "${_SSH_PUBKEY}" ]; then
+ echo "[DEBUG] Writing OSO_SSH_PUBKEY to ${HOME}/.ssh/authorized_keys"
+ echo "${_SSH_PUBKEY}" > "${HOME}/.ssh/authorized_keys"
+ chmod 0600 "${HOME}/.ssh/authorized_keys"
+ else
+ echo "[DEBUG] OSO_SSH_PUBKEY not set — publickey auth will not work"
+ > "${HOME}/.ssh/authorized_keys"
+ fi
+
+ # ── optional password auth ──────────────────────────────────────────────────
+ # Password is set at image build time (Dockerfile ARG SSH_PASSWORD) since
+ # the container runs as UID 1001 which cannot write /etc/passwd at runtime.
+ if [ -n "${_SSH_PASSWORD}" ]; then
+ PASSWORD_AUTH="yes"
+ else
+ PASSWORD_AUTH="no"
+ fi
+
+ # ── container-local sshd config — never touch host /etc/ssh/sshd_config ───
+ # Write a minimal self-contained config from scratch.
+ # Avoids inheriting the base config's "Include" drop-ins or conflicting
+ # directives (e.g. PasswordAuthentication no from sshd_config.d/).
+ SSHD_CONFIG="${HOME}/.ssh/sshd_config"
+ SSHD_LOG="${HOME}/.ssh/sshd.log"
+ cat > "${SSHD_CONFIG}" <<-EOF
+ Port ${SSH_PORT}
+ ListenAddress 0.0.0.0
+ HostKey /etc/ssh/ssh_host_rsa_key
+ HostKey /etc/ssh/ssh_host_ecdsa_key
+ HostKey /etc/ssh/ssh_host_ed25519_key
+ AuthorizedKeysFile ${HOME}/.ssh/authorized_keys
+ PasswordAuthentication ${PASSWORD_AUTH}
+ PubkeyAuthentication yes
+ StrictModes no
+ ChallengeResponseAuthentication no
+ KbdInteractiveAuthentication no
+ UsePAM yes
+ LogLevel DEBUG3
+ SyslogFacility AUTH
+ Subsystem sftp /usr/libexec/openssh/sftp-server
+ EOF
+ chmod 0644 "${SSHD_CONFIG}"
+ touch "${SSHD_LOG}"
+ chmod 0644 "${SSHD_LOG}"
+ export SSHD_CONFIG
+ export SSHD_LOG
+
+ # ── diagnostics printed at container startup ────────────────────────────────
+ echo "[DEBUG] ======== SSH debug info ========"
+ echo "[DEBUG] SSH_PORT = ${SSH_PORT}"
+ echo "[DEBUG] SSHD_CONFIG = ${SSHD_CONFIG}"
+ echo "[DEBUG] SSHD_LOG = ${SSHD_LOG}"
+ echo "[DEBUG] authorized_keys = $(cat "${HOME}/.ssh/authorized_keys")"
+ echo "[DEBUG] authorized_keys perms = $(stat -c '%a %U:%G' "${HOME}/.ssh/authorized_keys")"
+ echo "[DEBUG] .ssh dir perms = $(stat -c '%a %U:%G' "${HOME}/.ssh")"
+ echo "[DEBUG] HOME = ${HOME}"
+ echo "[DEBUG] whoami = $(whoami)"
+ echo "[DEBUG] id = $(id)"
+ echo "[DEBUG] sshd_config contents:"
+ cat "${SSHD_CONFIG}"
+ echo "[DEBUG] Validating sshd config:"
+ /usr/sbin/sshd -t -f "${SSHD_CONFIG}" && echo "[DEBUG] sshd config OK" || echo "[DEBUG] sshd config INVALID"
+ echo "[DEBUG] ================================"
else
export DEBUG="false"
fi
umask 0007
/app-root/entrypoints/entrypoint.sh
+
diff --git a/ripple-plugin/src/app-root/frontend_plugin/entrypoints/supervisord-frontend_plugin.conf b/ripple-plugin/src/app-root/frontend_plugin/entrypoints/supervisord-frontend_plugin.conf
index 0f401fb..962a2a4 100644
--- a/ripple-plugin/src/app-root/frontend_plugin/entrypoints/supervisord-frontend_plugin.conf
+++ b/ripple-plugin/src/app-root/frontend_plugin/entrypoints/supervisord-frontend_plugin.conf
@@ -37,7 +37,7 @@ redirect_stderr=true
stopsignal=QUIT
[program:sshd]
-command=sh -c '/usr/sbin/sshd -D'
+command=sh -c '/usr/sbin/sshd -D -f %(ENV_SSHD_CONFIG)s'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
diff --git a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py
index 834f59f..7065ce9 100644
--- a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py
+++ b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py
@@ -110,17 +110,42 @@ def get(self):
@api.route("/status", methods=["GET"])
class Status(Resource):
+ # Define the Error model
+ error_model = api.model(
+ "Error",
+ {
+ "code": fields.String(description="Error code"),
+ "message": fields.String(description="Error message")
+ }
+ )
+
component_status_model = api.model(
- "ComponentStatus", {"status": fields.String(), "error": fields.String()}
+ "ComponentStatus",
+ {
+ "status_code": fields.Integer(description="HTTP status code"),
+ "status": fields.String(description="Human readable message"),
+ "errors": fields.List(fields.Nested(error_model), default=[], description="List of errors")
+ }
)
@api.response(code=200, description="Success", model=component_status_model)
@api.response(code=503, description="Unavailable", model=component_status_model)
def get(self):
+ """Return the BPM component status"""
try:
- current_app.bpm.backend_status()
+ # Capture backend status if needed
+ backend_result = current_app.bpm.backend_status()
except Exception as e:
- logger.exception(e)
- abort(503)
-
- return {"status": "OK"}, 200
+ logger.exception("BPM backend status check failed")
+ return {
+ "status_code": 503,
+ "status": "Unavailable",
+ "errors": [{"code": "BACKEND_ERROR", "message": str(e)}]
+ }, 503
+
+ # Return a successful status
+ return {
+ "status_code": 200,
+ "status": "OK",
+ "errors": []
+ }, 200
diff --git a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py
index 3b2eea2..efc70c8 100644
--- a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py
+++ b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py
@@ -14,7 +14,6 @@
# limitations under the License.
-import copy
import json
import logging
import os
@@ -35,7 +34,7 @@ class BackendPluginManager:
def __init__(self):
self.cold_bridge_endpoint = os.environ.get("COLD_BRIDGE_ENDPOINT",
"http://localhost:8080")
- self.seed = os.environ.get("SEED", "")
+ self.seed = os.environ.get("OSOENCRYPTIONPASS", "")
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
self.logger = logging.getLogger(__name__)
@@ -55,110 +54,99 @@ def bulk_download(self) -> List[Dict]:
self.logger.info("Bulk download finished successfully")
- empty_content = {
- "accounts": [],
- "transactions": [],
- "manifests": [],
- "vaults": [],
- }
-
- def write_document_set(documents, content_key: str, id_key: str):
- for item in response_json.get(content_key, []):
- self.logger.info(
- f"Saving document from {content_key} for bulk download"
- )
-
- try:
- document_id = item.get(id_key)
- self.logger.info(f"Saving document {document_id} for bulk download")
-
- content = copy.deepcopy(empty_content)
- content.setdefault(content_key, []).append(item)
-
- # Encrypt content
- if len(self.seed) > 0:
- data = crypt.encrypt(json.dumps(content), self.seed)
- else:
- data = json.dumps(content)
-
- documents.append(
- {"id": item.get(id_key), "content": data, "metadata": ""}
- )
-
- self.logger.info(
- f"Successfully saved document {document_id} for bulk download"
- )
- except Exception as err:
- self.logger.exception(err)
- continue
-
documents = []
- for content_key, id_key in [
- ("transactions", "transactionId"),
- ("accounts", "accountId"),
- ("manifests", "manifestId"),
- ]:
- write_document_set(documents, content_key, id_key)
+
+ sections = [
+ ("transactions", "transactionId", "transaction"),
+ ("accounts", "accountId", "account"),
+ ("manifests", "manifestId", "manifest"),
+ ]
+ for section, id_key, type_name in sections:
+ for item in response_json.get(section, []):
+ # Encrypt if seed is set
+ if self.seed and "signedPayload" in item:
+ item["signedPayloadCiphered"] = crypt.encrypt(item["signedPayload"], self.seed)
+ del item["signedPayload"]
+
+ # Build content and metadata
+ content = {
+ "accounts": [item] if section == "accounts" else [],
+ "transactions": [item] if section == "transactions" else [],
+ "manifests": [item] if section == "manifests" else [],
+ "vaults": [],
+ }
+ meta = {"source": item["vaultId"], "type": type_name}
+
+ documents.append({
+ "id": item[id_key],
+ "content": json.dumps(content),
+ "metadata": json.dumps(meta)
+ })
return documents
def bulk_upload(self, documents):
- vault_id = None
- transactions = []
- accounts = []
- manifests = []
+ v_tx= {}
+ v_ac= {}
+ v_ma= {}
self.logger.info("Saving documents for bulk upload")
for document in documents:
try:
- document_id = document["id"]
- self.logger.info(f"Saving document {document_id} for bulk upload")
-
- # Decrypt content
- if len(self.seed) > 0:
- contents = json.loads(crypt.decrypt(document["content"], self.seed))
- else:
- contents = json.loads(document["content"])
+ contents = json.loads(document["content"])
+ vaultid= contents.get("vaultId")
+
+ if vaultid not in v_tx:
+ v_tx[vaultid]=[]
+ v_ac[vaultid]=[]
+ v_ma[vaultid]=[]
+ # Map sections to their storage dict
+ section_map = {
+ "transactions": v_tx[vaultid],
+ "accounts": v_ac[vaultid],
+ "manifests": v_ma[vaultid],
+ }
+
+ for section, storage in section_map.items():
+ for item in contents.get(section, []):
+ if self.seed and "signedPayloadCiphered" in item:
+ item["signedPayload"] = crypt.decrypt(item["signedPayloadCiphered"], self.seed)
+ del item["signedPayloadCiphered"]
+ storage.append(item)
+
+ self.logger.info(f"Saving document {document['id']} for bulk upload")
- transactions.extend(contents.get("transactions", []))
- accounts.extend(contents.get("accounts", []))
- manifests.extend(contents.get("manifests", []))
-
- if vault_id is None:
- vault_id = contents.get("vaultId")
-
- self.logger.info(
- f"Successfully saved document {document_id} for bulk upload"
- )
except Exception as e:
self.logger.exception(e)
continue
- if not vault_id:
- return Exception("Could not get vault id")
-
- content = {
- "vaultId": vault_id,
- "accounts": accounts,
- "transactions": transactions,
- "manifests": manifests,
- }
-
self.logger.info("Performing bulk upload to backend")
-
- try:
- with tempfile.NamedTemporaryFile(mode="w", delete=False) as vault_file:
- json.dump(content, vault_file)
-
- files = {"files": (vault_id, open(vault_file.name, "rb"))}
- response = requests.post(
- url=f"{self.cold_bridge_endpoint}/v1/feed/upload",
- files=files,
- )
- response.raise_for_status()
- except Exception as e:
- raise e
- finally:
- os.remove(vault_file.name)
+ for vaultid in v_tx.keys():
+ content = {
+ "vaultId": vaultid,
+ "accounts": v_ac[vaultid],
+ "transactions": v_tx[vaultid],
+ "manifests": v_ma[vaultid],
+ }
+ vault_file_name = None
+ try:
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as vault_file:
+ vault_file_name = vault_file.name
+ json.dump(content, vault_file)
+
+ files = {"files": (vaultid, open(vault_file.name, "rb"))}
+ response = requests.post(
+ url=f"{self.cold_bridge_endpoint}/v1/feed/upload",
+ files=files,
+ )
+ response.raise_for_status()
+ self.logger.info(f"Successfully uploaded vault {vaultid}")
+ except requests.HTTPError as http_err:
+ self.logger.error(f"HTTP error uploading vault {vaultid}: {http_err}")
+ except Exception as err:
+ self.logger.error(f"Unexpected error uploading vault {vaultid}: {err}")
+ finally:
+ if vault_file_name:
+ os.remove(vault_file_name)
self.logger.info("Bulk upload finished successfully")
diff --git a/ripple-plugin/src/oso_ripple_plugins/common/pre_request.py b/ripple-plugin/src/oso_ripple_plugins/common/pre_request.py
index 9e95b6e..807f78e 100644
--- a/ripple-plugin/src/oso_ripple_plugins/common/pre_request.py
+++ b/ripple-plugin/src/oso_ripple_plugins/common/pre_request.py
@@ -72,7 +72,7 @@ def set_cert():
def bind_flask_before_request(sender: Flask, **extras) -> None:
logger.info(f"HTTP Method: {request.method} URL Path: {request.path}")
-
+
client_verify = request.headers.get("X-SSL-CLIENT-VERIFY", "FAILED")
if client_verify != "SUCCESS":
logger.info(f"Could not verify certificate, client verify: {client_verify}")
diff --git a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py
index b692d51..f2026ef 100644
--- a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py
+++ b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py
@@ -1,17 +1,12 @@
-# Copyright (c) 2025 IBM Corp.
-# All rights reserved.
#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
+# Licensed Materials - Property of IBM
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# (c) Copyright IBM Corp. 2024
+#
+# The source code for this program is not published or otherwise
+# divested of its trade secrets, irrespective of what has been
+# deposited with the U.S. Copyright Office
#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
import logging
import sys
@@ -93,17 +88,43 @@ def post(self):
@api.route("/status", methods=["GET"])
class Status(Resource):
+ # Define the error model
+ error_model = api.model(
+ "Error",
+ {
+ "code": fields.String(description="Error code"),
+ "message": fields.String(description="Error message")
+ }
+ )
+
+ # Define the component status model
component_status_model = api.model(
- "ComponentStatus", {"status": fields.String(), "error": fields.String()}
+ "ComponentStatus",
+ {
+ "status_code": fields.Integer(description="HTTP status code"),
+ "status": fields.String(description="Human readable message"),
+ "errors": fields.List(fields.Nested(error_model), default=[], description="List of errors")
+ }
)
@api.response(code=200, description="Success", model=component_status_model)
@api.response(code=503, description="Unavailable", model=component_status_model)
def get(self):
+ """Return the component status"""
try:
- current_app.fpm.backend_status()
+ # Capture backend status if needed
+ backend_result = current_app.fpm.backend_status()
except Exception as e:
- logger.exception(e)
- abort(503)
-
- return {"status": "OK"}, 200
+ logger.exception("Backend status check failed")
+ return {
+ "status_code": 503,
+ "status": "Unavailable",
+ "errors": [{"code": "BACKEND_ERROR", "message": str(e)}]
+ }, 503
+
+ # Return a successful status
+ return {
+ "status_code": 200,
+ "status": "OK",
+ "errors": []
+ }, 200
\ No newline at end of file
diff --git a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py
index 5bff2bf..d7a49f5 100644
--- a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py
+++ b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py
@@ -37,7 +37,7 @@
class FrontendPluginManager:
def __init__(self):
if "SK" not in os.environ:
- raise errors.ConfigError("SK not found")
+ raise errors.ConfigError("Harmonize OSO user server key not found")
private_key_b64 = os.environ["SK"]
private_key_decoded = base64.b64decode(private_key_b64)
self.private_key = load_pem_private_key(private_key_decoded, password=None)
@@ -52,13 +52,21 @@ def __init__(self):
raise errors.ConfigError("HMZ_AUTH_HOSTNAME not found")
self.hmz_auth_hostname = os.environ["HMZ_AUTH_HOSTNAME"]
+ if "HMZ_AUTH_PATH" not in os.environ:
+ raise errors.ConfigError("HMZ_AUTH_PATH not found")
+ self.hmz_auth_path = os.environ["HMZ_AUTH_PATH"]
+
+ if "HMZ_AUTH_CUSTOMERID" not in os.environ:
+ raise errors.ConfigError("HMZ_AUTH_CUSTOMERID not found")
+ self.hmz_auth_customerid = os.environ["HMZ_AUTH_CUSTOMERID"]
+
if "HMZ_API_HOSTNAME" not in os.environ:
raise errors.ConfigError("HMZ_API_HOSTNAME not found")
self.hmz_api_hostname = os.environ["HMZ_API_HOSTNAME"]
if "VAULTID" not in os.environ:
raise errors.ConfigError("VAULTID not found")
- self.vault_id = os.environ["VAULTID"]
+ self.vaultids = os.environ["VAULTID"].split()
self.seed = os.environ.get("SEED", "")
@@ -127,7 +135,7 @@ def _get_token(self) -> Tuple[str, float]:
challenge = str(uuid.uuid4())
signature = self._sign(challenge)
data = {
- "client_id": "customer_api",
+ "client_id": self.hmz_auth_customerid,
"grant_type": "password",
"challenge": challenge,
"public_key": self.public_key,
@@ -135,7 +143,7 @@ def _get_token(self) -> Tuple[str, float]:
}
response = requests.post(
- f"https://{self.hmz_auth_hostname}/token",
+ f"https://{self.hmz_auth_hostname}{self.hmz_auth_path}",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
verify=self.verify,
@@ -173,119 +181,141 @@ def get_token(self) -> str:
def bulk_download(self) -> list:
self.logger.info("Performing bulk download from frontend")
token = self.get_token()
- url = f"https://{self.hmz_api_hostname}/v1/vaults/{self.vault_id}/operations/prepared"
- response = requests.get(
- url=url,
- headers={"Authorization": "Bearer " + token},
- stream=True,
- verify=self.verify,
- )
- response.raise_for_status()
- vault_json = response.json()
- self.logger.info("Bulk download finished successfully")
-
- empty_content = {
- "vaultId": "",
- "accounts": [],
- "transactions": [],
- "manifests": [],
- }
+ documents = []
+ for vaultid in self.vaultids:
- def write_document_set(documents, content_key: str, id_key: str):
- for item in vault_json.get(content_key, []):
- self.logger.info(
- f"Saving document from {content_key} for bulk download"
- )
+ url = f"https://{self.hmz_api_hostname}/v1/vaults/{vaultid}/operations/prepared"
+ response = requests.get(
+ url=url,
+ headers={"Authorization": "Bearer " + token},
+ stream=True,
+ verify=self.verify,
+ )
+ response.raise_for_status()
+ vault_json = response.json()
+ self.logger.info(f"Bulk download finished successfully for vault {vaultid}")
+ empty_content = {
+ "vaultId": vaultid,
+ "accounts": [],
+ "transactions": [],
+ "manifests": [],
+ }
+
+ def write_document_set(documents, content_key: str, id_key: str):
+ for item in vault_json.get(content_key, []):
+ self.logger.info(
+ f"Saving document from {content_key} for bulk download"
+ )
+ try:
+ document_id = item.get(id_key)
+ self.logger.info(f"Saving document {document_id} for bulk download")
- try:
- document_id = item.get(id_key)
- self.logger.info(f"Saving document {document_id} for bulk download")
+ content = copy.deepcopy(empty_content)
+ content.setdefault(content_key, []).append(item)
- content = copy.deepcopy(empty_content)
- content["vaultId"] = vault_json["vaultId"]
- content.setdefault(content_key, []).append(item)
+ # Encrypt content
+ if self.seed:
+ for section in ["transactions", "manifests", "accounts"]:
+ for item in content.get(section, []):
+ if "signedPayload" in item:
+ item["signedPayloadCiphered"] = crypt.encrypt(item["signedPayload"], self.seed)
+ del item["signedPayload"]
- # Encrypt content
- if len(self.seed) > 0:
- data = crypt.encrypt(json.dumps(content), self.seed)
- else:
data = json.dumps(content)
-
- documents.append(
- {"id": document_id, "content": data, "metadata": ""}
- )
-
- self.logger.info(
- f"Successfully saved document {document_id} for bulk download"
- )
- except Exception as e:
- self.logger.exception(e)
- continue
-
- documents = []
- for content_key, id_key in [
- ("transactions", "transactionId"),
- ("accounts", "accountId"),
- ("manifests", "manifestId"),
- ]:
- write_document_set(documents, content_key, id_key)
+ meta = { "source" : vaultid, "type": content_key}
+ documents.append(
+ {"id": document_id, "content": data, "metadata": json.dumps(meta) }
+ )
+
+ self.logger.info(
+ f"Successfully saved document {document_id} for bulk download"
+ )
+ except Exception as e:
+ self.logger.exception(e)
+ continue
+
+
+ for content_key, id_key in [
+ ("transactions", "transactionId"),
+ ("accounts", "accountId"),
+ ("manifests", "manifestId"),
+ ]:
+ write_document_set(documents, content_key, id_key)
return documents
def bulk_upload(self, documents):
- vaults = []
- transactions = []
- accounts = []
- manifests = []
+ v_tx = {}
+ v_ac = {}
+ v_ma = {}
+ v_vaults = {}
self.logger.info("Saving documents for bulk upload")
for document in documents:
try:
+ contents = json.loads(document["content"])
+ vaultid = contents.get("vaultId")
+
+ if vaultid not in v_tx:
+ v_tx[vaultid] = []
+ v_ac[vaultid] = []
+ v_ma[vaultid] = []
+ v_vaults[vaultid] = []
+
document_id = document["id"]
self.logger.info(f"Saving document {document_id} for bulk upload")
+
# Decrypt content
- if len(self.seed) > 0:
- contents = json.loads(crypt.decrypt(document["content"], self.seed))
- else:
- contents = json.loads(document["content"])
-
- transactions.extend(contents.get("transactions", []))
- accounts.extend(contents.get("accounts", []))
- manifests.extend(contents.get("manifests", []))
- vaults.extend(contents.get("vaults", []))
-
- self.logger.info(
- f"Successfully saved document {document_id} for bulk upload"
- )
+ if self.seed:
+ for section in ("transactions", "accounts", "manifests"):
+ for item in contents.get(section, []):
+ if "signedPayloadCiphered" in item:
+ item["signedPayload"] = crypt.decrypt(item["signedPayloadCiphered"], self.seed)
+ del item["signedPayloadCiphered"]
+
+ v_tx[vaultid].extend(contents.get("transactions", []))
+ v_ac[vaultid].extend(contents.get("accounts", []))
+ v_ma[vaultid].extend(contents.get("manifests", []))
+ v_vaults[vaultid].extend(contents.get("vaults", []))
+
+ self.logger.info(f"Successfully saved document {document_id} for bulk upload")
except Exception as e:
self.logger.exception(e)
continue
- content = {
- "accounts": accounts,
- "transactions": transactions,
- "manifests": manifests,
- "vaults": vaults,
- }
-
self.logger.info("Performing bulk upload to frontend")
token = self.get_token()
- try:
- with tempfile.NamedTemporaryFile(mode="w", delete=False) as vault_file:
- json.dump(content, vault_file)
-
- files = {"files": open(vault_file.name, "rb")}
- response = requests.post(
- url=f"https://{self.hmz_api_hostname}/v1/vaults/operations/signed",
- headers={"Authorization": "Bearer " + token},
- files=files,
- verify=self.verify,
- )
- response.raise_for_status()
- except Exception as e:
- raise e
- finally:
- os.remove(vault_file.name)
+
+ # Upload each vault separately
+ for vaultid in v_tx.keys():
+ content = {
+ "vaultId": vaultid,
+ "accounts": v_ac[vaultid],
+ "transactions": v_tx[vaultid],
+ "manifests": v_ma[vaultid],
+ "vaults": v_vaults[vaultid],
+ }
+
+ try:
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as vault_file:
+ json.dump(content, vault_file)
+
+ files = {"files": open(vault_file.name, "rb")}
+ response = requests.post(
+ url=f"https://{self.hmz_api_hostname}/v1/vaults/operations/signed",
+ headers={"Authorization": "Bearer " + token},
+ files=files,
+ verify=self.verify,
+ )
+ response.raise_for_status()
+ self.logger.info(f"Successfully uploaded vault {vaultid}")
+ except requests.HTTPError as http_err:
+ self.logger.error(f"HTTP error uploading vault {vaultid}: {http_err} - {response.text}")
+ except Exception as err:
+ self.logger.error(f"Unexpected error uploading vault {vaultid}: {err}")
+ finally:
+ os.remove(vault_file.name)
+
self.logger.info("Bulk upload finished successfully")
def backend_status(self):