@@ -22,7 +22,13 @@ set -o pipefail
2222DATE=" "
2323SSH_USER=" ${SSH_USER:- root} "
2424REQUESTED_HOSTS=()
25- SSH_OPTS=" ${SSH_OPTS:- -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no} "
25+ SSH_OPTS=" ${SSH_OPTS:- -o BatchMode=yes -o ConnectTimeout=10} "
26+ SSH_KEY=" ${SSH_KEY:- } "
27+ SSH_PASSWORD=" ${SSH_PASSWORD:- } "
28+ HOST_PASSWORDS_FILE=" "
29+ WORKDIR_BASE=" "
30+
31+ declare -A HOST_PASSWORDS=()
2632
2733MANAGEMENT_LOG_DIR=" /var/log/cloudstack/management"
2834MANAGEMENT_LOG=" management-server.log"
@@ -40,18 +46,25 @@ kvm_hosts=()
4046usage () {
4147 cat << EOF
4248Usage:
43- $0 [--date YYYY-MM-DD] [--hosts HOST1,HOST2,...]
49+ $0 [--date YYYY-MM-DD] [--hosts HOST1,HOST2,...] [--workdir DIR]
50+ [--ssh-key FILE | --ssh-password PASSWORD | --host-passwords FILE]
4451
4552Examples:
4653 $0
4754 $0 --date 2026-08-25
4855 $0 --hosts 10.0.0.21
4956 $0 --hosts 10.0.0.21,10.0.0.22,kvm03.example.com
5057 $0 --date 2026-08-25 --hosts 10.0.0.21,10.0.0.22
58+ $0 --workdir /data/cloudstack-logs
59+ $0 --ssh-key /root/.ssh/id_rsa
60+ $0 --ssh-password 'secret'
61+ $0 --host-passwords /root/host-passwords.txt
5162
5263Environment variables:
5364 SSH_USER=root
5465 SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=10"
66+ SSH_KEY
67+ SSH_PASSWORD
5568
5669Behavior:
5770 KVM host:
@@ -62,6 +75,24 @@ Behavior:
6275 If CloudMonkey is unavailable or API discovery fails, falls back to
6376 the CloudStack database.
6477
78+ Logs are pulled from other management servers and KVM hosts over
79+ SSH as \$ SSH_USER, authenticating with (in order of precedence):
80+ 1. A per-host password from --host-passwords, if the host appears
81+ in that file.
82+ 2. A single password from --ssh-password or \$ SSH_PASSWORD, applied
83+ to every remote host.
84+ 3. A single private key from --ssh-key or \$ SSH_KEY, applied to
85+ every remote host.
86+ 4. Whatever SSH would otherwise use (agent, default identity
87+ files), if none of the above are given.
88+
89+ Password-based authentication requires the "sshpass" utility.
90+
91+ --host-passwords FILE:
92+ FILE contains one "host=password" pair per line (blank lines and
93+ lines starting with # are ignored). host must match the IP or
94+ hostname used to reach that server.
95+
6596 Without --date:
6697 Collects the current active log files.
6798
@@ -74,6 +105,14 @@ Behavior:
74105
75106 On a KVM host, --hosts is ignored because only the local agent log is
76107 collected.
108+
109+ Without --workdir:
110+ Collected logs and the resulting archive are placed in a directory
111+ under \$ {TMPDIR:-/tmp}.
112+
113+ With --workdir:
114+ Collected logs and the resulting archive are placed in the specified
115+ directory instead, which must already exist.
77116EOF
78117}
79118
@@ -104,6 +143,26 @@ while [[ $# -gt 0 ]]; do
104143 [[ -n " $host " ]] && REQUESTED_HOSTS+=(" $host " )
105144 done
106145
146+ shift 2
147+ ;;
148+ --workdir)
149+ [[ $# -ge 2 ]] || die " --workdir requires a directory path"
150+ WORKDIR_BASE=" $2 "
151+ shift 2
152+ ;;
153+ --ssh-key)
154+ [[ $# -ge 2 ]] || die " --ssh-key requires a file path"
155+ SSH_KEY=" $2 "
156+ shift 2
157+ ;;
158+ --ssh-password)
159+ [[ $# -ge 2 ]] || die " --ssh-password requires a password"
160+ SSH_PASSWORD=" $2 "
161+ shift 2
162+ ;;
163+ --host-passwords)
164+ [[ $# -ge 2 ]] || die " --host-passwords requires a file path"
165+ HOST_PASSWORDS_FILE=" $2 "
107166 shift 2
108167 ;;
109168 -h|--help)
@@ -125,7 +184,29 @@ else
125184 SUFFIX=" $( date ' +%Y%m%d-%H%M%S' ) "
126185fi
127186
128- WORKDIR=" cloudstack-logs-${SUFFIX} "
187+ WORKDIR_BASE=" ${WORKDIR_BASE:- ${TMPDIR:-/ tmp} } "
188+ [[ -d " $WORKDIR_BASE " ]] || die " Working directory does not exist: $WORKDIR_BASE "
189+
190+ if [[ -n " $SSH_KEY " ]]; then
191+ [[ -f " $SSH_KEY " ]] || die " SSH key not found: $SSH_KEY "
192+ fi
193+
194+ if [[ -n " $HOST_PASSWORDS_FILE " ]]; then
195+ [[ -r " $HOST_PASSWORDS_FILE " ]] || die " Cannot read host password file: $HOST_PASSWORDS_FILE "
196+
197+ while IFS=' =' read -r host password; do
198+ host=" $( printf ' %s' " $host " | xargs) "
199+ [[ -z " $host " || " $host " == \# * ]] && continue
200+ HOST_PASSWORDS[" $host " ]=" $password "
201+ done < " $HOST_PASSWORDS_FILE "
202+ fi
203+
204+ if [[ -n " $SSH_PASSWORD " || ${# HOST_PASSWORDS[@]} -gt 0 ]]; then
205+ command -v sshpass > /dev/null 2>&1 ||
206+ die " sshpass is required for password-based SSH authentication"
207+ fi
208+
209+ WORKDIR=" ${WORKDIR_BASE%/ } /cloudstack-logs-${SUFFIX} "
129210ARCHIVE=" ${WORKDIR} .tar.gz"
130211
131212rm -rf " $WORKDIR "
@@ -203,19 +284,51 @@ collect_local_log() {
203284 fi
204285}
205286
287+ host_password () {
288+ local host=" $1 "
289+
290+ if [[ -n " ${HOST_PASSWORDS[$host]:- } " ]]; then
291+ printf ' %s' " ${HOST_PASSWORDS[$host]} "
292+ else
293+ printf ' %s' " $SSH_PASSWORD "
294+ fi
295+ }
296+
206297collect_remote_log () {
207298 local host=" $1 "
208299 local log_dir=" $2 "
209300 local log_name=" $3 "
210301 local output=" $4 "
302+ local password
303+ local passfile=" "
304+ local ssh_cmd=(ssh)
305+ local ssh_opts=" $SSH_OPTS "
306+ local rc
307+
308+ password=" $( host_password " $host " ) "
309+
310+ if [[ -n " $password " ]]; then
311+ passfile=" $( mktemp) " || return 1
312+ chmod 600 " $passfile "
313+ printf ' %s' " $password " > " $passfile "
314+ ssh_cmd=(sshpass -f " $passfile " ssh)
315+ # BatchMode disables password/keyboard-interactive auth outright; an
316+ # earlier -o wins over a later one for the same key, so this has to
317+ # come before $SSH_OPTS to override any BatchMode=yes already in it.
318+ ssh_opts=" -o BatchMode=no $ssh_opts "
319+ elif [[ -n " $SSH_KEY " ]]; then
320+ ssh_cmd=(ssh -i " $SSH_KEY " )
321+ fi
211322
212323 if [[ -z " $DATE " ]]; then
213- ssh ${SSH_OPTS } " ${SSH_USER} @${host} " \
324+ " ${ssh_cmd[@]} " ${ssh_opts } " ${SSH_USER} @${host} " \
214325 " cat '${log_dir} /${log_name} '" > " $output "
215- return
326+ rc=$?
327+ [[ -n " $passfile " ]] && rm -f " $passfile "
328+ return $rc
216329 fi
217330
218- ssh ${SSH_OPTS } " ${SSH_USER} @${host} " \
331+ " ${ssh_cmd[@]} " ${ssh_opts } " ${SSH_USER} @${host} " \
219332 bash -s -- " $log_dir " " $log_name " " $DATE " > " $output " << 'REMOTE '
220333LOG_DIR="$1"
221334LOG_NAME="$2"
@@ -247,6 +360,9 @@ awk -v requested_date="$REQUESTED_DATE" '
247360 }
248361'
249362REMOTE
363+ rc=$?
364+ [[ -n " $passfile " ]] && rm -f " $passfile "
365+ return $rc
250366}
251367
252368# --- CloudMonkey discovery ---
@@ -333,11 +449,26 @@ discover_with_cloudmonkey() {
333449get_property () {
334450 local property=" $1 "
335451
336- awk -F= -v property=" $property " '
337- $1 == property {
338- sub(/^[^=]*=/, "")
339- print
340- exit
452+ awk -v property=" $property " '
453+ {
454+ line = $0
455+ sub(/^[ \t]+/, "", line)
456+ sub(/[ \t]+$/, "", line)
457+
458+ if (line == "" || line ~ /^[#!]/) next
459+
460+ eq = index(line, "=")
461+ if (eq == 0) next
462+
463+ key = substr(line, 1, eq - 1)
464+ sub(/[ \t]+$/, "", key)
465+
466+ if (key == property) {
467+ value = substr(line, eq + 1)
468+ sub(/^[ \t]+/, "", value)
469+ print value
470+ exit
471+ }
341472 }
342473 ' " $DB_PROPERTIES "
343474}
@@ -349,6 +480,8 @@ query_cloudstack_db() {
349480 local db_name
350481 local db_user
351482 local db_password
483+ local defaults_file
484+ local rc
352485
353486 [[ -r " $DB_PROPERTIES " ]] || return 1
354487 command -v mysql > /dev/null 2>&1 || return 1
@@ -364,14 +497,27 @@ query_cloudstack_db() {
364497 db_port=" ${db_port:- 3306} "
365498 db_name=" ${db_name:- cloud} "
366499
367- MYSQL_PWD=" $db_password " mysql \
500+ defaults_file=" $( mktemp) " || return 1
501+ chmod 600 " $defaults_file "
502+
503+ {
504+ printf ' [client]\n'
505+ printf ' host=%s\n' " $db_host "
506+ printf ' port=%s\n' " $db_port "
507+ printf ' user=%s\n' " $db_user "
508+ printf ' password=%s\n' " $db_password "
509+ } > " $defaults_file "
510+
511+ mysql \
512+ --defaults-extra-file=" $defaults_file " \
368513 --batch \
369514 --skip-column-names \
370- -h " $db_host " \
371- -P " $db_port " \
372- -u " $db_user " \
373515 " $db_name " \
374516 -e " $query "
517+ rc=$?
518+
519+ rm -f " $defaults_file "
520+ return $rc
375521}
376522
377523discover_with_database () {
@@ -445,10 +591,13 @@ collect_from_kvm_host() {
445591
446592 mkdir -p " $WORKDIR /kvm/$host_name "
447593
448- collect_local_log \
594+ if ! collect_local_log \
449595 " $AGENT_LOG_DIR " \
450596 " $AGENT_LOG " \
451- " $WORKDIR /kvm/$host_name /$AGENT_LOG "
597+ " $WORKDIR /kvm/$host_name /$AGENT_LOG " ; then
598+ log " WARNING: Failed to collect local agent log"
599+ rm -f " $WORKDIR /kvm/$host_name /$AGENT_LOG "
600+ fi
452601}
453602
454603collect_from_management_server () {
@@ -478,6 +627,7 @@ collect_from_management_server() {
478627 " $MANAGEMENT_LOG " \
479628 " $WORKDIR /management/$host /$MANAGEMENT_LOG " ; then
480629 log " WARNING: Failed to collect local management log"
630+ rm -f " $WORKDIR /management/$host /$MANAGEMENT_LOG "
481631 fi
482632 elif ! collect_remote_log \
483633 " $host " \
@@ -536,8 +686,11 @@ Requested KVM hosts: ${REQUESTED_HOSTS[*]:-all}
536686EOF
537687
538688log " Creating archive $ARCHIVE "
539- tar -czf " $ARCHIVE " " $WORKDIR "
540- rm -rf " $WORKDIR "
689+ if tar -czf " $ARCHIVE " -C " $WORKDIR_BASE " " $( basename " $WORKDIR " ) " ; then
690+ rm -rf " $WORKDIR "
691+ else
692+ die " Failed to create archive $ARCHIVE ; collected logs remain in $WORKDIR "
693+ fi
541694
542695echo
543696echo " CloudStack logs collected:"
0 commit comments