Skip to content

Commit 1f10b90

Browse files
Merge remote-tracking branch 'origin/w/8.8/improvement/CLDSRV-673-kmip-cluster-tst' into w/9.0/improvement/CLDSRV-673-kmip-cluster-tst
2 parents b033564 + 941710c commit 1f10b90

File tree

6 files changed

+352
-8
lines changed

6 files changed

+352
-8
lines changed

.github/docker/docker-compose.sse.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,7 @@ services:
3636
- ../../tests/functional/sse-kms-migration/config.json:/conf/config.json
3737
environment:
3838
- S3_CONFIG_FILE=/conf/config.json
39-
- S3VAULT=scality
40-
depends_on:
41-
- vault-sse-before-migration
4239
cloudserver-sse-migration:
4340
extends: cloudserver-sse-before-migration
4441
profiles: [sse-migration]
4542
command: sh -c "yarn start > /artifacts/s3.migration.log 2> /artifacts/s3-stderr.migration.log"
46-
environment:
47-
- S3KMS
48-
depends_on: !override
49-
- redis
50-
- vault-sse-migration

.github/workflows/tests.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,67 @@ jobs:
610610
source: /tmp/artifacts
611611
if: always()
612612

613+
kmip-cluster-ft-tests:
614+
runs-on: ubuntu-latest
615+
needs: build
616+
env:
617+
S3BACKEND: file
618+
S3VAULT: mem
619+
MPU_TESTING: true
620+
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
621+
PYKMIP_IMAGE: ghcr.io/${{ github.repository }}/pykmip:${{ github.sha }}
622+
JOB_NAME: ${{ github.job }}
623+
COMPOSE_FILE: docker-compose.yaml:docker-compose.sse.yaml
624+
S3KMS: kmip
625+
steps:
626+
- name: Checkout
627+
uses: actions/checkout@v4
628+
- uses: actions/setup-python@v5
629+
with:
630+
python-version: 3.9
631+
- name: Setup CI environment
632+
uses: ./.github/actions/setup-ci
633+
- name: Install tcpdump to analyze traffic on kmip cluster interfaces
634+
run: sudo apt-get update && sudo apt-get install -y tcpdump
635+
- name: Copy KMIP certs
636+
run: cp -r ./certs /tmp/ssl-kmip
637+
working-directory: .github/pykmip
638+
- name: Merge config.json and kmip-cluster config
639+
run: |
640+
jq -s '
641+
.[0] * .[1]
642+
' \
643+
configs/base.json \
644+
configs/kmip-cluster.json \
645+
> config.json
646+
working-directory: tests/functional/sse-kms-migration
647+
- name: Setup CI services
648+
run: docker compose up -d --quiet-pull redis pykmip cloudserver-sse-before-migration
649+
working-directory: .github/docker
650+
- name: Run file KMIP cluster tests
651+
shell: bash # for pipefail
652+
env:
653+
# Functional tests needs access to the running config to use the same
654+
# KMS kmip cluster config
655+
# yarn run does a cd into the test folder
656+
S3_CONFIG_FILE: config.json
657+
S3KMS: kmip
658+
S3_END_TO_END: true # to use the default credentials profile and not vault profile
659+
run: |-
660+
set -ex -o pipefail;
661+
bash wait_for_local_port.bash 8000 40
662+
bash wait_for_local_port.bash 5696 40
663+
yarn run ft_kmip_cluster | tee /tmp/artifacts/${{ github.job }}/tests.log
664+
- name: Upload logs to artifacts
665+
uses: scality/action-artifacts@v4
666+
with:
667+
method: upload
668+
url: https://artifacts.scality.net
669+
user: ${{ secrets.ARTIFACTS_USER }}
670+
password: ${{ secrets.ARTIFACTS_PASSWORD }}
671+
source: /tmp/artifacts
672+
if: always()
673+
613674
ceph-backend-test:
614675
runs-on: ubuntu-24.04
615676
needs: build

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"ft_test": "npm-run-all -s ft_awssdk ft_s3cmd ft_s3curl ft_node ft_healthchecks ft_management ft_util ft_backbeat",
9999
"ft_search": "cd tests/functional/aws-node-sdk && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 90000 test/mdSearch --exit",
100100
"ft_kmip": "cd tests/functional/kmip && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
101+
"ft_kmip_cluster": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 300000 load.js --exit",
101102
"ft_sse_cleanup": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 cleanup.js --exit",
102103
"ft_sse_before_migration": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 cleanup.js beforeMigration.js --exit",
103104
"ft_sse_migration": "cd tests/functional/sse-kms-migration && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 10000 migration.js --exit",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"kmip": {
3+
"providerName": "thales",
4+
"client": {
5+
"compoundCreateActivate": false
6+
},
7+
"transport": [
8+
{
9+
"pipelineDepth": 8,
10+
"tls": {
11+
"port": 5696,
12+
"host": "1.pykmip.local",
13+
"key": "/tmp/ssl-kmip/kmip-client-key.pem",
14+
"cert": "/tmp/ssl-kmip/kmip-client-cert.pem",
15+
"ca": "/tmp/ssl-kmip/kmip-ca.pem"
16+
}
17+
},
18+
{
19+
"pipelineDepth": 8,
20+
"tls": {
21+
"port": 5696,
22+
"host": "127.0.0.2",
23+
"key": "/tmp/ssl-kmip/kmip-client-key.pem",
24+
"cert": "/tmp/ssl-kmip/kmip-client-cert.pem",
25+
"ca": "/tmp/ssl-kmip/kmip-ca.pem"
26+
}
27+
},
28+
{
29+
"pipelineDepth": 8,
30+
"tls": {
31+
"port": 5696,
32+
"host": "3.pykmip.local",
33+
"key": "/tmp/ssl-kmip/kmip-client-key.pem",
34+
"cert": "/tmp/ssl-kmip/kmip-client-cert.pem",
35+
"ca": "/tmp/ssl-kmip/kmip-ca.pem"
36+
}
37+
},
38+
{
39+
"pipelineDepth": 8,
40+
"tls": {
41+
"port": 5696,
42+
"host": "127.0.0.4",
43+
"key": "/tmp/ssl-kmip/kmip-client-key.pem",
44+
"cert": "/tmp/ssl-kmip/kmip-client-cert.pem",
45+
"ca": "/tmp/ssl-kmip/kmip-ca.pem"
46+
}
47+
}
48+
]
49+
}
50+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
3+
set -e -o pipefail
4+
5+
# Needs sudo to run tcpdump
6+
7+
# Usage: sudo ./countPacketsByIp.sh 5696 1000
8+
9+
PORT=${1:-5696}
10+
PACKETS_COUNT=${2:-1000}
11+
12+
# tcpdump options:
13+
# -i lo: listen on the loopback interface (localhost)
14+
# -n: don't resolve hostnames
15+
# -t: don't print a timestamp
16+
# -q: quiet mode, less verbose output
17+
# -c $PACKETS_COUNT: capture $PACKETS_COUNT packets
18+
# 'tcp dst port $PORT': filter for TCP packets destined for port $PORT
19+
20+
# Output of tcpdump will look like this:
21+
# IP 127.0.0.1.33428 > 127.0.0.3.5696: tcp 341
22+
23+
# Print only the destination part
24+
# Remove the port number at the end
25+
# Sort and count unique occurrences and trim spaces
26+
27+
# Output looks like this:
28+
# 332 127.0.0.1
29+
# 323 127.0.0.2
30+
# 345 127.0.0.3
31+
32+
tcpdump -i lo -n -t -q -c $PACKETS_COUNT "tcp dst port ${PORT}" | \
33+
awk '{print $4}' | \
34+
sed 's/\.[^.]*$//' | \
35+
sort | \
36+
uniq -c | \
37+
sed 's/^\s*//'
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/* eslint-disable no-console */
2+
/* eslint-disable no-unused-expressions */
3+
const { DummyRequestLogger } = require('../../unit/helpers');
4+
const assert = require('assert');
5+
const log = new DummyRequestLogger();
6+
const helpers = require('./helpers');
7+
const { spawn } = require('child_process');
8+
const path = require('path');
9+
const kms = require('../../../lib/kms/wrapper');
10+
const { promisify } = require('util');
11+
12+
const BUCKET_NUMBER = 10;
13+
const OBJECT_NUMBER = 200;
14+
const TOTAL_OBJECTS = BUCKET_NUMBER * OBJECT_NUMBER;
15+
16+
const KMS_NODES = helpers.config.kmip.transport.length;
17+
const TOTAL_OBJECTS_PER_NODE = Math.floor(TOTAL_OBJECTS / KMS_NODES);
18+
19+
/**
20+
* 10% approximation for the number of packets per IP
21+
* As we might not have an exact match of packets and the
22+
* round robin is confined to each nodejs cluster processes
23+
*/
24+
const APPROX = Math.floor(0.1 * TOTAL_OBJECTS_PER_NODE);
25+
const EXPECTED_MIN = TOTAL_OBJECTS_PER_NODE - APPROX;
26+
const EXPECTED_MAX = TOTAL_OBJECTS_PER_NODE + APPROX;
27+
28+
async function spawnTcpdump(port, packetCount) {
29+
const scriptPath = path.join(__dirname, 'countPacketsByIp.sh');
30+
return new Promise((resolve, reject) => {
31+
const child = spawn(
32+
'sudo', // run as root to allow tcpdump execution
33+
// 5m timeout as process is detached
34+
['timeout', 300, scriptPath, port, packetCount],
35+
{
36+
// detach to not mess with the tty, it would cause issues with
37+
// \r even when using another shell and piping stdout.
38+
detached: true,
39+
stdio: ['ignore', 'pipe', 'pipe'], // ignored stdin
40+
shell: false, // no need as it's detached
41+
42+
},
43+
);
44+
let stderr = '';
45+
child.stderr.on('data', data => {
46+
stderr += data.toString();
47+
});
48+
49+
/** Let a small 10ms timeout for a potential error */
50+
let spawnTimeout;
51+
child.on('spawn', () => {
52+
spawnTimeout = setTimeout(() => {
53+
if (child.exitCode !== null || child.signalCode !== null) {
54+
const err = `countPacketsByIp.sh stopped after spawn with code ${
55+
child.exitCode} and signal ${child.signalCode}.\nStderr: ${stderr}`;
56+
reject(new Error(err));
57+
} else {
58+
resolve(child);
59+
}
60+
}, 10);
61+
});
62+
child.on('error', err => {
63+
if (spawnTimeout) {
64+
clearTimeout(spawnTimeout);
65+
}
66+
reject(new Error(`${err.toString()}\nStderr: ${stderr}`));
67+
});
68+
69+
child.on('close', (code, signal) => {
70+
if (code) {
71+
if (spawnTimeout) {
72+
clearTimeout(spawnTimeout);
73+
}
74+
reject(new Error(
75+
`tcpdump script closed with code ${code} and signal ${signal}.\nStderr: ${stderr}`
76+
));
77+
}
78+
});
79+
});
80+
}
81+
82+
async function stopTcpdump(tcpdump) {
83+
if (tcpdump.exitCode !== null || tcpdump.signalCode !== null) {
84+
// tcpdump already closed, no need to kill it
85+
return;
86+
}
87+
await new Promise(resolve => {
88+
tcpdump.on('close', resolve);
89+
tcpdump.kill('SIGTERM');
90+
});
91+
}
92+
93+
describe(`KMS load (kmip cluster ${KMS_NODES} nodes): ${OBJECT_NUMBER
94+
} objs each in ${BUCKET_NUMBER} bkts (${TOTAL_OBJECTS} objs)`, () => {
95+
let buckets = [];
96+
let tcpdumpProcess;
97+
let stdout;
98+
let stderr;
99+
let closePromise;
100+
101+
before(async () => {
102+
buckets = await Promise.all(
103+
new Array(BUCKET_NUMBER).fill(0).map(async (_, i) => {
104+
const Bucket = `kms-load-${i}`;
105+
const { masterKeyArn } = await helpers.createKmsKey(log);
106+
107+
await helpers.s3.createBucket({ Bucket }).promise();
108+
await helpers.s3.putBucketEncryption({
109+
Bucket,
110+
ServerSideEncryptionConfiguration: helpers.hydrateSSEConfig({
111+
algo: 'aws:kms', masterKeyId: masterKeyArn }),
112+
}).promise();
113+
114+
return { Bucket, masterKeyArn };
115+
}));
116+
});
117+
118+
after(async () => {
119+
await Promise.all(buckets.map(async ({ Bucket, masterKeyArn }) => {
120+
await helpers.cleanup(Bucket);
121+
return helpers.destroyKmsKey(masterKeyArn, log);
122+
}));
123+
await promisify(kms.client.stop.bind(kms.client))();
124+
});
125+
126+
beforeEach(async () => {
127+
// tcpdump can catch more than TOTAL_OBJECTS packets because there are PSH and ACK packets
128+
// but we need to ensure it actually stops before there is no more packets
129+
// to count packets by IP
130+
tcpdumpProcess = await spawnTcpdump(5696, TOTAL_OBJECTS);
131+
stdout = '';
132+
stderr = '';
133+
tcpdumpProcess.stderr.on('data', data => {
134+
stderr += data.toString();
135+
});
136+
tcpdumpProcess.stdout.on('data', data => {
137+
stdout += data.toString();
138+
});
139+
closePromise = new Promise(resolve => {
140+
tcpdumpProcess.on('close', (code, signal) =>
141+
resolve({
142+
code,
143+
signal,
144+
repartition: stdout
145+
.split('\n')
146+
.filter(l => l)
147+
.map(line => {
148+
const [count, ip] = line.trim().split(' ');
149+
return { count: +count, ip };
150+
}),
151+
})
152+
);
153+
});
154+
});
155+
156+
afterEach(async () => {
157+
if (tcpdumpProcess) {
158+
await stopTcpdump(tcpdumpProcess);
159+
}
160+
});
161+
162+
async function assertRepartition(closePromise) {
163+
const { code, signal, repartition } = await closePromise;
164+
console.log('Test Details', {
165+
KMS_NODES,
166+
TOTAL_OBJECTS,
167+
TOTAL_OBJECTS_PER_NODE,
168+
APPROX,
169+
EXPECTED_MIN,
170+
EXPECTED_MAX,
171+
code,
172+
signal,
173+
stderr,
174+
repartition,
175+
});
176+
const repartitionCount = repartition.map(({ count }) => count);
177+
assert.strictEqual(code, 0, `tcpdump script closed with code ${code} and signal ${signal}`);
178+
assert(repartition.length === KMS_NODES, `Expected ${KMS_NODES} IPs but got ${repartition.length}`);
179+
assert(repartitionCount.every(count =>
180+
count >= EXPECTED_MIN && count <= EXPECTED_MAX),
181+
`Repartition counts should be around ${TOTAL_OBJECTS_PER_NODE} but got ${repartitionCount}`);
182+
}
183+
184+
it(`should encrypt ${TOTAL_OBJECTS} times in parallel, ~${TOTAL_OBJECTS_PER_NODE} per node`, async () => {
185+
await (Promise.all(
186+
buckets.map(async ({ Bucket }) => Promise.all(
187+
new Array(OBJECT_NUMBER).fill(0).map(async (_, i) =>
188+
helpers.s3.putObject({ Bucket, Key: `obj-${i}`, Body: `body-${i}` }).promise())
189+
))
190+
));
191+
await assertRepartition(closePromise);
192+
});
193+
194+
it(`should decrypt ${TOTAL_OBJECTS} times in parallel, ~${TOTAL_OBJECTS_PER_NODE} per node`, async () => {
195+
await Promise.all(
196+
buckets.map(async ({ Bucket }) => Promise.all(
197+
new Array(OBJECT_NUMBER).fill(0).map(async (_, i) =>
198+
helpers.s3.getObject({ Bucket, Key: `obj-${i}` }).promise())
199+
))
200+
);
201+
await assertRepartition(closePromise);
202+
});
203+
});

0 commit comments

Comments
 (0)