#!/bin/sh
#
# ds-identify is configured via /etc/cloud/ds-identify.cfg
# or on the kernel command line. It takes primarily 2 inputs:
# datasource: can specify the datasource that should be used.
#     kernel command line option: ci.datasource=<dsname>
#
# policy: a string that indicates how ds-identify should operate.
#     kernel command line option: ci.di.policy=<policy>
#   The format is:
#        <mode>,found=value,maybe=value,notfound=value
#   default setting is:
#     search,found=all,maybe=all,notfound=disable
#
#   Mode:
#     disabled: disable cloud-init
#     enabled:  enable cloud-init.
#               ds-identify writes no config and just exits success.
#               the caller (cloud-init-generator) then enables cloud-init to
#               run just without any aid from ds-identify.
#     search:   determine which source or sources should be used
#               and write the result (datasource_list) to
#               /run/cloud-init/cloud.cfg
#     report:   basically 'dry run' for search.  results are still written
#               to the file, but are namespaced under the top level key
#               'di_report' Thus cloud-init is not affected, but can still
#               see the result.
#
#   found,maybe,notfound:
#      found: (default=all)
#         first: use the first found do no further checking
#         all: enable all DS_FOUND
#
#      maybe: (default=all)
#       if nothing returned 'found', then how to handle maybe.
#       no network sources are allowed to return 'maybe'.
#         all: enable all DS_MAYBE
#         none: ignore any DS_MAYBE
#
#      notfound: (default=disabled)
#         disabled: disable cloud-init
#         enabled: enable cloud-init
#
# ci.datasource.ec2.strict_id: (true|false|warn[,0-9])
#     if ec2 datasource does not strictly match,
#        return not_found if true
#        return maybe if false or warn*.
#

set -u
set -f
UNAVAILABLE="unavailable"
CR="
"
ERROR="error"
DI_ENABLED="enabled"
DI_DISABLED="disabled"

DI_DEBUG_LEVEL="${DEBUG_LEVEL:-1}"

PATH_ROOT=${PATH_ROOT:-""}
PATH_RUN=${PATH_RUN:-"${PATH_ROOT}/run"}
PATH_SYS_CLASS_DMI_ID=${PATH_SYS_CLASS_DMI_ID:-${PATH_ROOT}/sys/class/dmi/id}
PATH_SYS_HYPERVISOR=${PATH_SYS_HYPERVISOR:-${PATH_ROOT}/sys/hypervisor}
PATH_SYS_CLASS_BLOCK=${PATH_SYS_CLASS_BLOCK:-${PATH_ROOT}/sys/class/block}
PATH_DEV_DISK="${PATH_DEV_DISK:-${PATH_ROOT}/dev/disk}"
PATH_VAR_LIB_CLOUD="${PATH_VAR_LIB_CLOUD:-${PATH_ROOT}/var/lib/cloud}"
PATH_DI_CONFIG="${PATH_DI_CONFIG:-${PATH_ROOT}/etc/cloud/ds-identify.cfg}"
PATH_PROC_CMDLINE="${PATH_PROC_CMDLINE:-${PATH_ROOT}/proc/cmdline}"
PATH_PROC_1_CMDLINE="${PATH_PROC_1_CMDLINE:-${PATH_ROOT}/proc/1/cmdline}"
PATH_PROC_1_ENVIRON="${PATH_PROC_1_ENVIRON:-${PATH_ROOT}/proc/1/environ}"
PATH_PROC_UPTIME=${PATH_PROC_UPTIME:-${PATH_ROOT}/proc/uptime}
PATH_ETC_CLOUD="${PATH_ETC_CLOUD:-${PATH_ROOT}/etc/cloud}"
PATH_ETC_CI_CFG="${PATH_ETC_CI_CFG:-${PATH_ETC_CLOUD}/cloud.cfg}"
PATH_ETC_CI_CFG_D="${PATH_ETC_CI_CFG_D:-${PATH_ETC_CI_CFG}.d}"
PATH_RUN_CI="${PATH_RUN_CI:-${PATH_RUN}/cloud-init}"
PATH_RUN_CI_CFG=${PATH_RUN_CI_CFG:-${PATH_RUN_CI}/cloud.cfg}
PATH_RUN_DI_RESULT=${PATH_RUN_DI_RESULT:-${PATH_RUN_CI}/.ds-identify.result}

DI_LOG="${DI_LOG:-${PATH_RUN_CI}/ds-identify.log}"
_DI_LOGGED=""

# set DI_MAIN='noop' in environment to source this file with no main called.
DI_MAIN=${DI_MAIN:-main}

DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
DI_DMI_CHASSIS_ASSET_TAG=""
DI_DMI_PRODUCT_NAME=""
DI_DMI_SYS_VENDOR=""
DI_DMI_PRODUCT_SERIAL=""
DI_DMI_PRODUCT_UUID=""
DI_FS_LABELS=""
DI_KERNEL_CMDLINE=""
DI_VIRT=""
DI_PID_1_PRODUCT_NAME=""

DI_UNAME_KERNEL_NAME=""
DI_UNAME_KERNEL_RELEASE=""
DI_UNAME_KERNEL_VERSION=""
DI_UNAME_MACHINE=""
DI_UNAME_NODENAME=""
DI_UNAME_OPERATING_SYSTEM=""
DI_UNAME_CMD_OUT=""

DS_FOUND=0
DS_NOT_FOUND=1
DS_MAYBE=2

DI_DSNAME=""
# this has to match the builtin list in cloud-init, it is what will
# be searched if there is no setting found in config.
DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \
OVF SmartOS"
DI_DSLIST=""
DI_MODE=""
DI_ON_FOUND=""
DI_ON_MAYBE=""
DI_ON_NOTFOUND=""

DI_EC2_STRICT_ID_DEFAULT="true"

error() {
    set -- "ERROR:" "$@";
    debug 0 "$@"
    stderr "$@"
}
warn() {
    set -- "WARN:" "$@"
    debug 0 "$@"
    stderr "$@"
}

stderr() { echo "$@" 1>&2; }

debug() {
    local lvl="$1"
    shift
    [ "$lvl" -gt "${DI_DEBUG_LEVEL}" ] && return

    if [ "$_DI_LOGGED" != "$DI_LOG" ]; then
        # first time here, open file descriptor for append
        case "$DI_LOG" in
            stderr) :;;
            ?*/*)
                if [ ! -d "${DI_LOG%/*}" ]; then
                    mkdir -p "${DI_LOG%/*}" || {
                        stderr "ERROR:" "cannot write to $DI_LOG"
                        DI_LOG="stderr"
                    }
                fi
        esac
        if [ "$DI_LOG" = "stderr" ]; then
            exec 3>&2
        else
            ( exec 3>>"$DI_LOG" ) && exec 3>>"$DI_LOG" || {
                stderr "ERROR: failed writing to $DI_LOG. logging to stderr.";
                exec 3>&2
                DI_LOG="stderr"
            }
        fi
        _DI_LOGGED="$DI_LOG"
    fi
    echo "$@" 1>&3
}

get_dmi_field() {
    local path="${PATH_SYS_CLASS_DMI_ID}/$1"
    if [ ! -f "$path" ] || [ ! -r "$path" ]; then
        _RET="$UNAVAILABLE"
        return
    fi
    read _RET < "${path}" || _RET="$ERROR"
}

block_dev_with_label() {
    local p="${PATH_DEV_DISK}/by-label/$1"
    [ -b "$p" ] || return 1
    _RET=$p
    return 0
}

read_fs_labels() {
    cached "${DI_FS_LABELS}" && return 0
    # do not rely on links in /dev/disk which might not be present yet.
    # note that older blkid versions do not report DEVNAME in 'export' output.
    local out="" ret=0 oifs="$IFS" line="" delim=","
    local labels=""
    if is_container; then
        # blkid will in a container, or at least currently in lxd
        # not provide useful information.
        DI_FS_LABELS="$UNAVAILABLE:container"
    else
        out=$(blkid -c /dev/null -o export) || {
            ret=$?
            error "failed running [$ret]: blkid -c /dev/null -o export"
            return $ret
        }
        IFS="$CR"
        set -- $out
        IFS="$oifs"
        for line in "$@"; do
            case "${line}" in
                LABEL=*) labels="${labels}${line#LABEL=}${delim}";;
            esac
        done
        DI_FS_LABELS="${labels%${delim}}"
    fi
}

cached() {
    [ -n "$1" ] && _RET="$1" && return || return 1
}


has_cdrom() {
    [ -e "${PATH_ROOT}/dev/cdrom" ]
}

detect_virt() {
    local virt="${UNAVAILABLE}" r="" out=""
    if [ -d /run/systemd ]; then
        out=$(systemd-detect-virt 2>&1)
        r=$?
        if [ $r -eq 0 ] || { [ $r -ne 0 ] && [ "$out" = "none" ]; }; then
            virt="$out"
        fi
    fi
    _RET="$virt"
}

read_virt() {
    cached "$DI_VIRT" && return 0
    detect_virt
    DI_VIRT=${_RET}
}

is_container() {
    case "${DI_VIRT}" in
        lxc|lxc-libvirt|systemd-nspawn|docker|rkt) return 0;;
        *) return 1;;
    esac
}

read_kernel_cmdline() {
    cached "${DI_KERNEL_CMDLINE}" && return
    local cmdline="" fpath="${PATH_PROC_CMDLINE}"
    if is_container; then
        local p1path="${PATH_PROC_1_CMDLINE}" x=""
        cmdline="${UNAVAILABLE}:container"
        if [ -f "$p1path" ] && x=$(tr '\0' ' ' < "$p1path"); then
            cmdline=$x
        fi
    elif [ -f "$fpath" ]; then
        read cmdline <"$fpath"
    else
        cmdline="${UNAVAILABLE}:no-cmdline"
    fi
    DI_KERNEL_CMDLINE="$cmdline"
}

read_dmi_chassis_asset_tag() {
    cached "${DI_DMI_CHASSIS_ASSET_TAG}" && return
    get_dmi_field chassis_asset_tag
    DI_DMI_CHASSIS_ASSET_TAG="$_RET"
}

read_dmi_sys_vendor() {
    cached "${DI_DMI_SYS_VENDOR}" && return
    get_dmi_field sys_vendor
    DI_DMI_SYS_VENDOR="$_RET"
}

read_dmi_product_name() {
    cached "${DI_DMI_PRODUCT_NAME}" && return
    get_dmi_field product_name
    DI_DMI_PRODUCT_NAME="$_RET"
}

read_dmi_product_uuid() {
    cached "${DI_DMI_PRODUCT_UUID}" && return
    get_dmi_field product_uuid
    DI_DMI_PRODUCT_UUID="$_RET"
}

read_dmi_product_serial() {
    cached "${DI_DMI_PRODUCT_SERIAL}" && return
    get_dmi_field product_serial
    DI_DMI_PRODUCT_SERIAL="$_RET"
}

read_uname_info() {
    # run uname, and parse output.
    # uname is tricky to parse as it outputs always in a given order
    # independent of option order. kernel-version is known to have spaces.
    # 1   -s kernel-name
    # 2   -n nodename
    # 3   -r kernel-release
    # 4.. -v kernel-version(whitespace)
    # N-2 -m machine
    # N-1 -o operating-system
    cached "${DI_UNAME_CMD_OUT}" && return
    local out="${1:-}" ret=0 buf=""
    if [ -z "$out" ]; then
        out=$(uname -snrvmo) || {
            ret=$?
            error "failed reading uname with 'uname -snrvmo'"
            return $ret
        }
    fi
    set -- $out
    DI_UNAME_KERNEL_NAME="$1"
    DI_UNAME_NODENAME="$2"
    DI_UNAME_KERNEL_RELEASE="$3"
    shift 3
    while [ $# -gt 2 ]; do
        buf="$buf $1"
        shift
    done
    DI_UNAME_KERNEL_VERSION="${buf# }"
    DI_UNAME_MACHINE="$1"
    DI_UNAME_OPERATING_SYSTEM="$2"
    DI_UNAME_CMD_OUT="$out"
    return 0
}

parse_yaml_array() {
    # parse a yaml single line array value ([1,2,3], not key: [1,2,3]).
    # supported with or without leading and closing brackets
    #   ['1'] or [1]
    #   '1', '2'
    local val="$1" oifs="$IFS" ret="" tok=""
    # i386/14.04 (dash=0.5.7-4ubuntu1): the following outputs "[foo"
    #   sh -c 'n="$1"; echo ${n#[}' -- "[foo"
    # the fix was to quote the open bracket (val=${val#"["}) (LP: #1689648)
    val=${val#"["}
    val=${val%"]"}
    IFS=","; set -- $val; IFS="$oifs"
    for tok in "$@"; do
        trim "$tok"
        unquote "$_RET"
        ret="${ret} $_RET"
    done
    _RET="${ret# }"
}

read_datasource_list() {
    cached "$DI_DSLIST" && return
    local dslist=""
    # if DI_DSNAME is set directly, then avoid parsing config.
    if [ -n "${DI_DSNAME}" ]; then
        dslist="${DI_DSNAME}"
    fi

    # LP: #1582323. cc:{'datasource_list': ['name']}
    # more generically cc:<yaml>[end_cc]
    local cb="]" ob="["
    case "$DI_KERNEL_CMDLINE" in
        *cc:*datasource_list*)
            t=${DI_KERNEL_CMDLINE##*datasource_list}
            t=${t%%$cb*}
            t=${t##*$ob}
            parse_yaml_array "$t"
            dslist=${_RET}
            ;;
    esac
    if [ -z "$dslist" ] && check_config datasource_list; then
        debug 1 "$_RET_fname set datasource_list: $_RET"
        parse_yaml_array "$_RET"
        dslist=${_RET}
    fi
    if [ -z "$dslist" ]; then
        dslist=${DI_DSLIST_DEFAULT}
        debug 1 "no datasource_list found, using default:" $dslist
    fi
    DI_DSLIST=$dslist
    return 0
}

read_pid1_product_name() {
    local oifs="$IFS" out="" tok="" key="" val="" product_name="${UNAVAILABLE}"
    cached "${DI_PID_1_PRODUCT_NAME}" && return
    [ -r "${PATH_PROC_1_ENVIRON}" ] || return
    out=$(tr '\0' '\n' <"${PATH_PROC_1_ENVIRON}")
    IFS="$CR"; set -- $out; IFS="$oifs"
    for tok in "$@"; do
        key=${tok%%=*}
        [ "$key" != "$tok" ] || continue
        val=${tok#*=}
        [ "$key" = "product_name" ] && product_name="$val" && break
    done
    DI_PID_1_PRODUCT_NAME="$product_name"
}

dmi_chassis_asset_tag_matches() {
    is_container && return 1
    case "${DI_DMI_CHASSIS_ASSET_TAG}" in
        $1) return 0;;
    esac
    return 1
}

dmi_product_name_matches() {
    is_container && return 1
    case "${DI_DMI_PRODUCT_NAME}" in
        $1) return 0;;
    esac
    return 1
}

dmi_product_serial_matches() {
    is_container && return 1
    case "${DI_DMI_PRODUCT_SERIAL}" in
        $1) return 0;;
    esac
    return 1
}

dmi_sys_vendor_is() {
    is_container && return 1
    [ "${DI_DMI_SYS_VENDOR}" = "$1" ]
}

has_fs_with_label() {
    local label="$1"
    case ",${DI_FS_LABELS}," in
        *,$label,*) return 0;;
    esac
    return 1
}

nocase_equal() {
    # nocase_equal(a, b)
    # return 0 if case insenstive comparision a.lower() == b.lower()
    # different lengths
    [ "${#1}" = "${#2}" ] || return 1
    # case sensitive equal
    [ "$1" = "$2" ] && return 0

    local delim="-delim-"
    out=$(echo "$1${delim}$2" | tr A-Z a-z)
    [ "${out#*${delim}}" = "${out%${delim}*}" ]
}

check_seed_dir() {
    # check_seed_dir(name, [required])
    # check the seed dir /var/lib/cloud/seed/<name> for 'required'
    # required defaults to 'meta-data'
    local name="$1"
    local dir="${PATH_VAR_LIB_CLOUD}/seed/$name"
    [ -d "$dir" ] || return 1
    shift
    if [ $# -eq 0 ]; then
        set -- meta-data
    fi
    local f=""
    for f in "$@"; do
        [ -f "$dir/$f" ] || return 1
    done
    return 0
}

probe_floppy() {
    cached "${STATE_FLOPPY_PROBED}" && return "${STATE_FLOPPY_PROBED}"
    local fpath=/dev/floppy

    [ -b "$fpath" ] ||
        { STATE_FLOPPY_PROBED=1; return 1; }

    modprobe --use-blacklist floppy >/dev/null 2>&1 ||
        { STATE_FLOPPY_PROBED=1; return 1; }

    udevadm settle "--exit-if-exists=$fpath" ||
        { STATE_FLOPPY_PROBED=1; return 1; }

    [ -b "$fpath" ]
    STATE_FLOPPY_PROBED=$?
    return "${STATE_FLOPPY_PROBED}"
}


dscheck_CloudStack() {
    is_container && return ${DS_NOT_FOUND}
    dmi_product_name_matches "CloudStack*" && return $DS_FOUND
    return $DS_NOT_FOUND
}

dscheck_CloudSigma() {
    # http://paste.ubuntu.com/23624795/
    dmi_product_name_matches "CloudSigma" && return $DS_FOUND
    return $DS_NOT_FOUND
}

check_config() {
    # check_config(key [,file_globs])
    # somewhat hackily read through file_globs for 'key'
    # file_globs are expanded via path expansion and
    # default to /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.d/*.cfg
    # currently does not respect any hierarchy in searching for key.
    local key="$1" files=""
    shift
    if [ $# -eq 0 ]; then
        files="${PATH_ETC_CI_CFG} ${PATH_ETC_CI_CFG_D}/*.cfg"
    else
        files="$*"
    fi
    set +f; set -- $files; set -f;
    if [ "$1" = "$files" -a ! -f "$1" ]; then
        return 1
    fi
    local fname="" line="" ret="" found=0 found_fn=""
    for fname in "$@"; do
        [ -f "$fname" ] || continue
        while read line; do
            line=${line%%#*}
            case "$line" in
                $key:\ *|$key:)
                    ret=${line#*:};
                    ret=${ret# };
                    found=$((found+1))
                    found_fn="$fname";;
            esac
        done <"$fname"
    done
    if [ $found -ne 0 ]; then
        _RET="$ret"
        _RET_fname="$found_fn"
        return 0
    fi
    return 1
}

dscheck_MAAS() {
    is_container && return "${DS_NOT_FOUND}"
    # heuristic check for ephemeral boot environment
    # for maas that do not set 'ci.dsname=' in the ephemeral environment
    # these have iscsi root and cloud-config-url on the cmdline.
    local maasiqn="iqn.2004-05.com.ubuntu:maas"
    case "${DI_KERNEL_CMDLINE}" in
        *cloud-config-url=*${maasiqn}*|*${maasiqn}*cloud-config-url=*)
            return ${DS_FOUND}
            ;;
    esac

    # check config files written by maas for installed system.
    if check_config "MAAS"; then
        return "${DS_FOUND}"
    fi
    return ${DS_NOT_FOUND}
}

dscheck_NoCloud() {
    local fslabel="cidata" d=""
    case " ${DI_KERNEL_CMDLINE} " in
        *\ ds=nocloud*) return ${DS_FOUND};;
    esac
    for d in nocloud nocloud-net; do
        check_seed_dir "$d" meta-data user-data && return ${DS_FOUND}
    done
    if has_fs_with_label "${fslabel}"; then
        return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

check_configdrive_v2() {
    if has_fs_with_label "config-2"; then
        return ${DS_FOUND}
    fi
    # look in /config-drive <vlc>/seed/config_drive for a directory
    # openstack/YYYY-MM-DD format with a file meta_data.json
    local d=""
    local vlc_config_drive_path="${PATH_VAR_LIB_CLOUD}/seed/config_drive"
    for d in /config-drive $vlc_config_drive_path; do
        set +f; set -- "$d/openstack/"2???-??-??/meta_data.json; set -f;
        [ -f "$1" ] && return ${DS_FOUND}
    done
    # at least one cloud (softlayer) seeds config drive with only 'latest'.
    local lpath="openstack/latest/meta_data.json"
    if [ -e "$vlc_config_drive_path/$lpath" ]; then
        debug 1 "config drive seeded directory had only 'latest'"
        return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

check_configdrive_v1() {
    # FIXME: this has to check any file system that is vfat...
    # for now, just return not found.
    return ${DS_NOT_FOUND}
}

dscheck_ConfigDrive() {
    local ret=""
    check_configdrive_v2
    ret=$?
    [ $DS_FOUND -eq $ret ] && return $ret

    check_configdrive_v1
}

dscheck_DigitalOcean() {
    dmi_sys_vendor_is DigitalOcean && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_OpenNebula() {
    check_seed_dir opennebula && return ${DS_FOUND}
    has_fs_with_label "CONTEXT" && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

ovf_vmware_guest_customization() {
    # vmware guest customization

    # virt provider must be vmware
    [ "${DI_VIRT}" = "vmware" ] || return 1

    # we have to have the plugin to do vmware customization
    local found="" pkg="" pre="/usr/lib"
    for pkg in vmware-tools open-vm-tools; do
        if [ -f "$pre/$pkg/plugins/vmsvc/libdeployPkgPlugin.so" ]; then
            found="$pkg"; break;
        fi
    done
    [ -n "$found" ] || return 1

    # vmware customization is disabled by default
    # (disable_vmware_customization=true). If it is set to false, then
    # user has requested customization.
    local key="disable_vmware_customization"
    if check_config "$key"; then
        debug 2 "${_RET_fname} set $key to $_RET"
        case "$_RET" in
            0|false|False) return 0;;
            *) return 1;;
        esac
    fi

    return 1
}

dscheck_OVF() {
    local p=""
    check_seed_dir ovf ovf-env.xml && return "${DS_FOUND}"

    if ovf_vmware_guest_customization; then
        return ${DS_FOUND}
    fi

    has_cdrom || return ${DS_NOT_FOUND}

    # FIXME: currently just return maybe if there is a cdrom
    # ovf iso9660 transport does not specify an fs label.
    # better would be to check if
    return ${DS_MAYBE}
}

dscheck_Azure() {
    # http://paste.ubuntu.com/23630873/
    # $ grep /sr0 /run/blkid/blkid.tab
    # <device DEVNO="0x0b00" TIME="1481737655.543841"
    #  UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209"
    #  TYPE="udf">/dev/sr0</device>
    #
    local azure_chassis="7783-7084-3265-9085-8269-3286-77"
    dmi_chassis_asset_tag_matches "${azure_chassis}" && return $DS_FOUND
    check_seed_dir azure ovf-env.xml && return ${DS_FOUND}

    [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND}

    has_fs_with_label "rd_rdfe_*" && return ${DS_FOUND}

    return ${DS_NOT_FOUND}
}

dscheck_Bigstep() {
    # bigstep is activated by presense of seed file 'url'
    [ -f "${PATH_VAR_LIB_CLOUD}/data/seed/bigstep/url" ] &&
        return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

ec2_read_strict_setting() {
    # the 'strict_id' setting for Ec2 controls behavior when
    # the platform does not identify itself directly as Ec2.
    # order of precedence is:
    #  1. builtin setting here cloud-init/ds-identify builtin
    #  2. ds-identify config
    #  3. system config (/etc/cloud/cloud.cfg.d/*Ec2*.cfg)
    #  4. kernel command line (undocumented)
    #  5. user-data or vendor-data (not available here)
    local default="$1" key="ci.datasource.ec2.strict_id" val=""

    # 4. kernel command line
    case " ${DI_KERNEL_CMDLINE} " in
        *\ $key=*\ )
            val=${DI_KERNEL_CMDLINE##*$key=}
            val=${val%% *};
            _RET=${val:-$default}
            return 0
    esac

    # 3. look for the key 'strict_id' (datasource/Ec2/strict_id)
    # only in cloud.cfg or cloud.cfg.d/EC2.cfg (case insensitive)
    local cfg="${PATH_ETC_CI_CFG}" cfg_d="${PATH_ETC_CI_CFG_D}"
    if check_config strict_id $cfg "$cfg_d/*[Ee][Cc]2*.cfg"; then
        debug 2 "${_RET_fname} set strict_id to $_RET"
        return 0
    fi

    # 2. ds-identify config (datasource.ec2.strict)
    local config="${PATH_DI_CONFIG}"
    if [ -f "$config" ]; then
        if _read_config "$key" < "$config"; then
            _RET=${_RET:-$default}
            return 0
        fi
    fi

    # 1. Default
    _RET=$default
    return 0
}

ec2_identify_platform() {
    local default="$1"
    local serial="${DI_DMI_PRODUCT_SERIAL}"

    # brightbox https://bugs.launchpad.net/cloud-init/+bug/1661693
    case "$serial" in
        *brightbox.com) _RET="Brightbox"; return 0;;
    esac

    # AWS http://docs.aws.amazon.com/AWSEC2/
    #     latest/UserGuide/identify_ec2_instances.html
    local uuid="" hvuuid="${PATH_SYS_HYPERVISOR}/uuid"
    # if the (basically) xen specific /sys/hypervisor/uuid starts with 'ec2'
    if [ -r "$hvuuid" ] && read uuid < "$hvuuid" &&
        [ "${uuid#ec2}" != "$uuid" ]; then
        _RET="AWS"
        return 0
    fi

    # product uuid and product serial start with case insensitive
    local uuid="${DI_DMI_PRODUCT_UUID}"
    case "$uuid:$serial" in
        [Ee][Cc]2*:[Ee][Cc]2*)
            # both start with ec2, now check for case insenstive equal
            nocase_equal "$uuid" "$serial" &&
                { _RET="AWS"; return 0; };;
    esac

    _RET="$default"
    return 0;
}

dscheck_Ec2() {
    check_seed_dir "ec2" meta-data user-data && return ${DS_FOUND}
    is_container && return ${DS_NOT_FOUND}

    local unknown="Unknown" platform=""
    if ec2_identify_platform "$unknown"; then
        platform="$_RET"
    else
        warn "Failed to identify ec2 platform. Using '$unknown'."
        platform=$unknown
    fi

    debug 1 "ec2 platform is '$platform'."
    if [ "$platform" != "$unknown" ]; then
        return $DS_FOUND
    fi

    local default="${DI_EC2_STRICT_ID_DEFAULT}"
    if ec2_read_strict_setting "$default"; then
        strict="$_RET"
    else
        debug 1 "ec2_read_strict returned non-zero: $?. using '$default'."
        strict="$default"
    fi

    local key="datasource/Ec2/strict_id"
    case "$strict" in
        true|false|warn|warn,[0-9]*) :;;
        *)
            warn "$key was set to invalid '$strict'. using '$default'"
            strict="$default";;
    esac

    _RET_excfg="datasource: {Ec2: {strict_id: \"$strict\"}}"
    if [ "$strict" = "true" ]; then
        return $DS_NOT_FOUND
    else
        return $DS_MAYBE
    fi
}

dscheck_GCE() {
    if dmi_product_name_matches "Google Compute Engine"; then
        return ${DS_FOUND}
    fi
    # product name is not guaranteed (LP: #1674861)
    if dmi_product_serial_matches "GoogleCloud-*"; then
        return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

dscheck_OpenStack() {
    # the openstack metadata http service

    # if there is a config drive, then do not check metadata
    # FIXME: if config drive not in the search list, then we should not
    # do this check.
    check_configdrive_v2
    if [ $? -eq ${DS_FOUND} ]; then
        return ${DS_NOT_FOUND}
    fi
    local nova="OpenStack Nova" compute="OpenStack Compute"
    if dmi_product_name_matches "$nova"; then
        return ${DS_FOUND}
    fi
    if dmi_product_name_matches "$compute"; then
        # RDO installed nova (LP: #1675349).
        return ${DS_FOUND}
    fi
    if [ "${DI_PID_1_PRODUCT_NAME}" = "$nova" ]; then
        return ${DS_FOUND}
    fi

    return ${DS_NOT_FOUND}
}

dscheck_AliYun() {
    check_seed_dir "AliYun" meta-data user-data && return ${DS_FOUND}
    if dmi_product_name_matches "Alibaba Cloud ECS"; then
        return $DS_FOUND
    fi
    return $DS_NOT_FOUND
}

dscheck_AltCloud() {
    # ctype: either the dmi product name, or contents of
    #        /etc/sysconfig/cloud-info
    # if ctype == "vsphere"
    #    device = device with label 'CDROM'
    # elif ctype == "rhev"
    #    device = /dev/floppy
    # then, filesystem on that device must have
    #    user-data.txt or deltacloud-user-data.txt
    local ctype="" dev=""
    local match_rhev="[Rr][Hh][Ee][Vv]"
    local match_vsphere="[Vv][Ss][Pp][Hh][Ee][Rr][Ee]"
    local cinfo="${PATH_ROOT}/etc/sysconfig/cloud-info"
    if [ -f "$cinfo" ]; then
        read ctype < "$cinfo"
    else
        ctype="${DI_DMI_PRODUCT_NAME}"
    fi
    case "$ctype" in
        ${match_rhev})
            probe_floppy || return ${DS_NOT_FOUND}
            dev="/dev/floppy"
            ;;
        ${match_vsphere})
            block_dev_with_label CDROM || return ${DS_NOT_FOUND}
            dev="$_RET"
            ;;
        *) return ${DS_NOT_FOUND};;
    esac

    # FIXME: need to check $dev for user-data.txt or deltacloud-user-data.txt
    : "$dev"
    return $DS_MAYBE
}

dscheck_SmartOS() {
    # joyent cloud has two virt types: kvm and container
    # on kvm, product name on joyent public cloud shows 'SmartDC HVM'
    # on the container platform, uname's version has: BrandZ virtual linux
    local smartdc_kver="BrandZ virtual linux"
    dmi_product_name_matches "SmartDC*" && return $DS_FOUND
    if [ "${DI_UNAME_KERNEL_VERSION}" = "${smartdc_kver}" ] &&
       [ "${DI_VIRT}" = "container-other" ]; then
       return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

dscheck_None() {
    return ${DS_NOT_FOUND}
}

collect_info() {
    read_virt
    read_pid1_product_name
    read_kernel_cmdline
    read_uname_info
    read_config
    read_datasource_list
    read_dmi_sys_vendor
    read_dmi_chassis_asset_tag
    read_dmi_product_name
    read_dmi_product_serial
    read_dmi_product_uuid
    read_fs_labels
}

print_info() {
    collect_info
    _print_info
}

_print_info() {
    local n="" v="" vars=""
    vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL"
    vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME DMI_CHASSIS_ASSET_TAG"
    vars="$vars FS_LABELS KERNEL_CMDLINE VIRT"
    vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_RELEASE UNAME_KERNEL_VERSION"
    vars="$vars UNAME_MACHINE UNAME_NODENAME UNAME_OPERATING_SYSTEM"
    vars="$vars DSNAME DSLIST"
    vars="$vars MODE ON_FOUND ON_MAYBE ON_NOTFOUND"
    for v in ${vars}; do
        eval n='${DI_'"$v"'}'
        echo "$v=$n"
    done
    echo "pid=$$ ppid=$PPID"
    is_container && echo "is_container=true" || echo "is_container=false"
}

write_result() {
    local runcfg="${PATH_RUN_CI_CFG}" ret="" line="" pre=""
    {
        if [ "$DI_MODE" = "report" ]; then
            echo "di_report:"
            pre="  "
        fi
        for line in "$@"; do
            echo "${pre}$line";
        done
    } > "$runcfg"
    ret=$?
    [ $ret -eq 0 ] || {
        error "failed to write to ${runcfg}"
        return $ret
    }
    return 0
}

record_notfound() {
    # in report mode, report nothing was found.
    # if not report mode: only report the negative result.
    #   reporting an empty list would mean cloud-init would not search
    #   any datasources.
    if [ "$DI_MODE" = "report" ]; then
        found --
    elif [ "$DI_MODE" = "search" ]; then
        local msg="# reporting not found result. notfound=${DI_ON_NOTFOUND}."
        local DI_MODE="report"
        found -- "$msg"
    fi
}

found() {
    # found(ds1, [ds2 ...], [-- [extra lines]])
    local list="" ds=""
    while [ $# -ne 0 ]; do
        if [ "$1" = "--" ]; then
            shift
            break
        fi
        list="${list:+${list}, }$1"
        shift
    done
    if [ $# -eq 1 ] && [ -z "$1" ]; then
        # do not pass an empty line through.
        shift
    fi
    # if None is not already in the list, then add it last.
    case " $list " in
        *\ None,\ *|*\ None\ ) :;;
        *) list=${list:+${list}, None};;
    esac
    write_result "datasource_list: [ $list ]" "$@"
    return
}

trim() {
    set -- $*
    _RET="$*"
}

unquote() {
    # remove quotes from quoted value
    local quote='"' tick="'"
    local val="$1"
    case "$val" in
        ${quote}*${quote}|${tick}*${tick})
            val=${val#?}; val=${val%?};;
    esac
    _RET="$val"
}

_read_config() {
    # reads config from stdin,
    # if no parameters are set, modifies _rc scoped environment vars.
    # if keyname is provided, then returns found value of that key.
    local keyname="${1:-_unset}"
    local line="" hash="#" ckey="" key="" val=""
    while read line; do
        line=${line%%${hash}*}
        key="${line%%:*}"

        # no : in the line.
        [ "$key" = "$line" ] && continue
        trim "$key"
        key=${_RET}

        [ "$keyname" != "_unset" ] && [ "$keyname" != "$key" ] &&
            continue

        val="${line#*:}"
        trim "$val"
        unquote "${_RET}"
        val=${_RET}

        if [ "$keyname" = "$key" ]; then
            _RET="$val"
            return 0
        fi

        case "$key" in
            datasource) _rc_dsname="$val";;
            policy) _rc_policy="$val";;
        esac
    done
    if [ "$keyname" = "_unset" ]; then
        return 1
    fi
    _RET=""
    return 0
}

parse_warn() {
    echo "WARN: invalid value '$2' for key '$1'. Using $1=$3." 1>&2
}

parse_def_policy() {
    local _rc_mode="" _rc_report="" _rc_found="" _rc_maybe="" _rc_notfound=""
    local ret=""
    parse_policy "$@"
    ret=$?
    _def_mode=$_rc_mode
    _def_report=$_rc_report
    _def_found=$_rc_found
    _def_maybe=$_rc_maybe
    _def_notfound=$_rc_notfound
    return $ret
}

parse_policy() {
    # parse_policy(policy, default)
    # parse a policy string.  sets
    #   _rc_mode (enabled|disabled|search|report)
    #   _rc_report true|false
    #   _rc_found first|all
    #   _rc_maybe all|none
    #   _rc_notfound enabled|disabled
    local def=""
    case "$DI_UNAME_MACHINE" in
        # these have dmi data
        i?86|x86_64) def=${DI_DEFAULT_POLICY};;
        # aarch64 has dmi, but not currently used (LP: #1663304)
        aarch64) def=${DI_DEFAULT_POLICY_NO_DMI};;
        *) def=${DI_DEFAULT_POLICY_NO_DMI};;
    esac
    local policy="$1"
    local _def_mode="" _def_report="" _def_found="" _def_maybe=""
    local _def_notfound=""
    if [ $# -eq 1 ] || [ "$2" != "-" ]; then
        def=${2:-${def}}
        parse_def_policy "$def" -
    fi

    local mode="" report="" found="" maybe="" notfound=""
    local oifs="$IFS" tok="" val=""
    IFS=","; set -- $policy; IFS="$oifs"
    for tok in "$@"; do
        val=${tok#*=}
        case "$tok" in
            $DI_ENABLED|$DI_DISABLED|search|report) mode=$tok;;
            found=all|found=first) found=$val;;
            maybe=all|maybe=none) maybe=$val;;
            notfound=$DI_ENABLED|notfound=$DI_DISABLED) notfound=$val;;
            found=*)
               parse_warn found "$val" "${_def_found}"
               found=${_def_found};;
            maybe=*)
               parse_warn maybe "$val" "${_def_maybe}"
               maybe=${_def_maybe};;
            notfound=*)
               parse_warn notfound "$val" "${_def_notfound}"
               notfound=${_def_notfound};;
        esac
    done
    report=${report:-${_def_report:-false}}
    _rc_report=${report}
    _rc_mode=${mode:-${_def_mode}}
    _rc_found=${found:-${_def_found}}
    _rc_maybe=${maybe:-${_def_maybe}}
    _rc_notfound=${notfound:-${_def_notfound}}
}

read_config() {
    local config="${PATH_DI_CONFIG}"
    local _rc_dsname="" _rc_policy="" ret=""
    if [ -f "$config" ]; then
        _read_config < "$config"
        ret=$?
    elif [ -e "$config" ]; then
        error "$config exists but is not a file!"
        ret=1
    fi
    local tok="" key="" val=""
    for tok in ${DI_KERNEL_CMDLINE}; do
        key=${tok%%=*}
        val=${tok#*=}
        case "$key" in
            ci.ds) _rc_dsname="$val";;
            ci.datasource) _rc_dsname="$val";;
            ci.di.policy) _rc_policy="$val";;
        esac
    done

    local _rc_mode _rc_report _rc_found _rc_maybe _rc_notfound
    parse_policy "${_rc_policy}"
    debug 1 "policy loaded: mode=${_rc_mode} report=${_rc_report}" \
            "found=${_rc_found} maybe=${_rc_maybe} notfound=${_rc_notfound}"
    DI_MODE=${_rc_mode}
    DI_ON_FOUND=${_rc_found}
    DI_ON_MAYBE=${_rc_maybe}
    DI_ON_NOTFOUND=${_rc_notfound}

    DI_DSNAME="${_rc_dsname}"
    return $ret
}


manual_clean_and_existing() {
    [ -f "${PATH_VAR_LIB_CLOUD}/instance/manual-clean" ]
}

read_uptime() {
    local up idle
    _RET="${UNAVAILABLE}"
    [ -f "$PATH_PROC_UPTIME" ] &&
        read up idle < "$PATH_PROC_UPTIME" && _RET="$up"
    return
}

_main() {
    local dscheck="" ret_dis=1 ret_en=0

    read_uptime
    debug 1 "[up ${_RET}s]" "ds-identify $*"
    collect_info

    if [ "$DI_LOG" = "stderr" ]; then
        _print_info 1>&2
    else
        _print_info >> "$DI_LOG"
    fi

    case "$DI_MODE" in
        $DI_DISABLED)
            debug 1 "mode=$DI_DISABLED. returning $ret_dis"
            return $ret_dis
            ;;
        $DI_ENABLED)
            debug 1 "mode=$DI_ENABLED. returning $ret_en"
            return $ret_en;;
        search|report) :;;
    esac

    if [ -n "${DI_DSNAME}" ]; then
        debug 1 "datasource '$DI_DSNAME' specified."
        found "$DI_DSNAME"
        return
    fi

    if manual_clean_and_existing; then
        debug 1 "manual_cache_clean enabled. Not writing datasource_list."
        write_result "# manual_cache_clean."
        return
    fi

    # if there is only a single entry in $DI_DSLIST
    set -- $DI_DSLIST
    if [ $# -eq 1 ] || [ $# -eq 2 -a "$2" = "None" ] ; then
        debug 1 "single entry in datasource_list ($DI_DSLIST) use that."
        found "$@"
        return
    fi

    local found="" ret="" ds="" maybe="" _RET_excfg=""
    local exfound_cfg="" exmaybe_cfg=""
    for ds in ${DI_DSLIST}; do
        dscheck_fn="dscheck_${ds}"
        debug 2 "Checking for datasource '$ds' via '$dscheck_fn'"
        if ! type "$dscheck_fn" >/dev/null 2>&1; then
            warn "No check method '$dscheck_fn' for datasource '$ds'"
            continue
        fi
        _RET_excfg=""
        $dscheck_fn
        ret="$?"
        case "$ret" in
            $DS_FOUND)
                debug 1 "check for '$ds' returned found";
                exfound_cfg="${exfound_cfg:+${exfound_cfg}${CR}}${_RET_excfg}"
                found="${found} $ds";;
            $DS_MAYBE)
                debug 1 "check for '$ds' returned maybe";
                exmaybe_cfg="${exmaybe_cfg:+${exmaybe_cfg}${CR}}${_RET_excfg}"
                maybe="${maybe} $ds";;
            *) debug 2 "check for '$ds' returned not-found[$ret]";;
        esac
    done

    debug 2 "found=${found# } maybe=${maybe# }"
    set -- $found
    if [ $# -ne 0 ]; then
        if [ $# -eq 1 ]; then
            debug 1 "Found single datasource: $1"
        else
            # found=all
            debug 1 "Found $# datasources found=${DI_ON_FOUND}: $*"
            if [ "${DI_ON_FOUND}" = "first" ]; then
                set -- "$1"
            fi
        fi
        found "$@" -- "${exfound_cfg}"
        return
    fi

    set -- $maybe
    if [ $# -ne 0 -a "${DI_ON_MAYBE}" != "none" ]; then
        debug 1 "$# datasources returned maybe: $*"
        found "$@" -- "${exmaybe_cfg}"
        return
    fi

    # record the empty result.
    record_notfound

    local basemsg="No ds found [mode=$DI_MODE, notfound=$DI_ON_NOTFOUND]."
    local msg="" ret=3
    case "$DI_MODE:$DI_ON_NOTFOUND" in
        report:$DI_DISABLED)
            msg="$basemsg Would disable cloud-init [$ret_dis]"
            ret=$ret_en;;
        report:$DI_ENABLED)
            msg="$basemsg Would enable cloud-init [$ret_en]"
            ret=$ret_en;;
        search:$DI_DISABLED)
            msg="$basemsg Disabled cloud-init [$ret_dis]"
            ret=$ret_dis;;
        search:$DI_ENABLED)
            msg="$basemsg Enabled cloud-init [$ret_en]"
            ret=$ret_en;;
        *) error "Unexpected result";;
    esac
    debug 1 "$msg"
    return $ret
}

main() {
    local ret=""
    [ -d "$PATH_RUN_CI" ] || mkdir -p "$PATH_RUN_CI"
    if [ "${1:+$1}" != "--force" ] && [ -f "$PATH_RUN_CI_CFG" ] &&
        [ -f "$PATH_RUN_DI_RESULT" ]; then
        if read ret < "$PATH_RUN_DI_RESULT"; then
            if [ "$ret" = "0" ] || [ "$ret" = "1" ]; then
                debug 2 "used cached result $ret. pass --force to re-run."
                return $ret;
            fi
            debug 1 "previous run returned unexpected '$ret'. Re-running."
        else
            error "failed to read result from $PATH_RUN_DI_RESULT!"
        fi
    fi
    _main "$@"
    ret=$?
    echo "$ret" > "$PATH_RUN_DI_RESULT"
    read_uptime
    debug 1 "[up ${_RET}s]" "returning $ret"
    return $ret
}

noop() {
    :
}

case "${DI_MAIN}" in
    main|print_info|noop) "${DI_MAIN}" "$@";;
    *) error "unexpected value for DI_MAIN"; exit 1;;
esac

# vi: syntax=sh ts=4 expandtab