327 lines
16 KiB
Bash
Executable File
327 lines
16 KiB
Bash
Executable File
#!/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
|