#!/bin/bash # Copyright Broadcom, Inc. All Rights Reserved. # SPDX-License-Identifier: APACHE-2.0 # # Library for operating system actions # shellcheck disable=SC1091 # Load Generic Libraries . /opt/bitnami/scripts/liblog.sh . /opt/bitnami/scripts/libfs.sh . /opt/bitnami/scripts/libvalidations.sh # Functions ######################## # Check if an user exists in the system # Arguments: # $1 - user # Returns: # Boolean ######################### user_exists() { local user="${1:?user is missing}" id "$user" >/dev/null 2>&1 } ######################## # Check if a group exists in the system # Arguments: # $1 - group # Returns: # Boolean ######################### group_exists() { local group="${1:?group is missing}" getent group "$group" >/dev/null 2>&1 } ######################## # Create a group in the system if it does not exist already # Arguments: # $1 - group # Flags: # -i|--gid - the ID for the new group # -s|--system - Whether to create new user as system user (uid <= 999) # Returns: # None ######################### ensure_group_exists() { local group="${1:?group is missing}" local gid="" local is_system_user=false # Validate arguments shift 1 while [ "$#" -gt 0 ]; do case "$1" in -i | --gid) shift gid="${1:?missing gid}" ;; -s | --system) is_system_user=true ;; *) echo "Invalid command line flag $1" >&2 return 1 ;; esac shift done if ! group_exists "$group"; then local -a args=("$group") if [[ -n "$gid" ]]; then if group_exists "$gid"; then error "The GID $gid is already in use." >&2 return 1 fi args+=("--gid" "$gid") fi $is_system_user && args+=("--system") groupadd "${args[@]}" >/dev/null 2>&1 fi } ######################## # Create an user in the system if it does not exist already # Arguments: # $1 - user # Flags: # -i|--uid - the ID for the new user # -g|--group - the group the new user should belong to # -a|--append-groups - comma-separated list of supplemental groups to append to the new user # -h|--home - the home directory for the new user # -s|--system - whether to create new user as system user (uid <= 999) # Returns: # None ######################### ensure_user_exists() { local user="${1:?user is missing}" local uid="" local group="" local append_groups="" local home="" local is_system_user=false # Validate arguments shift 1 while [ "$#" -gt 0 ]; do case "$1" in -i | --uid) shift uid="${1:?missing uid}" ;; -g | --group) shift group="${1:?missing group}" ;; -a | --append-groups) shift append_groups="${1:?missing append_groups}" ;; -h | --home) shift home="${1:?missing home directory}" ;; -s | --system) is_system_user=true ;; *) echo "Invalid command line flag $1" >&2 return 1 ;; esac shift done if ! user_exists "$user"; then local -a user_args=("-N" "$user") if [[ -n "$uid" ]]; then if user_exists "$uid"; then error "The UID $uid is already in use." return 1 fi user_args+=("--uid" "$uid") else $is_system_user && user_args+=("--system") fi useradd "${user_args[@]}" >/dev/null 2>&1 fi if [[ -n "$group" ]]; then local -a group_args=("$group") $is_system_user && group_args+=("--system") ensure_group_exists "${group_args[@]}" usermod -g "$group" "$user" >/dev/null 2>&1 fi if [[ -n "$append_groups" ]]; then local -a groups read -ra groups <<<"$(tr ',;' ' ' <<<"$append_groups")" for group in "${groups[@]}"; do ensure_group_exists "$group" usermod -aG "$group" "$user" >/dev/null 2>&1 done fi if [[ -n "$home" ]]; then mkdir -p "$home" usermod -d "$home" "$user" >/dev/null 2>&1 configure_permissions_ownership "$home" -d "775" -f "664" -u "$user" -g "$group" fi } ######################## # Check if the script is currently running as root # Arguments: # $1 - user # $2 - group # Returns: # Boolean ######################### am_i_root() { if [[ "$(id -u)" = "0" ]]; then true else false fi } ######################## # Print OS metadata # Arguments: # $1 - Flag name # Flags: # --id - Distro ID # --version - Distro version # --branch - Distro branch # --codename - Distro codename # --name - Distro name # --pretty-name - Distro pretty name # Returns: # String ######################### get_os_metadata() { local -r flag_name="${1:?missing flag}" # Helper function get_os_release_metadata() { local -r env_name="${1:?missing environment variable name}" ( . /etc/os-release echo "${!env_name}" ) } case "$flag_name" in --id) get_os_release_metadata ID ;; --version) get_os_release_metadata VERSION_ID ;; --branch) get_os_release_metadata VERSION_ID | sed 's/\..*//' ;; --codename) get_os_release_metadata VERSION_CODENAME ;; --name) get_os_release_metadata NAME ;; --pretty-name) get_os_release_metadata PRETTY_NAME ;; *) error "Unknown flag ${flag_name}" return 1 ;; esac } ######################## # Get total memory available # Arguments: # None # Returns: # Memory in bytes ######################### get_total_memory() { echo $(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024)) } ######################## # Get machine size depending on specified memory # Globals: # None # Arguments: # None # Flags: # --memory - memory size (optional) # Returns: # Detected instance size ######################### get_machine_size() { local memory="" # Validate arguments while [[ "$#" -gt 0 ]]; do case "$1" in --memory) shift memory="${1:?missing memory}" ;; *) echo "Invalid command line flag $1" >&2 return 1 ;; esac shift done if [[ -z "$memory" ]]; then debug "Memory was not specified, detecting available memory automatically" memory="$(get_total_memory)" fi sanitized_memory=$(convert_to_mb "$memory") if [[ "$sanitized_memory" -gt 26000 ]]; then echo 2xlarge elif [[ "$sanitized_memory" -gt 13000 ]]; then echo xlarge elif [[ "$sanitized_memory" -gt 6000 ]]; then echo large elif [[ "$sanitized_memory" -gt 3000 ]]; then echo medium elif [[ "$sanitized_memory" -gt 1500 ]]; then echo small else echo micro fi } ######################## # Get machine size depending on specified memory # Globals: # None # Arguments: # $1 - memory size (optional) # Returns: # Detected instance size ######################### get_supported_machine_sizes() { echo micro small medium large xlarge 2xlarge } ######################## # Convert memory size from string to amount of megabytes (i.e. 2G -> 2048) # Globals: # None # Arguments: # $1 - memory size # Returns: # Result of the conversion ######################### convert_to_mb() { local amount="${1:-}" if [[ $amount =~ ^([0-9]+)(m|M|g|G) ]]; then size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" if [[ "$unit" = "g" || "$unit" = "G" ]]; then amount="$((size * 1024))" else amount="$size" fi fi echo "$amount" } ######################### # Redirects output to /dev/null if debug mode is disabled # Globals: # BITNAMI_DEBUG # Arguments: # $@ - Command to execute # Returns: # None ######################### debug_execute() { if is_boolean_yes "${BITNAMI_DEBUG:-false}"; then "$@" else "$@" >/dev/null 2>&1 fi } ######################## # Retries a command a given number of times # Arguments: # $1 - cmd (as a string) # $2 - max retries. Default: 12 # $3 - sleep between retries (in seconds). Default: 5 # Returns: # Boolean ######################### retry_while() { local cmd="${1:?cmd is missing}" local retries="${2:-12}" local sleep_time="${3:-5}" local return_value=1 read -r -a command <<<"$cmd" for ((i = 1; i <= retries; i += 1)); do "${command[@]}" && return_value=0 && break sleep "$sleep_time" done return $return_value } ######################## # Generate a random string # Arguments: # -t|--type - String type (ascii, alphanumeric, numeric), defaults to ascii # -c|--count - Number of characters, defaults to 32 # Arguments: # None # Returns: # None # Returns: # String ######################### generate_random_string() { local type="ascii" local count="32" local filter local result # Validate arguments while [[ "$#" -gt 0 ]]; do case "$1" in -t | --type) shift type="$1" ;; -c | --count) shift count="$1" ;; *) echo "Invalid command line flag $1" >&2 return 1 ;; esac shift done # Validate type case "$type" in ascii) filter="[:print:]" ;; numeric) filter="0-9" ;; alphanumeric) filter="a-zA-Z0-9" ;; alphanumeric+special|special+alphanumeric) # Limit variety of special characters, so there is a higher chance of containing more alphanumeric characters # Special characters are harder to write, and it could impact the overall UX if most passwords are too complex filter='a-zA-Z0-9:@.,/+!=' ;; *) echo "Invalid type ${type}" >&2 return 1 ;; esac # Obtain count + 10 lines from /dev/urandom to ensure that the resulting string has the expected size # Note there is a very small chance of strings starting with EOL character # Therefore, the higher amount of lines read, this will happen less frequently result="$(head -n "$((count + 10))" /dev/urandom | tr -dc "$filter" | head -c "$count")" echo "$result" } ######################## # Create md5 hash from a string # Arguments: # $1 - string # Returns: # md5 hash - string ######################### generate_md5_hash() { local -r str="${1:?missing input string}" echo -n "$str" | md5sum | awk '{print $1}' } ######################## # Create sha1 hash from a string # Arguments: # $1 - string # $2 - algorithm - 1 (default), 224, 256, 384, 512 # Returns: # sha1 hash - string ######################### generate_sha_hash() { local -r str="${1:?missing input string}" local -r algorithm="${2:-1}" echo -n "$str" | "sha${algorithm}sum" | awk '{print $1}' } ######################## # Converts a string to its hexadecimal representation # Arguments: # $1 - string # Returns: # hexadecimal representation of the string ######################### convert_to_hex() { local -r str=${1:?missing input string} local -i iterator local char for ((iterator = 0; iterator < ${#str}; iterator++)); do char=${str:iterator:1} printf '%x' "'${char}" done } ######################## # Get boot time # Globals: # None # Arguments: # None # Returns: # Boot time metadata ######################### get_boot_time() { stat /proc --format=%Y } ######################## # Get machine ID # Globals: # None # Arguments: # None # Returns: # Machine ID ######################### get_machine_id() { local machine_id if [[ -f /etc/machine-id ]]; then machine_id="$(cat /etc/machine-id)" fi if [[ -z "$machine_id" ]]; then # Fallback to the boot-time, which will at least ensure a unique ID in the current session machine_id="$(get_boot_time)" fi echo "$machine_id" } ######################## # Get the root partition's disk device ID (e.g. /dev/sda1) # Globals: # None # Arguments: # None # Returns: # Root partition disk ID ######################### get_disk_device_id() { local device_id="" if grep -q ^/dev /proc/mounts; then device_id="$(grep ^/dev /proc/mounts | awk '$2 == "/" { print $1 }' | tail -1)" fi # If it could not be autodetected, fallback to /dev/sda1 as a default if [[ -z "$device_id" || ! -b "$device_id" ]]; then device_id="/dev/sda1" fi echo "$device_id" } ######################## # Get the root disk device ID (e.g. /dev/sda) # Globals: # None # Arguments: # None # Returns: # Root disk ID ######################### get_root_disk_device_id() { get_disk_device_id | sed -E 's/p?[0-9]+$//' } ######################## # Get the root disk size in bytes # Globals: # None # Arguments: # None # Returns: # Root disk size in bytes ######################### get_root_disk_size() { fdisk -l "$(get_root_disk_device_id)" | grep 'Disk.*bytes' | sed -E 's/.*, ([0-9]+) bytes,.*/\1/' || true } ######################## # Run command as a specific user and group (optional) # Arguments: # $1 - USER(:GROUP) to switch to # $2..$n - command to execute # Returns: # Exit code of the specified command ######################### run_as_user() { run_chroot "$@" } ######################## # Execute command as a specific user and group (optional), # replacing the current process image # Arguments: # $1 - USER(:GROUP) to switch to # $2..$n - command to execute # Returns: # Exit code of the specified command ######################### exec_as_user() { run_chroot --replace-process "$@" } ######################## # Run a command using chroot # Arguments: # $1 - USER(:GROUP) to switch to # $2..$n - command to execute # Flags: # -r | --replace-process - Replace the current process image (optional) # Returns: # Exit code of the specified command ######################### run_chroot() { local userspec local user local homedir local replace=false local -r cwd="$(pwd)" # Parse and validate flags while [[ "$#" -gt 0 ]]; do case "$1" in -r | --replace-process) replace=true ;; --) shift break ;; -*) stderr_print "unrecognized flag $1" return 1 ;; *) break ;; esac shift done # Parse and validate arguments if [[ "$#" -lt 2 ]]; then echo "expected at least 2 arguments" return 1 else userspec=$1 shift # userspec can optionally include the group, so we parse the user user=$(echo "$userspec" | cut -d':' -f1) fi if ! am_i_root; then error "Could not switch to '${userspec}': Operation not permitted" return 1 fi # Get the HOME directory for the user to switch, as chroot does # not properly update this env and some scripts rely on it homedir=$(eval echo "~${user}") if [[ ! -d $homedir ]]; then homedir="${HOME:-/}" fi # Obtaining value for "$@" indirectly in order to properly support shell parameter expansion if [[ "$replace" = true ]]; then exec chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@" else chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@" fi }