Setting the file. One moment. Db2client Configure · RDS Db2 · aws/agent-toolkit-for-aws · Skills Docs70
Creating Amazon Aurora Db Cluster With Instances
93
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
— line 264
This file
- Number
- 72.20
- Position
- 20 of 28
- Type
- Shell
- Size
- 38 KB
- Lines
- 949
scripts/db2client-configure.sh
Shell·949 lines·38 KB
15
16if [ -z "$BASH_VERSION" ]; then exec bash "$0" "$@"; fi
17
18RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
19BLUE='\033[0;34m'; NC='\033[0m'
20log_info() { echo -e "${BLUE}[ INFO]${NC} $(date '+%H:%M:%S') - $1" >&2; }
21log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%H:%M:%S') - $1" >&2; }
22log_warning() { echo -e "${YELLOW}[WARNING]${NC} $(date '+%H:%M:%S') - $1" >&2; }
23log_error() { echo -e "${RED}[ ERROR]${NC} $(date '+%H:%M:%S') - $1" >&2; }
24
25# RDS cert bundle URL — partition-aware (commercial / GovCloud / China)
26rds_truststore_url() {
27 local region="$1"
28 case "$region" in
29 us-gov-*) echo "https://truststore.pki.${region}.rds.amazonaws.com/${region}/${region}-bundle.pem" ;;
30 cn-*) echo "https://truststore.pki.${region}.rds.amazonaws.com.cn/${region}/${region}-bundle.pem" ;;
31 *) echo "https://truststore.pki.rds.amazonaws.com/${region}/${region}-bundle.pem" ;;
32 esac
33}
34
35# =============================================================================
36# Kerberos / domain-join detection
37# =============================================================================
38# Sets IS_DOMAIN_JOINED=true and KRB_REALM=<realm> when the host is confirmed
39# to be a member of an Active Directory / Kerberos realm.
40#
41# Detection order (first match wins):
42# 1. 'realm list' shows "configured: kerberos-member" (realmd + sssd — most common)
43# 2. /etc/krb5.conf contains a default_realm (any kerberos setup)
44#
45# When domain-joined, also validates that a TGT exists in the Kerberos cache.
46# RDS for Db2 does not support local user authentication when Kerberos is
47# enabled — a valid TGT is required for ALL connections (including the
48# internal bootstrap query). The script exits if no ticket is found.
49#
50IS_DOMAIN_JOINED=false
51KRB_REALM=""
52
53detect_domain_join() {
54 # Method 1: realm list (realmd)
55 if command -v realm &>/dev/null; then
56 local realm_out
57 realm_out=$(realm list 2>/dev/null)
58 if echo "$realm_out" | grep -q "configured: kerberos-member"; then
59 KRB_REALM=$(echo "$realm_out" | awk '/^[^ ]/ {realm=$1} /configured: kerberos-member/ {print realm; exit}')
60 log_info "Domain join detected via 'realm list' — realm: $KRB_REALM"
61 # Only treat the host as domain-joined if a valid TGT is present. Otherwise
62 # the Kerberos DSNs would be written but fail at connect time.
63 if _require_tgt; then
64 IS_DOMAIN_JOINED=true
65 else
66 log_warning "Domain join detected but no valid TGT — Kerberos DSNs will NOT be created"
67 IS_DOMAIN_JOINED=false
68 fi
69 return
70 fi
71 fi
72
73 # Method 2: /etc/krb5.conf default_realm
74 if [ -f /etc/krb5.conf ]; then
75 local realm_line
76 realm_line=$(grep -i '^\s*default_realm\s*=' /etc/krb5.conf 2>/dev/null | head -1)
77 if [ -n "$realm_line" ]; then
78 KRB_REALM=$(echo "$realm_line" | awk -F'=' '{gsub(/[[:space:]]/,"",$2); print $2}')
79 log_info "Domain join detected via /etc/krb5.conf — realm: $KRB_REALM"
80 # Only treat the host as domain-joined if a valid TGT is present. Otherwise
81 # the Kerberos DSNs would be written but fail at connect time.
82 if _require_tgt; then
83 IS_DOMAIN_JOINED=true
84 else
85 log_warning "Domain join detected but no valid TGT — Kerberos DSNs will NOT be created"
86 IS_DOMAIN_JOINED=false
87 fi
88 return
89 fi
90 fi
91
92 log_info "No domain join detected — Kerberos DSN parameters will not be added"
93}
94
95# Gate: verify a valid TGT exists. Called only when IS_DOMAIN_JOINED=true.
96# Both local auth and Kerberos SSL DSNs will be written on domain-joined hosts.
97# A valid TGT is required for the Kerberos DSNs and for the bootstrap connect
98# when db2comm=SSL (since local auth over SSL also needs a working SSL path
99# that the Kerberos ticket provides for discovery).
100_require_tgt() {
101 if ! command -v klist &>/dev/null; then
102 log_error "klist not found — cannot verify Kerberos ticket."
103 log_error "Install krb5-workstation (AL2/AL2023) and obtain a ticket:"
104 log_error " sudo dnf install -y krb5-workstation"
105 log_error " kinit $(whoami)@${KRB_REALM}"
106 return 1
107 fi
108
109 if ! klist -s 2>/dev/null; then
110 log_error "============================================================="
111 log_error "This host is domain-joined (realm: $KRB_REALM)."
112 log_error "RDS for Db2 does not support local user authentication"
113 log_error "when Kerberos is enabled — a valid TGT is required."
114 log_error ""
115 log_error "No Kerberos ticket found in the cache. Obtain one first:"
116 log_error " kinit $(whoami)@${KRB_REALM}"
117 log_error " klist # confirm ticket is present"
118 log_error " REGION=$REGION source db2client-configure.sh"
119 log_error "============================================================="
120 return 1
121 fi
122
123 # Ticket exists — show the principal so the user can confirm it's the right one
124 local principal
125 principal=$(klist 2>/dev/null | awk '/^Default principal:/ {print $3}')
126 log_success "Kerberos TGT found — principal: ${principal:-<unknown>}"
127}
128
129# --- Defaults ---
130PROFILE=${PROFILE:-"default"}
131DB2USER_NAME=${DB2USER_NAME:-"db2inst1"}
132DB_NAMES_INPUT=${DB_NAMES:-""} # optional: comma-separated list, e.g. DB_NAMES=DB2DB,MYDB
133E_URL=${E_URL:-""} # optional: custom RDS endpoint, e.g.
134 # E_URL="--endpoint-url https://rds-siteb.us-east-1.amazonaws.com --no-verify-ssl"
135SSL_CERT_FILE="" # set by download_pem_file() — do not set manually
136declare -a HELP_COMMANDS=()
137declare -a DB_INSTANCES=()
138declare -a MASTER_USER_NAMES=()
139declare -a MASTER_USER_PASSWORDS=()
140declare -a DB_NAMES=()
141
142# Wrapper for all 'aws rds' calls — injects E_URL when set.
143# Usage: aws_rds describe-db-instances --region ... --query ... --output text
144aws_rds() {
145 # shellcheck disable=SC2086
146 aws rds "$@" ${E_URL}
147}
148
149# =============================================================================
150# Validation
151# =============================================================================
152validate() {
153 if [ -z "$REGION" ]; then
154 log_error "REGION is required. Example: BUCKET=... REGION=us-east-1 source db2client-configure.sh"
155 return 1
156 fi
157 # BUCKET is optional — only needed for airgap SSL cert download
158 if [ "$(whoami)" != "$DB2USER_NAME" ]; then
159 log_error "This script must be run as $DB2USER_NAME. Run: sudo su - $DB2USER_NAME"
160 return 1
161 fi
162 if [ ! -d "$HOME/sqllib" ]; then
163 log_error "RT client not installed — $HOME/sqllib not found. Run db2-driver.sh as root first."
164 return 1
165 fi
166}
167
168# =============================================================================
169# Credentials
170# =============================================================================
171set_credentials() {
172 # CloudShell
173 if curl -s --connect-timeout 1 http://127.0.0.1:1338/latest/meta-data/ >/dev/null 2>&1; then
174 log_info "Detected AWS CloudShell environment"
175 local token creds
176 token=$(curl -sX PUT "http://127.0.0.1:1338/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
177 creds=$(curl -s -H "Authorization: $token" "http://127.0.0.1:1338/latest/meta-data/container/security-credentials")
178 export AWS_ACCESS_KEY_ID=$(echo "$creds" | jq -r .AccessKeyId)
179 export AWS_SECRET_ACCESS_KEY=$(echo "$creds" | jq -r .SecretAccessKey)
180 export AWS_SESSION_TOKEN=$(echo "$creds" | jq -r .Token)
181 log_success "AWS credentials set from CloudShell"
182 return
183 fi
184 # EC2 IMDSv2
185 if curl -s --connect-timeout 1 http://169.254.169.254/latest/meta-data/ >/dev/null 2>&1; then
186 log_info "Detected EC2 environment"
187 local token role creds
188 token=$(curl -sX PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
189 role=$(curl -s -H "X-aws-ec2-metadata-token: $token" http://169.254.169.254/latest/meta-data/iam/security-credentials/)
190 creds=$(curl -s -H "X-aws-ec2-metadata-token: $token" "http://169.254.169.254/latest/meta-data/iam/security-credentials/$role")
191 export AWS_ACCESS_KEY_ID=$(echo "$creds" | jq -r .AccessKeyId)
192 export AWS_SECRET_ACCESS_KEY=$(echo "$creds" | jq -r .SecretAccessKey)
193 export AWS_SESSION_TOKEN=$(echo "$creds" | jq -r .Token)
194 log_success "AWS credentials set from EC2 instance role"
195 return
196 fi
197 # Fall back to configured profile
198 if [ -n "${AWS_ACCESS_KEY_ID:-}" ] && [ -n "${AWS_SECRET_ACCESS_KEY:-}" ]; then
199 # SECURITY: AWS_ACCESS_KEY_ID/SECRET are long-lived static keys — acceptable
200 # only for temporary CI/CD automation, NEVER for production. Production
201 # workflows MUST obtain credentials through a CloudShell/EC2 IAM role (handled
202 # above) or a configured profile, never hard-coded or long-lived keys.
203 log_info "Using AWS credentials from environment variables"
204 else
205 log_info "Using AWS CLI profile: $PROFILE"
206 export AWS_PROFILE="$PROFILE"
207 fi
208}
209
210# =============================================================================
211# Instance discovery
212# =============================================================================
213list_db_instances() {
214 local query='DBInstances[?starts_with(Engine, `db2`)].DBInstanceIdentifier'
215 local aws_output
216 aws_output=$(aws_rds describe-db-instances \
217 --region "$REGION" \
218 --query "$query" \
219 --output text 2>/dev/null)
220
221 local existing_instances=($aws_output)
222 if [ ${#existing_instances[@]} -eq 0 ]; then
223 log_error "No DB2 instances found in region $REGION"
224 return 1
225 fi
226
227 if [ -n "${DB_INSTANCE_ID:-}" ]; then
228 if [ "$DB_INSTANCE_ID" = "ALL" ]; then
229 DB_INSTANCES=("${existing_instances[@]}")
230 log_info "Processing ALL DB2 instances: ${DB_INSTANCES[*]}"
231 return 0
232 fi
233 DB_INSTANCES=("$DB_INSTANCE_ID")
234 log_info "Using specified instance: $DB_INSTANCE_ID"
235 return 0
236 fi
237
238 if [ ${#existing_instances[@]} -eq 1 ]; then
239 DB_INSTANCES=("${existing_instances[0]}")
240 log_info "Auto-selected only available instance: ${existing_instances[0]}"
241 return 0
242 fi
243
244 # Interactive selection — one instance only
245 local choice=-1
246 while [ "$choice" -lt 1 ] || [ "$choice" -gt ${#existing_instances[@]} ]; do
247 echo "Available DB2 instances:" >&2
248 for i in "${!existing_instances[@]}"; do
249 echo "$((i+1)). ${existing_instances[$i]}" >&2
250 done
251 read -p "Select instance (1-${#existing_instances[@]}): " choice
252 if [ "$choice" -ge 1 ] && [ "$choice" -le ${#existing_instances[@]} ]; then
253 DB_INSTANCES=("${existing_instances[$((choice-1))]}")
254 else
255 log_warning "Invalid choice"
256 choice=-1
257 fi
258 done
259}
260
261# =============================================================================
262# Master user names and passwords
263# =============================================================================
264get_all_master_user_names() {
265 MASTER_USER_NAMES=()
266 for db_instance in "${DB_INSTANCES[@]}"; do
267 local name
268 name=$(aws_rds describe-db-instances \
269 --db-instance-identifier "$db_instance" \
270 --region "$REGION" \
271 --query "DBInstances[0].MasterUsername" \
272 --output text 2>/dev/null)
273 [ "$name" = "None" ] && name=""
274 MASTER_USER_NAMES+=("$name")
275 log_info "Master user for $db_instance: ${name:-<not found>}"
276 done
277}
278
279get_all_master_passwords() {
280 MASTER_USER_PASSWORDS=()
281 local password_file="$HOME/.need_password"
282
283 for db_instance in "${DB_INSTANCES[@]}"; do
284 local secret_arn
285 secret_arn=$(aws_rds describe-db-instances \
286 --db-instance-identifier "$db_instance" \
287 --region "$REGION" \
288 --query "DBInstances[0].MasterUserSecret.SecretArn" \
289 --output text 2>/dev/null)
290
291 if [ -n "$secret_arn" ] && [ "$secret_arn" != "None" ]; then
292 local secret_json password
293 secret_json=$(aws secretsmanager get-secret-value \
294 --secret-id "$secret_arn" \
295 --region "$REGION" \
296 --query "SecretString" \
297 --output text 2>/dev/null)
298 password=$(jq -r '.password' <<< "$secret_json")
299 if [ -n "$password" ]; then
300 log_info "Retrieved password from Secrets Manager for $db_instance"
301 MASTER_USER_PASSWORDS+=("$password")
302 continue
303 fi
304 fi
305
306 # Fall back to .need_password file. This is a DEVELOPMENT/TEST-ONLY path for
307 # instances not using Secrets Manager. The file MUST be created with
308 # `chmod 600 ~/.need_password` (owner read/write only) and MUST NEVER be
309 # committed to version control or shared. For production, provision with
310 # --manage-master-user-password so RDS stores and rotates the master
311 # credential in Secrets Manager instead of keeping plaintext on disk.
312 local file_password=""
313 if [ -f "$password_file" ]; then
314 file_password=$(grep "^$db_instance " "$password_file" 2>/dev/null | cut -d' ' -f2-)
315 fi
316
317 if [ -n "$file_password" ] && [ "$file_password" != "replace this with the master user password" ]; then
318 log_warning "Using password from $password_file for $db_instance (dev/test only — use --manage-master-user-password in production)"
319 MASTER_USER_PASSWORDS+=("$file_password")
320 else
321 log_warning "No password found for $db_instance — prompting"
322 read -rsp "Password for $db_instance: " entered_password; echo
323 MASTER_USER_PASSWORDS+=("${entered_password:-}")
324 fi
325 done
326}
327
328# =============================================================================
329# Database name discovery
330# =============================================================================
331#
332# Resolution order:
333# 1. DB_NAMES env var — comma-separated list, e.g. DB_NAMES=DB2DB,MYDB
334# (useful for automation or when RDSADMIN is inaccessible)
335# 2. DBName field on the RDS instance (single-database, most common case)
336# 3. Bootstrap connect to RDSADMIN + rdsadmin.list_databases()
337# — requires the connecting user to have CONNECT on RDSADMIN
338# — when domain-joined, this uses the Kerberos TGT (no master user/password)
339# — when Kerberos is active but the AD user lacks RDSADMIN access, this
340# step fails and the script falls through to the interactive prompt
341# 4. Interactive prompt — user enters names manually; skipped when stdin
342# is not a terminal (non-interactive mode)
343#
344get_all_database_names() {
345 local db_instance_id="$1" master_user="$2" master_password="$3" temp_dsn="${4:-RDSADMIN}"
346 DB_NAMES=()
347
348 # --- Resolution 1: DB_NAMES env var ---
349 if [ -n "${DB_NAMES_INPUT:-}" ]; then
350 IFS=',' read -ra DB_NAMES <<< "$DB_NAMES_INPUT"
351 # Trim whitespace from each entry
352 DB_NAMES=("${DB_NAMES[@]// /}")
353 log_info "Using database list from DB_NAMES env var: ${DB_NAMES[*]}"
354 return 0
355 fi
356
357 # --- Resolution 2: DBName field on the RDS instance ---
358 local default_dbname
359 default_dbname=$(aws_rds describe-db-instances \
360 --db-instance-identifier "$db_instance_id" \
361 --region "$REGION" \
362 --query "DBInstances[0].DBName" \
363 --output text 2>/dev/null)
364 [ "$default_dbname" = "None" ] && default_dbname=""
365
366 if [ -n "$default_dbname" ]; then
367 log_info "Default database from RDS metadata: $default_dbname"
368 DB_NAMES=("$default_dbname")
369 return 0
370 fi
371
372 # --- Resolution 3: Bootstrap connect to RDSADMIN ---
373 log_info "No default database set — attempting RDSADMIN bootstrap query"
374 local connect_out connect_rc
375 if [ "${IS_DOMAIN_JOINED:-false}" = "true" ]; then
376 connect_out=$(db2 "connect to $temp_dsn" 2>&1)
377 connect_rc=$?
378 else
379 connect_out=$(db2 "connect to $temp_dsn user $master_user using '$master_password'" 2>&1)
380 connect_rc=$?
381 fi
382
383 if [ $connect_rc -eq 0 ]; then
384 local db_names_raw
385 mapfile -t db_names_raw < <(
386 db2 -x "SELECT database_name FROM TABLE(rdsadmin.list_databases()) WHERE UPPER(database_name) <> 'RDSADMIN'" 2>/dev/null
387 )
388 db2 connect reset >/dev/null 2>&1 || true
389
390 local db_names_clean=()
391 for dbname in "${db_names_raw[@]}"; do
392 dbname="$(echo "$dbname" | xargs)"
393 [[ -n "$dbname" && ! "$dbname" =~ ^SQL ]] && db_names_clean+=("$dbname")
394 done
395
396 if [ ${#db_names_clean[@]} -gt 0 ]; then
397 DB_NAMES=("${db_names_clean[@]}")
398 log_info "Found ${#DB_NAMES[@]} database(s) via RDSADMIN: ${DB_NAMES[*]}"
399 return 0
400 fi
401 log_warning "RDSADMIN connect succeeded but no user databases found"
402 else
403 db2 connect reset >/dev/null 2>&1 || true
404 log_warning "RDSADMIN bootstrap connect failed (rc=$connect_rc)"
405 if [ "${IS_DOMAIN_JOINED:-false}" = "true" ]; then
406 local principal
407 principal=$(klist 2>/dev/null | awk '/^Default principal:/ {print $3}')
408 log_warning "Kerberos principal '${principal}' may not have CONNECT privilege on RDSADMIN."
409 log_warning "This is expected — RDSADMIN is protected and AD users are not granted access by default."
410 fi
411 fi
412
413 # --- Resolution 4: Interactive prompt ---
414 log_info "------------------------------------------------------------"
415 log_info "Cannot discover databases automatically for $db_instance_id."
416 log_info "To skip this prompt next time, set before running:"
417 log_info " DB_NAMES=DB2DB,MYDB REGION=$REGION source db2client-configure.sh"
418 log_info "------------------------------------------------------------"
419
420 if [ -t 0 ]; then
421 local input
422 read -rp "Enter database name(s) for $db_instance_id (comma-separated, or Enter to skip): " input
423 if [ -n "$input" ]; then
424 IFS=',' read -ra DB_NAMES <<< "$input"
425 DB_NAMES=("${DB_NAMES[@]// /}")
426 log_info "Registering databases: ${DB_NAMES[*]}"
427 return 0
428 fi
429 log_warning "No databases entered — only the RDSDBSSL admin DSN will be created for $db_instance_id"
430 else
431 log_warning "Non-interactive mode and no DB_NAMES set — only the admin DSN will be created"
432 log_warning "Re-run with: DB_NAMES=<name1,name2> REGION=$REGION source db2client-configure.sh"
433 fi
434
435 return 0 # not fatal — admin DSN is still useful
436}
437
438# =============================================================================
439# DSN helpers
440# =============================================================================
441#
442# Naming convention (all aliases must be ≤ 8 characters):
443#
444# Admin database (RDSADMIN):
445# RDSAT — TCP, local auth (SERVER_ENCRYPT)
446# RDSAS — SSL, local auth
447# RDSAKS — SSL, Kerberos
448#
449# User databases (<DB>, truncated to fit):
450# <DB>T — TCP, local auth
451# <DB>S — SSL, local auth
452# <DB>SK — SSL, Kerberos
453#
454# Multi-instance: numeric index appended before the type suffix,
455# e.g. RDSAT0 / RDSAT1, DB2DB0T / DB2DB0S / DB2DB0SK
456#
457# generate_db_alias NAME SUFFIX [INSTANCE_SUFFIX]
458# Builds a user-DB alias that fits in 8 chars including BOTH the type suffix
459# and the optional multi-instance index, e.g. generate_db_alias DB2DB SK 0 -> DB2DB0SK.
460# The instance index is placed before the type suffix (matching the documented
461# DB2DB0SK convention) and is counted against the 8-char budget so callers must
462# NOT append ${SUFFIX} themselves.
463# SUFFIX = T | S | SK (1-2 chars); INSTANCE_SUFFIX = "" | 0 | 1 | ...
464generate_db_alias() {
465 local raw="${1^^}" suffix="${2}" instance_suffix="${3:-}"
466 local maxbase=$(( 8 - ${#suffix} - ${#instance_suffix} ))
467 (( maxbase < 0 )) && maxbase=0
468 local base="${raw:0:$maxbase}"
469 echo "${base}${instance_suffix}${suffix}"
470}
471
472writecfg_tcp() {
473 local dsn=$1 dbname=$2 host=$3 port=$4
474 db2cli writecfg add -dsn "$dsn" -database "$dbname" -host "$host" -port "$port" \
475 -parameter "Authentication=SERVER_ENCRYPT"
476}
477
478# SSL + local auth (SERVER_ENCRYPT)
479writecfg_ssl_local() {
480 local dsn=$1 dbname=$2 host=$3 port=$4
481 local cert_file="${SSL_CERT_FILE:-$HOME/$REGION-bundle.pem}"
482 db2cli writecfg add -dsn "$dsn" -database "$dbname" -host "$host" -port "$port" \
483 -parameter "SSLServerCertificate=${cert_file};SecurityTransportMode=SSL;TLSVersion=TLSV12"
484}
485
486# SSL + Kerberos
487writecfg_ssl_krb() {
488 local dsn=$1 dbname=$2 host=$3 port=$4
489 local cert_file="${SSL_CERT_FILE:-$HOME/$REGION-bundle.pem}"
490 db2cli writecfg add -dsn "$dsn" -database "$dbname" -host "$host" -port "$port" \
491 -parameter "Authentication=KERBEROS;KRBPlugin=IBMkrb5;SSLServerCertificate=${cert_file};SecurityTransportMode=SSL;TLSVersion=TLSV12"
492}
493
494# =============================================================================
495# Read parameter group values for a given instance
496# Returns the ParameterValue or "" if not found / None
497# =============================================================================
498get_param_group_name() {
499 # Sets global PARAM_GROUP for the current DB_INSTANCE_IDENTIFIER
500 PARAM_GROUP=$(aws_rds describe-db-instances \
501 --db-instance-identifier "$DB_INSTANCE_IDENTIFIER" \
502 --region "$REGION" \
503 --query "DBInstances[0].DBParameterGroups[0].DBParameterGroupName" \
504 --output text 2>/dev/null)
505 [ "$PARAM_GROUP" = "None" ] && PARAM_GROUP=""
506}
507
508get_param_value() {
509 local param_name="$1"
510 [ -z "${PARAM_GROUP:-}" ] && echo "" && return
511 local val
512 val=$(aws_rds describe-db-parameters \
513 --db-parameter-group-name "$PARAM_GROUP" \
514 --region "$REGION" \
515 --query "Parameters[?ParameterName=='${param_name}'].ParameterValue" \
516 --output text 2>/dev/null)
517 [ "$val" = "None" ] && val=""
518 echo "$val"
519}
520
521get_ssl_port() {
522 get_param_value "ssl_svcename"
523}
524
525# Returns the db2comm value from the parameter group (e.g. "SSL", "TCPIP", "TCPIP,SSL")
526get_db2comm() {
527 local raw
528 raw=$(get_param_value "db2comm")
529 # Normalise: upper-case, strip spaces
530 echo "${raw^^}" | tr -d ' '
531}
532
533# True when db2comm contains TCPIP (and so TCP connections are allowed)
534db2comm_has_tcpip() {
535 local comm="$1"
536 [[ "$comm" == *"TCPIP"* ]]
537}
538
539# True when db2comm is set to SSL-only (no TCPIP)
540db2comm_ssl_only() {
541 local comm="$1"
542 [[ "$comm" == "SSL" ]]
543}
544
545download_pem_file() {
546 # Sets global SSL_CERT_FILE to the path of the cert Db2 should trust.
547 #
548 # Standard endpoint (E_URL not set):
549 # Downloads <region>-bundle.pem from the public RDS truststore.
550 # The bundle is reordered so RSA2048 is first (Db2 CLP requirement).
551 #
552 # Custom endpoint (E_URL set — PrivateLink, siteb, internal domain):
553 # The server presents a cert signed by an internal/Preprod CA that is
554 # NOT in the public RDS bundle. Instead, the root CA is extracted live
555 # from the server's TLS chain and saved as <region>-siteb-root-ca.pem.
556 # Only the root is needed — GSKit walks the chain from root to leaf.
557
558 if [ -n "${E_URL:-}" ]; then
559 _download_pem_custom_endpoint "$@"
560 else
561 _download_pem_standard "$@"
562 fi
563}
564
565_download_pem_standard() {
566 local pem_file="$HOME/$REGION-bundle.pem"
567 SSL_CERT_FILE="$pem_file"
568
569 if [ -f "$pem_file" ]; then
570 log_info "SSL certificate already present: $pem_file"
571 return 0
572 fi
573
574 if [ -n "${BUCKET:-}" ]; then
575 log_info "Downloading SSL certificate from s3://$BUCKET/ssl/$REGION-bundle.pem ..."
576 aws s3 cp "s3://$BUCKET/ssl/$REGION-bundle.pem" "$pem_file" \
577 --region "$REGION" --quiet
578 else
579 local url
580 url=$(rds_truststore_url "$REGION")
581 log_info "Downloading SSL certificate from $url ..."
582 curl -sL "$url" -o "$pem_file"
583 fi
584 if [ $? -ne 0 ]; then
585 log_error "Failed to download SSL certificate"
586 return 1
587 fi
588
589 # Reorder certificates so RSA2048 is first.
590 # Db2 CLP picks the first cert in the bundle for the TLS handshake.
591 # RDS for Db2 only has RSA2048 — if RSA4096 is first (e.g. us-west-1)
592 # the CLP connection fails. Python/JCC drivers iterate all certs so
593 # they are unaffected. This reorder is a no-op for regions where
594 # RSA2048 is already first (e.g. us-east-1).
595 if command -v openssl &>/dev/null; then
596 local tmp_pem; tmp_pem=$(mktemp)
597 awk '
598 /-----BEGIN CERTIFICATE-----/ { cert=""; in_cert=1 }
599 in_cert { cert = cert $0 "\n" }
600 /-----END CERTIFICATE-----/ { certs[++n] = cert; in_cert=0 }
601 END {
602 first=""; rest=""
603 for (i=1; i<=n; i++) {
604 cmd = "echo \"" certs[i] "\" | openssl x509 -noout -subject 2>/dev/null"
605 cmd | getline subj; close(cmd)
606 if (subj ~ /RSA2048/) { first = certs[i] }
607 else { rest = rest certs[i] }
608 }
609 printf "%s%s", first, rest
610 }
611 ' "$pem_file" > "$tmp_pem"
612 if [ -s "$tmp_pem" ]; then
613 mv -f "$tmp_pem" "$pem_file"
614 log_info "SSL cert reordered: RSA2048 first (Db2 CLP compatibility)"
615 else
616 rm -f "$tmp_pem"
617 log_warning "SSL cert reorder skipped — openssl subject parse returned empty"
618 fi
619 else
620 log_warning "openssl not found — skipping cert reorder (Db2 CLP may fail on regions where RSA2048 is not first)"
621 fi
622
623 log_success "SSL certificate saved to $pem_file"
624}
625
626_download_pem_custom_endpoint() {
627 # For custom/internal endpoints the server presents a cert signed by an
628 # internal CA (e.g. Amazon RDS Preprod Root CA) that is not in the public
629 # RDS bundle. Extract the root CA directly from the live TLS chain.
630 #
631 # The DB_ADDRESS global must be set before this is called (set in configure_dsn).
632
633 local root_ca_file="$HOME/$REGION-siteb-root-ca.pem"
634 SSL_CERT_FILE="$root_ca_file"
635
636 if [ -f "$root_ca_file" ]; then
637 log_info "Custom endpoint root CA already present: $root_ca_file"
638 return 0
639 fi
640
641 if [ -z "${DB_ADDRESS:-}" ]; then
642 log_error "DB_ADDRESS not set — cannot extract root CA from custom endpoint"
643 return 1
644 fi
645
646 if ! command -v openssl &>/dev/null; then
647 log_error "openssl not found — required to extract root CA from custom endpoint"
648 return 1
649 fi
650
651 log_info "Custom endpoint detected (E_URL set) — extracting root CA from TLS chain ..."
652 log_info "Connecting to $DB_ADDRESS:${SSL_PORT:-50443} ..."
653
654 # Pull full chain, skip the leaf (cert #1), save intermediate + root
655 local full_chain
656 full_chain=$(openssl s_client \
657 -connect "${DB_ADDRESS}:${SSL_PORT:-50443}" \
658 -showcerts \
659 2>/dev/null </dev/null)
660
661 if [ -z "$full_chain" ]; then
662 log_error "Could not retrieve TLS chain from $DB_ADDRESS:${SSL_PORT:-50443}"
663 return 1
664 fi
665
666 # Extract root CA — the last self-signed cert in the chain
667 # (issuer == subject). Works for chains of any depth.
668 echo "$full_chain" | awk '
669 /-----BEGIN CERTIFICATE-----/ { n++; cert="" }
670 { cert = cert $0 "\n" }
671 /-----END CERTIFICATE-----/ { certs[n] = cert }
672 END { print certs[n] }
673 ' > "$root_ca_file"
674
675 if [ ! -s "$root_ca_file" ]; then
676 log_error "Failed to extract root CA from TLS chain"
677 rm -f "$root_ca_file"
678 return 1
679 fi
680
681 # Verify it's actually self-signed (root CA)
682 local issuer subject
683 issuer=$(openssl x509 -noout -issuer -in "$root_ca_file" 2>/dev/null | sed 's/issuer=//')
684 subject=$(openssl x509 -noout -subject -in "$root_ca_file" 2>/dev/null | sed 's/subject=//')
685 if [ "$issuer" != "$subject" ]; then
686 log_warning "Extracted cert may not be a root CA (issuer != subject)"
687 log_warning "issuer: $issuer"
688 log_warning "subject: $subject"
689 fi
690
691 log_success "Root CA extracted: $root_ca_file"
692 log_info " Subject: $subject"
693}
694
695build_connect_help_rt() {
696 local alias_name=$1 db_name=$2 use_kerberos=${3:-false}
697 if [ "$use_kerberos" = "true" ]; then
698 HELP_COMMANDS+=("db2 \"connect to ${alias_name}\" # ${db_name}")
699 else
700 HELP_COMMANDS+=("db2 \"connect to ${alias_name} user ${MASTER_USER_NAME} using '\$MASTER_USER_PASSWORD'\" # ${db_name}")
701 fi
702}
703
704print_all_help() {
705 [ ${#HELP_COMMANDS[@]} -eq 0 ] && return
706 echo ""
707 echo " ========================="
708 echo " db2 terminate"
709 for c in "${HELP_COMMANDS[@]}"; do echo " $c"; done
710 echo " ========================="
711 echo ""
712}
713
714# =============================================================================
715# Main DSN configuration
716# =============================================================================
717configure_dsn() {
718 log_info "============================================================================"
719 log_info "Creating DB2 RT DSN entries for RDS DB2 instance(s)"
720 log_info "Region: $REGION"
721 log_info "============================================================================"
722
723 detect_domain_join
724 list_db_instances || return 1
725 get_all_master_user_names
726 get_all_master_passwords
727
728 # Clean slate before writing any DSN entries
729 rm -f "$HOME/sqllib/cfg/db2dsdriver.cfg"
730
731 for i in "${!DB_INSTANCES[@]}"; do
732 local DB_INSTANCE_IDENTIFIER="${DB_INSTANCES[$i]}"
733 local MASTER_USER_NAME="${MASTER_USER_NAMES[$i]}"
734 local MASTER_USER_PASSWORD="${MASTER_USER_PASSWORDS[$i]}"
735 local SUFFIX; [ ${#DB_INSTANCES[@]} -eq 1 ] && SUFFIX="" || SUFFIX="$i"
736
737 log_info "============================================================================"
738 log_info "Processing: $DB_INSTANCE_IDENTIFIER"
739
740 [ -z "$MASTER_USER_NAME" ] && log_error "No master user for $DB_INSTANCE_IDENTIFIER — skipping" && continue
741 [ -z "$MASTER_USER_PASSWORD" ] && log_warning "No password for $DB_INSTANCE_IDENTIFIER — skipping" && continue
742
743 local DB_ADDRESS DB_TCP_IP_PORT
744 DB_ADDRESS=$(aws_rds describe-db-instances \
745 --db-instance-identifier "$DB_INSTANCE_IDENTIFIER" \
746 --region "$REGION" \
747 --query "DBInstances[0].Endpoint.Address" \
748 --output text 2>/dev/null)
749 DB_TCP_IP_PORT=$(aws_rds describe-db-instances \
750 --db-instance-identifier "$DB_INSTANCE_IDENTIFIER" \
751 --region "$REGION" \
752 --query "DBInstances[0].Endpoint.Port" \
753 --output text 2>/dev/null)
754
755 [ -z "$DB_ADDRESS" ] && log_error "No endpoint for $DB_INSTANCE_IDENTIFIER — skipping" && continue
756
757 # -----------------------------------------------------------------------
758 # Read parameter group values for this instance
759 # -----------------------------------------------------------------------
760 get_param_group_name # sets $PARAM_GROUP
761
762 local DB2COMM SSL_PORT
763 DB2COMM=$(get_db2comm)
764 SSL_PORT=$(get_ssl_port)
765
766 # Default to TCPIP if db2comm is not set in the parameter group
767 [ -z "$DB2COMM" ] && DB2COMM="TCPIP"
768
769 log_info "db2comm : ${DB2COMM} | ssl_svcename : ${SSL_PORT:-<not set>}"
770
771 local WANT_TCP=false WANT_SSL=false
772 db2comm_has_tcpip "$DB2COMM" && WANT_TCP=true
773 [ -n "$SSL_PORT" ] && WANT_SSL=true
774
775 if [ "$WANT_SSL" = "false" ] && [ "$WANT_TCP" = "false" ]; then
776 log_warning "Neither TCPIP port nor ssl_svcename configured for $DB_INSTANCE_IDENTIFIER — skipping"
777 continue
778 fi
779
780 # -----------------------------------------------------------------------
781 # Bootstrap: write a temporary DSN to discover database names.
782 # Use SSL (local auth) when db2comm is SSL-only; otherwise use TCP.
783 # -----------------------------------------------------------------------
784 local TEMP_DSN="RDSTMP${SUFFIX}"
785 if [ "$WANT_TCP" = "true" ]; then
786 writecfg_tcp "$TEMP_DSN" "RDSADMIN" "$DB_ADDRESS" "$DB_TCP_IP_PORT" >/dev/null 2>&1
787 else
788 # SSL-only — download cert first (sets SSL_CERT_FILE)
789 if ! download_pem_file; then
790 log_error "Cannot download SSL cert for $DB_INSTANCE_IDENTIFIER — skipping"
791 continue
792 fi
793 # Bootstrap always uses local auth — Kerberos DSNs are written after discovery
794 writecfg_ssl_local "$TEMP_DSN" "RDSADMIN" "$DB_ADDRESS" "$SSL_PORT" >/dev/null 2>&1
795 fi
796
797 # Fetch database names using the temporary DSN
798 get_all_database_names "$DB_INSTANCE_IDENTIFIER" "$MASTER_USER_NAME" "$MASTER_USER_PASSWORD" "$TEMP_DSN" || true
799 log_info "Databases to register: ${DB_NAMES[*]:-<none found>}"
800
801 # Remove temp DSN — final entries written below
802 db2cli writecfg remove -dsn "$TEMP_DSN" >/dev/null 2>&1 || true
803
804 # -----------------------------------------------------------------------
805 # Write TCP DSN entries (RDSAT / <DB>T)
806 # -----------------------------------------------------------------------
807 if [ "$WANT_TCP" = "true" ]; then
808 local tcp_admin_dsn="RDSAT${SUFFIX}"
809 log_info "Creating TCP DSN: $tcp_admin_dsn (local auth)"
810 writecfg_tcp "$tcp_admin_dsn" "RDSADMIN" "$DB_ADDRESS" "$DB_TCP_IP_PORT"
811 build_connect_help_rt "$tcp_admin_dsn" "RDSADMIN TCP"
812 for dbname in "${DB_NAMES[@]}"; do
813 local alias_t; alias_t="$(generate_db_alias "$dbname" "T" "$SUFFIX")"
814 log_info "Registering $dbname as $alias_t (TCP local)"
815 writecfg_tcp "$alias_t" "$dbname" "$DB_ADDRESS" "$DB_TCP_IP_PORT"
816 build_connect_help_rt "$alias_t" "$dbname TCP"
817 done
818 fi
819
820 # -----------------------------------------------------------------------
821 # Write SSL DSN entries (RDSAS / <DB>S and, when domain-joined, RDSAKS / <DB>SK)
822 # -----------------------------------------------------------------------
823 if [ "$WANT_SSL" = "true" ]; then
824 # Cert may already be downloaded in the bootstrap block above; idempotent
825 if ! download_pem_file; then
826 log_warning "SSL cert unavailable — skipping SSL entries for $DB_INSTANCE_IDENTIFIER"
827 else
828 log_info "SSL port: $SSL_PORT"
829
830 # --- SSL + local auth ---
831 local ssl_local_admin="RDSAS${SUFFIX}"
832 log_info "Creating SSL DSN: $ssl_local_admin (local auth)"
833 writecfg_ssl_local "$ssl_local_admin" "RDSADMIN" "$DB_ADDRESS" "$SSL_PORT"
834 build_connect_help_rt "$ssl_local_admin" "RDSADMIN SSL"
835
836 for dbname in "${DB_NAMES[@]}"; do
837 local alias_s; alias_s="$(generate_db_alias "$dbname" "S" "$SUFFIX")"
838 log_info "Registering $dbname as $alias_s (SSL local)"
839 writecfg_ssl_local "$alias_s" "$dbname" "$DB_ADDRESS" "$SSL_PORT"
840 build_connect_help_rt "$alias_s" "$dbname SSL"
841 done
842
843 # --- SSL + Kerberos (domain-joined only) ---
844 if [ "${IS_DOMAIN_JOINED:-false}" = "true" ]; then
845 log_info "Domain-joined host — also creating Kerberos SSL DSN entries"
846
847 local ssl_krb_admin="RDSAKS${SUFFIX}"
848 log_info "Creating SSL+Kerberos DSN: $ssl_krb_admin"
849 writecfg_ssl_krb "$ssl_krb_admin" "RDSADMIN" "$DB_ADDRESS" "$SSL_PORT"
850 build_connect_help_rt "$ssl_krb_admin" "RDSADMIN SSL+Kerberos" "true"
851
852 for dbname in "${DB_NAMES[@]}"; do
853 local alias_sk; alias_sk="$(generate_db_alias "$dbname" "SK" "$SUFFIX")"
854 log_info "Registering $dbname as $alias_sk (SSL Kerberos)"
855 writecfg_ssl_krb "$alias_sk" "$dbname" "$DB_ADDRESS" "$SSL_PORT"
856 build_connect_help_rt "$alias_sk" "$dbname SSL+Kerberos" "true"
857 done
858 fi
859 fi
860 fi
861 done
862}
863
864# =============================================================================
865# Entry point
866# =============================================================================
867main() {
868 validate || return 1
869 set_credentials
870 configure_dsn || return 1
871 unset DB_INSTANCE_ID # clean up the user-supplied env var only AFTER configure_dsn has consumed it
872 print_all_help | tee "$HOME/CONN_HELP_README.txt" >&2
873 log_info "Run 'db2 terminate' then use the commands above (also saved to ~/CONN_HELP_README.txt)"
874
875 # Write instance registry (instance→DSN mapping, no passwords)
876 local registry="$HOME/.db2instances"
877 # Append or create entry for each instance
878 touch "$registry"
879 for i in "${!DB_INSTANCES[@]}"; do
880 local suffix; [ ${#DB_INSTANCES[@]} -eq 1 ] && suffix="" || suffix="$i"
881 # Determine which DSN names were written based on db2comm
882 DB_INSTANCE_IDENTIFIER="${DB_INSTANCES[$i]}"
883 get_param_group_name
884 local comm; comm=$(get_db2comm)
885 [ -z "$comm" ] && comm="TCPIP"
886 local tcp_dsn="" ssl_dsn="" krb_dsn=""
887 local ssl_port_val; ssl_port_val=$(get_ssl_port)
888 db2comm_has_tcpip "$comm" && tcp_dsn="RDSAT${suffix}"
889 [ -n "$ssl_port_val" ] && ssl_dsn="RDSAS${suffix}"
890 [ -n "$ssl_port_val" ] && [ "${IS_DOMAIN_JOINED:-false}" = "true" ] && krb_dsn="RDSAKS${suffix}"
891 # Remove existing entry for this instance then re-add
892 sed -i '' "/^${DB_INSTANCES[$i]}|/d" "$registry" 2>/dev/null || \
893 sed -i "/^${DB_INSTANCES[$i]}|/d" "$registry" 2>/dev/null || true
894 echo "${DB_INSTANCES[$i]}|${MASTER_USER_NAMES[$i]}|${tcp_dsn}|${ssl_dsn}|${krb_dsn}|${REGION}" >> "$registry"
895 done
896 chmod 600 "$registry"
897 log_success "Instance registry saved to $registry"
898
899 # Persist credentials for the last processed instance to ~/.db2env
900 # Uses printf %q to safely escape special characters in the password.
901 local last=$((${#DB_INSTANCES[@]} - 1))
902 export MASTER_USER_NAME="${MASTER_USER_NAMES[$last]}"
903 export MASTER_USER_PASSWORD="${MASTER_USER_PASSWORDS[$last]}"
904 # Default DSN priority: Kerberos SSL > local SSL > TCP
905 DB_INSTANCE_IDENTIFIER="${DB_INSTANCES[$last]}"
906 get_param_group_name
907 local last_comm; last_comm=$(get_db2comm)
908 [ -z "$last_comm" ] && last_comm="TCPIP"
909 local last_suffix; [ ${#DB_INSTANCES[@]} -eq 1 ] && last_suffix="" || last_suffix="$last"
910 local last_ssl_port; last_ssl_port=$(get_ssl_port)
911 if [ -n "$last_ssl_port" ] && [ "${IS_DOMAIN_JOINED:-false}" = "true" ]; then
912 export DB_DSN="RDSAKS${last_suffix}"
913 elif [ -n "$last_ssl_port" ]; then
914 export DB_DSN="RDSAS${last_suffix}"
915 else
916 export DB_DSN="RDSAT${last_suffix}"
917 fi
918 {
919 echo "export REGION=$(printf '%q' "$REGION")"
920 echo "export DB_INSTANCE_ID=$(printf '%q' "${DB_INSTANCES[$last]}")"
921 echo "export DB_DSN=$(printf '%q' "$DB_DSN")"
922 echo "export MASTER_USER_NAME=$(printf '%q' "${MASTER_USER_NAMES[$last]}")"
923 echo "export MASTER_USER_PASSWORD=$(printf '%q' "${MASTER_USER_PASSWORDS[$last]}")"
924 } > "$HOME/.db2env"
925 chmod 600 "$HOME/.db2env"
926 log_success "Credentials saved to ~/.db2env — auto-loaded by functions.sh"
927 log_success "DSN configuration complete. Connection help saved to ~/CONN_HELP_README.txt"
928 # Add source functions.sh to shell profile files if not already there
929 local source_line='source ~/functions.sh'
930 local comment='# DB2 helper functions'
931 for profile in "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do
932 [ -f "$profile" ] || continue
933 if ! grep -q 'source ~/functions.sh' "$profile" 2>/dev/null; then
934 echo '' >> "$profile"
935 echo "$comment" >> "$profile"
936 echo "$source_line" >> "$profile"
937 log_success "Added 'source ~/functions.sh' to $profile"
938 fi
939 done
940 log_info "Run 'source ~/.bashrc' or log out and back in to activate. Then run 'db2_help' to see available helper functions."
941 echo "" >&2
942 echo " ============================" >&2
943 echo " source ~/.bashrc" >&2
944 echo " db2_help" >&2
945 echo " ============================" >&2
946 echo "" >&2
947}
948
949main