Files
OpenStackScripts/3-5-compute.sh
T

307 lines
16 KiB
Bash
Executable File
Raw 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
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))
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(得 %d 分)%b\n' "$GREEN" "$success_message" "$points" "$NC"
else
FAIL_CNT=$((FAIL_CNT+1))
printf '%b[✘ 失败] %s(扣 %d 分)%b\n' "$RED" "$failure_message" "$points" "$NC"
fi
}
finish() {
printf '\n'
separator
printf '实验自测检查完毕!通过项: %b%d%b,失败项: %b%d%b\n' "$GREEN" "$PASS_CNT" "$NC" "$RED" "$FAIL_CNT" "$NC"
printf '%b总成绩:%d/100 分%b\n' "$YELLOW" "$SCORE" "$NC"
if [ "$TOTAL" -ne 100 ]; then
error "评分项总分异常:$TOTAL,请联系教师检查脚本。"
separator
exit 2
fi
if [ "$FAIL_CNT" -eq 0 ]; then
printf '%b所有检查项均已通过,实验自测完成!%b\n' "$GREEN" "$NC"
separator
exit 0
fi
printf '%b你有 %d 处检查未通过,请根据上方红字提示核对实验步骤并修正!%b\n' "$RED" "$FAIL_CNT" "$NC"
separator
exit 1
}
preflight() {
if [ "$EUID" -ne 0 ]; then error '请以 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() { [ "$(timeout 10 systemctl is-active firewalld 2>/dev/null)" = 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 当前为 Disabled 或 Permissive' 'SELinux 当前应为 Disabled 或 Permissive' selinux_runtime
check 2 'SELinux 持久配置为 disabled' '/etc/selinux/config 应设置 SELINUX=disabled' selinux_persistent
section '主机名、解析与网络(12分)'
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 映射唯一,控制节点 IP 配置正确' '/etc/hosts 中 controller 应唯一映射到控制节点 IP' controller_mapping
check 2 'compute 的 hosts 映射唯一,且指向本机管理 IPv4' '/etc/hosts 中 compute 应唯一映射到本机管理 IPv4' compute_mapping
check 2 'controller 和 compute 的系统解析与 hosts 配置一致' 'controller/compute 的系统解析应与 hosts 一致' both_resolve
check 2 'hosts 中保留了 localhost 的 IPv4 映射' '新增 hosts 条目时应保留 localhost 的 IPv4 映射' 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] 配置缺失、重复或遗留其他启用源(Rocky 还需 exclude=sip,PyQt4)" repo_ok "/etc/yum.repos.d/$file" "$id" "$url"
check 1 "[$id] 仓库索引可正常访问:$url" "[$id] 仓库索引不可访问:$url" repo_online "$url"
check 1 "[$id] 已有 YUM 缓存可正常读取" "[$id] 已有 YUM 缓存不可读取,请检查 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 'NTP 时间同步(15分)'
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" '
$1=="^*" && $2==ip {found=1}
$1 ~ /^[\^=]/ && $2!=ip {bad=1}
END{exit !(found && !bad)}'
}
check 2 'chrony 时间同步软件包已安装' 'chrony 软件包未安装' rpm -q chrony
check 4 'chrony 仅启用了 server controller iburst,默认外网源已注释' 'chrony 应仅启用 server controller iburst,并注释默认外网源' chrony_config
check 2 'chronyd 时间同步服务正在运行' 'chronyd 未运行' active_service chronyd
check 2 'chronyd 时间同步服务已设置持久开机自启' 'chronyd 未持久设置开机自启' enabled_service chronyd
check 5 'chronyd 已选中控制节点为同步源(^*),且未加载其他服务器源' 'chronyd 当前应选中控制节点为同步源(^*),且没有其他服务器源' chrony_synced
section 'Nova 软件包(5分)'
check 5 'openstack-nova-compute 软件包已安装' 'openstack-nova-compute 未安装' rpm -q openstack-nova-compute
section 'Nova 配置(30分)'
# 配置按小组评分;组内错误键附在失败提示中,不输出实际值或口令。
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" "[$group] Nova 配置符合实验要求" "[$group] 错误或缺失的配置键:${bad:-文件不可读};请检查 $CONF" 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分)'
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 'Nova 虚拟化类型与 CPU 硬件虚拟化能力匹配' '无 vmx/svm 时必须设置 [libvirt] virt_type=qemu;有扩展时允许默认 kvm 或 qemu' virtualization_ok
section '计算服务运行与开机自启(10分)'
for service in libvirtd openstack-nova-compute; do
check 3 "$service 服务正在运行" "$service 未运行" active_service "$service"
check 2 "$service 服务已设置持久开机自启" "$service 未持久设置开机自启" enabled_service "$service"
done
finish