#!/bin/bash # # cpictl - Configure the Control-Program-Information (CPI) settings # # This is an internal helper script that is used by the "cpi.service" # systemd unit and the "90-cpi.rules" udev rule. # # The bash shell is really needed. Other shells have different ideas of how # bitwise operators work. # # Copyright 2017 IBM Corp. # # s390-tools is free software; you can redistribute it and/or modify # it under the terms of the MIT license. See LICENSE for details. # readonly CPI_LOCK="/var/lock/cpictl.lock" readonly PRG="${0##*/}" readonly SYSTEM_LEVEL_PATH="/sys/firmware/cpi/system_level" readonly SYSTEM_TYPE_PATH="/sys/firmware/cpi/system_type" readonly SYSTEM_NAME_PATH="/sys/firmware/cpi/system_name" readonly SYSPLEX_NAME_PATH="/sys/firmware/cpi/sysplex_name" readonly CPI_SET="/sys/firmware/cpi/set" # Location of os-release file - can be specified externally for testing purpose readonly OS_RELEASE=${CPI_OS_RELEASE:-"/etc/os-release"} declare LEVEL declare TYPE declare NAME declare SYSPLEX declare PV_GUEST declare -i CPI_PERMIT="$CPI_PERMIT_ON_PVGUEST" declare -i DRYRUN=0 # Exit codes readonly EXIT_SUCCESS=0 readonly EXIT_FAILURE=1 readonly EXIT_ARG_TOO_LONG=3 readonly EXIT_INVALID_CHARS=4 readonly EXIT_INVALID_ARGS=5 readonly EXIT_NOTCONFIGURED=6 # Distro-IDs as supported by SE/HMC firmware readonly DISTRO_GENERIC=0 readonly DISTRO_RHEL=1 readonly DISTRO_SLES=2 readonly DISTRO_UBUNTU=3 readonly DISTRO_FEDORA=4 readonly DISTRO_OPENSUSE=5 readonly DISTRO_DEBIAN=6 readonly DISTRO_RHCOS=7 print_help_and_exit() { cat < or [[[flags:]distro_id:distro_version:]kernel_version] -N, --name SYSTEM Set and commit the system name to SYSTEM -S, --sysplex SYSPLEX Set and commit the sysplex name to SYSPLEX -T, --type TYPE Set and commit OS type to TYPE -v, --version Print version information, then exit --permit-cpi Permit to send Control-Program Identification data of protected virtualization guest to the host (must be specified before any commit option). See also the important note. --commit Ignore all other options and commit any uncommitted values --dry-run Do not actually set or commit anything, but show what would be done --show Ignore all other options and show current (possibly uncommitted) values Environment variables used for the --defaults option: CPI_SYSTEM_TYPE, CPI_SYSTEM_LEVEL, CPI_SYSTEM_NAME, CPI_SYSPLEX_NAME, CPI_PERMIT_ON_PVGUEST (See also the important note.) Important: Set CPI_PERMIT_ON_PVGUEST=1 or use --permit-cpi option only if you trust the host system. Enabling these options allows the host to receive potentially sensitive Control-Program Identification (CPI) data from the protected virtualization guest, including: - system_type - system_level - sysplex_name - system_name Available bits for the --set-bit option: kvm: Indicate that system is a KVM host The maximum length for system type, system name, and sysplex name is 8 characters. The allowed characters are: [A-Z0-9 @#$] This tool should be called as root. EndHelp exit $EXIT_SUCCESS } print_version_and_exit() { cat <&2 exit $EXIT_FAILURE } fail_with() { echo "$1" >&2 echo "Try '$PRG --help' for more information." >&2 exit "${2:-$EXIT_FAILURE}" } cpi_commit() { # Commit Control-Program Identification changes on protected # virtualization guests only if it is permitted by the guest. This # prevents leakage of potentially sensitive information to untrusted # hosts. if [[ -f "/sys/firmware/uv/prot_virt_guest" ]]; then read -r PV_GUEST < "/sys/firmware/uv/prot_virt_guest" if [[ "$PV_GUEST" -eq 1 ]]; then if [[ -z "$CPI_PERMIT" ]] || [[ "$CPI_PERMIT" -ne 1 ]]; then echo "Sending CPI data from secure execution Linux guests is disabled. Use --permit-cpi to enable CPI data." >&2 exit "$EXIT_NOTCONFIGURED" fi fi fi echo 1 > "$CPI_SET" 2> /dev/null } do_length_check() { [ ${#1} -gt 8 ] && fail_with "$PRG: Specified $2 too long. The maximum length is 8 characters." $EXIT_ARG_TOO_LONG } do_character_check() { echo "$1" | grep -q -E '^[a-zA-Z0-9@#$ ]*$' || fail_with "$PRG: Invalid characters in $2. Valid characters are: A-Z0-9 @#$" $EXIT_INVALID_CHARS } cpi_set_bit() { LEVEL=$(printf '0x%x' $((LEVEL | (1 << (63 - $1)) )) ) } # # split_version - Split generic version string into array of sub-versions # # @version: Version string # @delim: Characters that delimit sub-versions in version string # @num: Number of sub-versions # # Print @num sub-versions of @version where each sub-version is delimited by # any of the characters in @delim. Print 0 in place of non-decimal or missing # sub-versions. # # Examples: # version=10 delim=. num=3 => 10 0 0 # version=4.8 delim=. num=2 => 4 8 # split_version() { local version="$1" delim="$2" num="$3" local list i subver IFS="$delim" read -r -a list <<< "$version" for (( i=0; i 2 4 0 13 # version=3.0.93_3.0.101-0.8.2_0.8.1 num=6 => 3 0 93 3 0 101 # version=4.12.14-lp150.11.4 num=5 => 4 12 14 0 11 # split_kver() { local version="$1" num="$2" local main extra # Separate extra version to handle short main version (e.g. 2.4-13) IFS="-_" read -r main extra <<< "$version" split_version "$main" "." $(( num > 3 ? 3 : num )) [[ "$num" -gt 3 ]] && split_version "$extra" ".-_" $(( num - 3 )) } # # bytes_to_word - Convert byte array to hexadecimal word # # @bytes: List of numbers representing byte values # # Print a big-endian hexadecimal representation of the word that results from # combining the specified byte values. # bytes_to_word() { printf "0x" printf "%02x" "$@" } # # get_system_level - Print system level word for specified distribution version # # @distro: Distro ID (ID value from /etc/os-release) # @ver_str: Distro version string (VERSION_ID from /etc/os-release) # @kver_str: Kernel version string (output of 'uname -r') # @flags: Optional statistics flags # # Print a 64 bit hexadecimal system level in a format as understood by firmware. # # The format is 0xabccddeeeeffgghh, where # - a=statistics flags # - b=distro id # - c=distro major version # - d=distro minor version(s) # - e=kernel sublevel 2 # - f=kernel version # - g=kernel patchlevel # - h=kernel sublevel 1 # get_system_level() { local distro="$1" ver_str="$2" kver_str="$3" flags="${4:-0}" local distro_id d_major d_minor d_minor2 local k_ver k_patchlvl k_sublvl k_sublvl2 bytes=() # Extract list of sub-version numbers from version strings read -r d_major d_minor d_minor2 <<< "$(split_version "$ver_str" "._-" 3)" read -r k_ver k_patchlvl k_sublvl k_sublvl2 <<< "$(split_kver "$kver_str" 4)" # Handle excessive sublevel numbers consistently if [[ "$k_sublvl" -gt 255 ]] ; then k_sublvl=0 fi if [[ "$k_sublvl2" -gt 65535 ]] ; then k_sublvl2=0 fi # Apply distro-specific logic case "$distro" in "rhel") distro_id=$DISTRO_RHEL ;; "sles") distro_id=$DISTRO_SLES ;; "ubuntu") distro_id=$DISTRO_UBUNTU # Encode minor and update version numbers in minor field (( d_minor=((d_minor & 0xf) * 0x10) + (d_minor2 & 0xf) )) ;; "fedora") distro_id=$DISTRO_FEDORA ;; "opensuse-leap") distro_id=$DISTRO_OPENSUSE ;; "debian") distro_id=$DISTRO_DEBIAN ;; "rhcos") distro_id=$DISTRO_RHCOS ;; *) distro_id=$DISTRO_GENERIC # Reset unsupported fields d_major=0 d_minor=0 k_sublvl2=0 ;; esac # Assemble byte data (( bytes[0] = (flags & 0xf) * 0x10 + distro_id )) (( bytes[1] = d_major )) (( bytes[2] = d_minor )) (( bytes[3] = (k_sublvl2 / 256) & 0xff )) (( bytes[4] = k_sublvl2 & 0xff )) (( bytes[5] = k_ver )) (( bytes[6] = k_patchlvl )) (( bytes[7] = k_sublvl )) # Print as single hex word bytes_to_word "${bytes[@]}" } get_distro() { local line ID="linux" VERSION_ID="0" VERSION="" update # CoreOS variants don’t have a unique ID in /etc/os-release, so use VARIANT_ID instead. # https://github.com/coreos/rhel-coreos-config/blob/main/common.yaml#L85 # https://github.com/coreos/fedora-coreos-config/blob/testing-devel/tests/kola/data/commonlib.sh#L43 local VARIANT_ID="" [[ ! -e "$OS_RELEASE" ]] && return # Only import required variables while read -r line ; do if [[ "$line" =~ ^ID= ]] || [[ "$line" =~ ^VERSION_ID= ]] || [[ "$line" =~ ^VERSION= ]] || [[ "$line" =~ ^VARIANT_ID= ]] ; then eval "$line" fi done <"$OS_RELEASE" if [[ "$ID" == "ubuntu" ]] ; then # Extract update version number only found in VERSION, e.g. # VERSION_ID="18.04" VERSION="18.04.5 LTS" update="${VERSION/*$VERSION_ID/}" update="${update%% *}" VERSION_ID="$VERSION_ID$update" fi if [[ "$ID" == "rhel" ]] && [[ "$VARIANT_ID" == "coreos" ]] ; then # CoreOS detection via VARIANT_ID (RHCOS only for now) # FCOS and SCOS may be added in the future. ID="rhcos" fi echo "$ID:$VERSION_ID" } cpi_set_oslevel() { local level="${1:-}" local flags list distro_id distro_ver kver id ver if [[ "$level" =~ ^0x ]] && ! [[ "$level" =~ : ]] ; then # Format: level=0x printf -v LEVEL "0x%016x" "$level" 2>/dev/null || fail_with "$PRG: Invalid hexadecimal number in $level" \ $EXIT_INVALID_CHARS return fi # Format: level=[[[flags:]distro_id:distro_ver:]kver] IFS=":" read -r -a list <<< "$level:" kver="${list[*]: -1: 1}" distro_ver="${list[*]: -2: 1}" distro_id="${list[*]: -3: 1}" flags="${list[*]: -4: 1}" if [[ -z "$kver" ]] ; then # Use version of currently running kernel kver="$(uname -r)" fi if [[ -z "$distro_ver" ]] || [[ -z "$distro_id" ]] ; then # Use distro ID and version from os-release file IFS=":" read -r id ver <<< "$(get_distro)" distro_id=${distro_id:-$id} distro_ver=${distro_ver:-$ver} fi if [[ -z "$flags" ]] ; then # Keep statistics flags from current system level flags=$(( (LEVEL >> 60) & 0xf )) fi LEVEL=$(get_system_level "$distro_id" "$distro_ver" "$kver" "$flags") } cpi_set_type() { TYPE="$1" do_length_check "$TYPE" "system type" do_character_check "$TYPE" "system type" } cpi_set_sysplex() { SYSPLEX="$1" do_length_check "$SYSPLEX" "sysplex name" do_character_check "$SYSPLEX" "sysplex name" } cpi_set_name() { NAME="$1" do_length_check "$NAME" "system name" do_character_check "$NAME" "system name" } # cpictl starts here if [ $# -le 0 ]; then echo "$PRG: No parameters specified" print_parse_error_and_exit fi if ! opts=$(getopt -o b:ehL:N:S:T:v \ -l set-bit:,environment,help,level:,name:,sysplex:,type:,commit,dry-run,permit-cpi,show,version \ -n "$PRG" -- "$@") ; then print_parse_error_and_exit fi # This guarantees that only one instance will be running, and will serialize # the execution of multiple instances if [[ -e "$CPI_LOCK" && ! -w "$CPI_LOCK" ]] ; then fail_with "$PRG: Cannot access lock file: $CPI_LOCK" fi if [[ ! -w "${CPI_LOCK%/*}" ]] ; then fail_with "$PRG: Cannot access lock file: $CPI_LOCK" fi exec 9<> "$CPI_LOCK" flock -x 9 # Get current values from sys/firmware read -r LEVEL < "$SYSTEM_LEVEL_PATH" read -r TYPE < "$SYSTEM_TYPE_PATH" read -r NAME < "$SYSTEM_NAME_PATH" read -r SYSPLEX < "$SYSPLEX_NAME_PATH" # Parse command line options: Use eval to remove getopt quotes eval set -- "$opts" while [ -n "$1" ]; do case "$1" in --help|-h) print_help_and_exit ;; --version|-v) print_version_and_exit ;; -b|--set-bit) case "$2" in kvm) cpi_set_bit 0 ;; *) fail_with "$PRG: Unknown bit \"$2\" for the $1 option" ;; esac shift 2 ;; -L|--level) cpi_set_oslevel "$2" shift 2 ;; -e|--environment) cpi_set_type "$CPI_SYSTEM_TYPE" cpi_set_name "$CPI_SYSTEM_NAME" cpi_set_oslevel "$CPI_SYSTEM_LEVEL" cpi_set_sysplex "$CPI_SYSPLEX_NAME" shift ;; -T|--type) cpi_set_type "$2" shift 2 ;; -S|--sysplex) cpi_set_sysplex "$2" shift 2 ;; -N|--name) cpi_set_name "$2" shift 2 ;; --show) cpi_show exit $EXIT_SUCCESS ;; --permit-cpi) CPI_PERMIT=1 shift ;; --commit) cpi_commit exit $EXIT_SUCCESS ;; --dry-run) DRYRUN=1 shift ;; --) shift break ;; *) break; ;; esac done # Unparsed options are not supported if [ $# -ne 0 ]; then fail_with "$PRG: Invalid command-line option: $*" $EXIT_INVALID_ARGS fi # Print settings for --dry-run or commit them to sysfs otherwise if [ $DRYRUN -eq 1 ]; then cat <<-EndDryrun System type: $TYPE System level: $LEVEL System name: $NAME Sysplex name: $SYSPLEX EndDryrun else echo "$LEVEL" > "$SYSTEM_LEVEL_PATH" echo "$TYPE" > "$SYSTEM_TYPE_PATH" echo "$NAME" > "$SYSTEM_NAME_PATH" echo "$SYSPLEX" > "$SYSPLEX_NAME_PATH" cpi_commit fi exit $EXIT_SUCCESS