Files

327 lines
16 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# 3-5 计算节点评分:CentOS 7.9 / OpenStack Rocky,Bash 4+,满分 100。
# 用法:在 compute 上执行 bash 3-5-compute.sh
# 可选:CHECK_CONTROLLER_IP=控制节点管理IP(默认从 hosts 读取,否则 192.168.30.129)
# CHECK_COMPUTE_IP=计算节点管理IP(默认从 hosts 读取)
# CHECK_REPO_BASE_URL=软件源根URL(默认 http://192.168.192.205:3080)
# 只查询当前状态;不安装软件、改配置、重启服务或重建缓存。
# 无法从节点内证明 VMware 操作、克隆或命令历史,不为这些过程评分。
# 退出码:0=100分,1=有扣分,2=权限/基础工具/参数错误,未完成评分。
# 分值:安全8、主机与网络12、软件源15、NTP15、Nova软件包5、
# Nova配置30、虚拟化5、服务10。每个配置小组内全部通过才获得该小组分值。
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
set -o pipefail
SCORE=0
TOTAL=0
PASS_CNT=0
FAIL_CNT=0
TASK_CNT=0
MANUAL_LOCATION=''
MANUAL_HINT_SHOWN=0
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'
separator() {
printf '%b====================================================%b\n' "$YELLOW" "$NC"
}
banner() {
separator
printf '%b %s%b\n' "$YELLOW" "$1" "$NC"
separator
}
section() {
TASK_CNT=$((TASK_CNT+1))
MANUAL_LOCATION=${2:-}
MANUAL_HINT_SHOWN=0
printf '%b[➜] %d. %s%b\n' "$BLUE" "$TASK_CNT" "$1" "$NC"
}
error() {
printf '%b[✘ 失败] %s%b\n' "$RED" "$1" "$NC"
}
# 每个评分项分值固定;查询失败/依赖缺失也计入满分,不跳项缩小分母。
# 通过和失败各自使用明确的提示,逐项实时显示。
check() {
local points=$1 success_message=$2 failure_message=$3
shift 3
TOTAL=$((TOTAL+points))
if "$@" >/dev/null 2>&1; then
SCORE=$((SCORE+points)); PASS_CNT=$((PASS_CNT+1))
printf '%b[✔ 通过] %s%b\n' "$GREEN" "$success_message" "$NC"
else
FAIL_CNT=$((FAIL_CNT+1))
printf '%b[✘ 失败] %s%b\n' "$RED" "$failure_message" "$NC"
# 同一任务只在第一次失败时提示手册位置;全部通过时不增加输出。
if [ -n "$MANUAL_LOCATION" ] && [ "$MANUAL_HINT_SHOWN" -eq 0 ]; then
printf '%b → 手册:%s%b\n' "$RED" "$MANUAL_LOCATION" "$NC"
MANUAL_HINT_SHOWN=1
fi
fi
}
finish() {
printf '\n'
separator
printf '%b总成绩:%d/100 分%b | 通过: %b%d%b | 失败: %b%d%b\n' "$YELLOW" "$SCORE" "$NC" "$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
if [ "$TOTAL" -ne 100 ]; then
error "评分项总分异常:$TOTAL,请联系教师检查脚本。"
separator
exit 2
fi
if [ "$FAIL_CNT" -eq 0 ]; then
separator
exit 0
fi
printf '%b请按红字提示修正。%b\n' "$RED" "$NC"
separator
exit 1
}
preflight() {
if [ "$EUID" -ne 0 ]; then error '请在 compute 节点用 root 执行。未评分。'; exit 2; fi
local cmd
for cmd in awk grep sort tr timeout; do
if ! command -v "$cmd" >/dev/null 2>&1; then error "缺少基础工具 $cmd,无法评分。"; exit 2; fi
done
}
valid_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF!=4 { bad=1 }
{ for(i=1;i<=NF;i++) if($i !~ /^[0-9]+$/ || length($i)>3 || $i+0>255) bad=1 }
END { exit (NR!=1 || bad) }'
}
hosts_ip() {
awk -v name="$1" '{sub(/#.*/, ""); for(i=2;i<=NF;i++) if($i==name) print $1}' /etc/hosts 2>/dev/null | sort -u
}
resolved_as() {
local addresses
valid_ipv4 "$2" || return 1
addresses=$(timeout 5 getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u) || return 1
[ "$addresses" = "$2" ]
}
# 按节读取最后一个有效赋值;忽略整行注释,不执行配置文件。
ini_value() {
awk -v section="$2" -v key="$3" '
/^[[:space:]]*[#;]/ { next }
{ sub(/\r$/, "") }
/^[[:space:]]*\[/ {
s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); next
}
s==section && index($0,"=") {
k=substr($0,1,index($0,"=")-1); gsub(/^[[:space:]]+|[[:space:]]+$/,"",k)
if(k==key) {
v=substr($0,index($0,"=")+1)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",v)
}
}
END {print v}' "$1" 2>/dev/null
}
enabled_service() { [ "$(timeout 10 systemctl is-enabled "$1" 2>/dev/null)" = enabled ]; }
active_service() { timeout 10 systemctl is-active --quiet "$1"; }
banner 'OpenStack 计算节点评分'
preflight
CONF=/etc/nova/nova.conf
# 自动检测失败(例如 hosts 冲突)仍继续评分,让解析项正常扣分。
CONTROLLER_IP=${CHECK_CONTROLLER_IP:-$(hosts_ip controller)}
if [ -z "${CHECK_CONTROLLER_IP:-}" ] && ! valid_ipv4 "$CONTROLLER_IP"; then
CONTROLLER_IP=192.168.30.129
fi
COMPUTE_IP=${CHECK_COMPUTE_IP:-$(hosts_ip compute)}
REPO_BASE=${CHECK_REPO_BASE_URL:-http://192.168.192.205:3080}
REPO_BASE=${REPO_BASE%/}
if ! valid_ipv4 "$CONTROLLER_IP"; then error '控制节点 IP 参数或 hosts 映射无效。未评分。'; exit 2; fi
if [ -n "${CHECK_COMPUTE_IP:-}" ] && ! valid_ipv4 "$CHECK_COMPUTE_IP"; then error 'CHECK_COMPUTE_IP 无效。未评分。'; exit 2; fi
case "$REPO_BASE" in http://*|https://*) ;; *) error '软件源根 URL 必须使用 http(s)。未评分。'; exit 2;; esac
section '安全设置(8分)' '节点环境准备 / 二、基础安全设置'
firewall_stopped() {
local properties
# CentOS 7 上 is-active 可能返回 unknown;直接查询服务属性确认状态。
# 不使用 --value,兼容旧版 systemctl;服务不存在或查询失败不能算通过。
properties=$(timeout 10 systemctl show firewalld.service -p LoadState -p ActiveState 2>/dev/null) || return 1
printf '%s\n' "$properties" | awk -F= '
$1=="LoadState" {load=$2; loads++}
$1=="ActiveState" {active=$2; actives++}
END {exit !(loads==1 && actives==1 &&
(load=="loaded" || load=="masked") && active=="inactive")}'
}
firewall_disabled() {
local state
state=$(timeout 10 systemctl is-enabled firewalld 2>/dev/null)
case "$state" in disabled|masked) return 0;; *) return 1;; esac
}
selinux_runtime() { case "$(getenforce 2>/dev/null)" in Disabled|Permissive) return 0;; *) return 1;; esac; }
selinux_persistent() {
awk '/^[[:space:]]*#/ {next}
/^[[:space:]]*SELINUX[[:space:]]*=/ {
v=$0; sub(/^[^=]*=/,"",v); sub(/[[:space:]]*#.*/,"",v); gsub(/[[:space:]"\047]/,"",v)
} END {exit (v!="disabled")}' /etc/selinux/config
}
check 2 'firewalld 已停止' 'firewalld 未停止或查询失败' firewall_stopped
check 2 'firewalld 自启已禁用' 'firewalld 自启未禁用' firewall_disabled
check 2 'SELinux 非强制模式' 'SELinux 仍强制执行或查询失败' selinux_runtime
check 2 'SELinux 永久禁用' 'SELinux 未永久禁用' selinux_persistent
section '主机与网络(12分)' '节点环境准备 / 三、配置主机名解析(检查 /etc/hosts)'
hostname_ok() { [ "$(hostname)" = compute ] && [ "$(tr -d '[:space:]' < /etc/hostname)" = compute ]; }
controller_mapping() { [ "$(hosts_ip controller)" = "$CONTROLLER_IP" ]; }
compute_mapping() {
valid_ipv4 "$COMPUTE_IP" && [ "$COMPUTE_IP" != "$CONTROLLER_IP" ] &&
[ "$(hosts_ip compute)" = "$COMPUTE_IP" ] &&
ip -o -4 addr show scope global | awk -v ip="$COMPUTE_IP" '{split($4,a,"/"); if(a[1]==ip)found=1} END{exit !found}'
}
both_resolve() { resolved_as controller "$CONTROLLER_IP" && resolved_as compute "$COMPUTE_IP"; }
localhost_preserved() { awk '{sub(/#.*/,""); if($1=="127.0.0.1")for(i=2;i<=NF;i++)if($i=="localhost")found=1} END{exit !found}' /etc/hosts; }
check 2 '主机名 compute' '主机名应为 compute' hostname_ok
check 2 'controller hosts 映射' 'controller hosts 映射缺失或冲突' controller_mapping
check 2 'compute hosts 映射' 'compute hosts 应指向本机管理 IP' compute_mapping
check 2 '主机名解析' '主机名解析与 hosts 不一致' both_resolve
check 2 'localhost 映射保留' 'localhost 映射缺失' localhost_preserved
check 2 'ping controller' 'ping controller 不通' timeout 8 ping -4 -c 2 -W 2 controller
section '软件源(15分)' '节点环境准备 / 四、配置软件源(配置文件、重建缓存)'
# 每个仓库分别评价配置、repomd 索引、已有缓存,各1分。
shopt -s nullglob
REPOS=(/etc/yum.repos.d/*.repo)
repo_layout_ok() {
[ "${#REPOS[@]}" -gt 0 ] || return 1
awk '
function flush() {
if(s!="" && enabled!="0" && s!="local-base" && s!="local-updates" &&
s!="local-extras" && s!="local-qemu-ev" && s!="local-openstack-rocky") bad=1
}
FNR==1 {flush(); s=""; enabled="1"}
/^[[:space:]]*[#;]/ {next}
/^[[:space:]]*\[/ {
flush(); s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); enabled="1"; next
}
/^[[:space:]]*enabled[[:space:]]*=/ {
enabled=$0; sub(/^[^=]*=/,"",enabled); sub(/[[:space:]]+[#;].*$/,"",enabled)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",enabled)
}
END{flush(); exit bad}' "${REPOS[@]}"
}
repo_ok() {
local file=$1 id=$2 url=$3 key value count excludes
[ -r "$file" ] && repo_layout_ok || return 1
count=$(awk -v id="$id" '/^[[:space:]]*\[/ {s=$0; sub(/^[[:space:]]*\[/,"",s); sub(/\].*$/,"",s); gsub(/^[[:space:]]+|[[:space:]]+$/,"",s); if(s==id)n++} END{print n+0}' "${REPOS[@]}") || return 1
[ "$count" = 1 ] || return 1
value=$(ini_value "$file" "$id" baseurl)
[ "${value%/}" = "${url%/}" ] && [ "$(ini_value "$file" "$id" enabled)" = 1 ] &&
[ "$(ini_value "$file" "$id" gpgcheck)" = 0 ] || return 1
for key in mirrorlist metalink; do [ -z "$(ini_value "$file" "$id" "$key")" ] || return 1; done
if [ "$id" = local-openstack-rocky ]; then
excludes=$(ini_value "$file" "$id" exclude | tr ',' ' ')
printf '%s\n' "$excludes" | awk '{for(i=1;i<=NF;i++){if($i=="sip")s=1;if($i=="PyQt4")p=1}} END{exit !(s&&p)}' || return 1
fi
}
repo_online() {
local data
data=$(curl --noproxy '*' -fsSL --connect-timeout 3 --max-time 10 --max-filesize 1048576 "$1/repodata/repomd.xml") || return 1
printf '%s\n' "$data" | grep -Eq '<repomd([[:space:]>])' && printf '%s\n' "$data" | grep -Fq '</repomd>'
}
repo_cache() {
timeout 20 yum -C --noplugins --disablerepo='*' --enablerepo="$1" --setopt="$1.skip_if_unavailable=0" list available
}
while IFS='|' read -r file id path; do
url="$REPO_BASE/$path"
check 1 "[$id] 配置" "[$id] 配置异常,检查重复项、启用源和 exclude" repo_ok "/etc/yum.repos.d/$file" "$id" "$url"
check 1 "[$id] 连通" "[$id] 索引不可访问" repo_online "$url"
check 1 "[$id] 缓存" "[$id] 缓存异常,执行 yum makecache" repo_cache "$id"
done <<'REPOLIST'
CentOS-Base.repo|local-base|vault-base
CentOS-Base.repo|local-updates|vault-updates
CentOS-Base.repo|local-extras|vault-extras
CentOS-QEMU-EV.repo|local-qemu-ev|vault-centos-qemu-ev
CentOS-OpenStack-rocky.repo|local-openstack-rocky|vault-centos-openstack-rocky
REPOLIST
section '时间同步(15分)' '节点环境准备 / 五、配置 NTP 时间同步(按提示的步骤检查)'
chrony_config() {
awk '{sub(/[#!;].*/,"")}
$1=="server" && $2=="controller" {n++; for(i=3;i<=NF;i++)if($i=="iburst")good++}
($1=="server" || $1=="pool" || $1=="peer") && !($1=="server" && $2=="controller") {bad=1}
END{exit !(n==1 && good==1 && !bad)}' /etc/chrony.conf
}
chrony_synced() {
local sources
sources=$(timeout 10 chronyc -n sources) || return 1
printf '%s\n' "$sources" | awk -v ip="$CONTROLLER_IP" '
# 时间源标记只有两个字符(如 ^*、^?、=+);排除表头的等号分隔线。
length($1)==2 && (substr($1,1,1)=="^" || substr($1,1,1)=="=") {
if($1=="^*" && $2==ip) found=1
if($2!=ip) bad=1
}
END{exit !(found && !bad)}'
}
check 2 'chrony 已安装' 'chrony 未安装(第1步:安装软件)' rpm -q chrony
check 4 'chrony 时间源配置' '时间源配置不符(第2步:修改 chrony.conf)' chrony_config
check 2 'chronyd 运行' 'chronyd 未运行(第3步:启动服务)' active_service chronyd
check 2 'chronyd 自启' 'chronyd 未设置自启(第3步:设置自启)' enabled_service chronyd
check 5 '时间已同步到 controller' '尚未仅同步到 controller(第4步:检查 chronyc sources)' chrony_synced
section 'Nova 软件包(5分)' '部署过程 / 一、安装并配置 Nova 计算组件 / 安装软件包'
check 5 'nova-compute 已安装' 'nova-compute 未安装' rpm -q openstack-nova-compute
section 'Nova 配置(30分)' '部署过程 / 一、安装并配置 Nova 计算组件 / 编辑 /etc/nova/nova.conf'
# 配置按小组评分;组内错误键附在失败提示中,不输出实际值或口令。
config_group() {
local points=$1 group=$2 key expected actual bad=''
shift 2
while [ "$#" -ge 2 ]; do
key=$1 expected=$2; shift 2
actual=$(ini_value "$CONF" "$group" "$key")
case "$key" in
enabled|use_neutron)
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
case "$actual" in 1|yes|on) actual=true;; esac;;
project_domain_name|user_domain_name)
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]');;
enabled_apis)
# APIs 的顺序无关;仅接受手册要求的这两个 API。
actual=$(ini_value "$CONF" "$group" "$key" | tr ',' '\n' | tr -d ' \t\r' | sort | tr '\n' ',')
expected='metadata,osapi_compute,';;
server_proxyclient_address)
[ "$actual" != '$my_ip' ] || actual=$(ini_value "$CONF" DEFAULT my_ip);;
auth_url|api_servers) actual=${actual%/};;
esac
if [ -z "$actual" ] || [ "$actual" != "$expected" ]; then bad="$bad $key"; fi
done
# my_ip 必须同时属于本机;即便 hosts 未完成,也不接受示例中的远端 IP。
if [ "$group" = DEFAULT ] && ! compute_mapping >/dev/null 2>&1; then bad="$bad my_ip/hosts/本机地址"; fi
check "$points" "Nova [$group]" "请核对 nova.conf 的 [$group]:${bad:-文件不可读}" test -z "$bad"
}
config_group 6 DEFAULT enabled_apis 'osapi_compute,metadata' transport_url 'rabbit://openstack:openstack@controller' my_ip "$COMPUTE_IP" use_neutron true firewall_driver nova.virt.firewall.NoopFirewallDriver
config_group 2 api auth_strategy keystone
config_group 6 keystone_authtoken auth_url http://controller:5000/v3 memcached_servers controller:11211 auth_type password project_domain_name Default user_domain_name Default project_name service username nova password nova
config_group 6 vnc enabled true server_listen 0.0.0.0 server_proxyclient_address "$COMPUTE_IP" novncproxy_base_url "http://$CONTROLLER_IP:6080/vnc_auto.html"
config_group 3 glance api_servers http://controller:9292
config_group 2 oslo_concurrency lock_path /var/lib/nova/tmp
config_group 5 placement region_name RegionOne project_domain_name Default project_name service auth_type password user_domain_name Default auth_url http://controller:5000/v3 username placement password placement
section '虚拟化类型(5分)' '部署过程 / 一、安装并配置 Nova 计算组件 / 配置硬件加速'
virtualization_ok() {
local type
[ -r /proc/cpuinfo ] && [ -r "$CONF" ] || return 1
type=$(ini_value "$CONF" libvirt virt_type)
if grep -Eq '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo; then
# Rocky 默认为 kvm;显式使用 qemu 也可运行手册的计算服务。
case "$type" in ''|kvm|qemu) return 0;; *) return 1;; esac
fi
[ "$type" = qemu ]
}
check 5 '虚拟化类型' '虚拟化类型不符:无 vmx/svm 时设置 virt_type=qemu' virtualization_ok
section '服务状态(10分)' '部署过程 / 一、安装并配置 Nova 计算组件 / 启动计算服务'
for service in libvirtd openstack-nova-compute; do
check 3 "$service 运行" "$service 未运行" active_service "$service"
check 2 "$service 自启" "$service 未设置自启" enabled_service "$service"
done
finish