diff --git a/3-5-compute.sh b/3-5-compute.sh new file mode 100755 index 0000000..df94cf2 --- /dev/null +++ b/3-5-compute.sh @@ -0,0 +1,306 @@ +#!/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 '])' && printf '%s\n' "$data" | grep -Fq '' +} +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