From 86c1d97b4a817ebb567544b38af4068260c382ce Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Fri, 10 Jan 2025 17:03:51 +0100 Subject: [PATCH] Added nt-discovery.sh to get information about host Ticket: ENT-12576 Signed-off-by: Victor Moene --- cf_remote/remote.py | 54 ++++++++++++++++++++++++++---------------- cf_remote/utils.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ nt-discovery.sh | 54 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- tests/test_utils.py | 47 +++++++++++++++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 21 deletions(-) create mode 100644 nt-discovery.sh create mode 100644 tests/test_utils.py diff --git a/cf_remote/remote.py b/cf_remote/remote.py index 51ef60f..e84cb3c 100644 --- a/cf_remote/remote.py +++ b/cf_remote/remote.py @@ -5,9 +5,12 @@ from collections import OrderedDict from cf_remote.utils import ( + error_and_none, os_release, column_print, + parse_envfile, pretty, + programmer_error, user_error, parse_systeminfo, parse_version, @@ -205,38 +208,49 @@ def get_info(host, *, users=None, connection=None): data["agent_version"] = parse_version(ssh_cmd(connection, version_cmd)) else: data["os"] = "unix" - data["uname"] = ssh_cmd(connection, "uname") - data["arch"] = ssh_cmd(connection, "uname -m") - data["os_release"] = os_release(ssh_cmd(connection, "cat /etc/os-release")) + + scp("nt-discovery.sh", host, connection) + discovery = parse_envfile(ssh_sudo(connection, "bash nt-discovery.sh")) + + if discovery is None: + programmer_error("Couldn't parse NT discovery file") + + data["uname"] = ( + discovery.get("NTD_UNAME") + if discovery.get("NTD_UNAME") + else error_and_none(discovery.get("NTD_UNAME_ERROR")) + ) + data["arch"] = ( + discovery.get("NTD_ARCH") + if discovery.get("NTD_ARCH") + else error_and_none(discovery.get("NTD_ARCH_ERROR")) + ) + data["os_release"] = ( + os_release(discovery.get("NTD_OS_RELEASE")) + if discovery.get("NTD_OS_RELEASE") + else error_and_none(discovery.get("NTD_OS_RELEASE_ERROR")) + ) os_release_data = data.get("os_release") redhat_release_data = None if not os_release_data: - redhat_release_data = ssh_cmd(connection, "cat /etc/redhat-release") + redhat_release_data = ( + discovery.get("NTD_REDHAT_RELEASE") + if discovery.get("NTD_REDHAT_RELEASE") + else error_and_none(discovery.get("NTD_REDHAT_RELEASE_ERROR")) + ) data["redhat_release"] = redhat_release_data data["package_tags"] = get_package_tags(os_release_data, redhat_release_data) - - data["agent_location"] = ssh_cmd(connection, "command -v cf-agent") - data["policy_server"] = ssh_cmd( - connection, "cat /var/cfengine/policy_server.dat" - ) - if user != "root" and not data["policy_server"]: - # If we are not SSHing as root and we failed to read - # the policy_server.dat file try again using sudo: - data["policy_server"] = ssh_sudo( - connection, "cat /var/cfengine/policy_server.dat" - ) - + data["agent_location"] = discovery.get("NTD_CFAGENT_PATH") + data["policy_server"] = discovery.get("NTD_POLICY_SERVER") agent = r"/var/cfengine/bin/cf-agent" data["agent"] = agent - data["agent_version"] = parse_version( - ssh_cmd(connection, "{} --version".format(agent)) - ) + data["agent_version"] = parse_version(discovery.get("NTD_CFAGENT_VERSION")) data["bin"] = {} for bin in ["dpkg", "rpm", "yum", "apt", "pkg", "zypper"]: - path = ssh_cmd(connection, "command -v {}".format(bin)) + path = discovery.get("NTD_{}".format(bin.upper())) if path: data["bin"][bin] = path diff --git a/cf_remote/utils.py b/cf_remote/utils.py index eb94a1c..0dde62f 100644 --- a/cf_remote/utils.py +++ b/cf_remote/utils.py @@ -222,3 +222,60 @@ def is_different_checksum(checksum, content): digest = hashlib.sha256(content).digest().hex() return checksum != digest + + +def error_and_none(msg): + log.error(msg) + return None + + +def parse_envfile(text): + + if not text: + return error_and_none("Missing env file") + + data = OrderedDict() + lines = text.splitlines() + for line in lines: + if line.strip() == "": + return error_and_none( + "Invalid env file format: Empty or whitespace only line" + ) + + if "=" not in line: + return error_and_none("Invalid env file format: '=' missing") + + key, _, val = line.partition("=") + + if not key: + return error_and_none("Invalid env file format: Key missing") + + if not re.fullmatch(r"([A-Z]+\_?)+", key): + return error_and_none("Invalid env file format: Invalid key") + + if not (val.startswith('"') and val.endswith('"')): + return error_and_none( + "Invalid env file format: value must start and end with double quotes" + ) + + val = val[1:-1] # Remove double quotes on each side + + if has_unescaped_character(val, '"'): + return error_and_none("Invalid env file format: quotes not escaped") + + data[key] = val.encode("utf-8").decode("unicode_escape") + + return data + + +def has_unescaped_character(string, char): + previous = None + for current in string: + if current == char and previous != "\\": + return True + previous = current + return False + + +def programmer_error(msg): + sys.exit("Programmer error: " + msg) diff --git a/nt-discovery.sh b/nt-discovery.sh new file mode 100644 index 0000000..506c677 --- /dev/null +++ b/nt-discovery.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -o pipefail + +run_command() { + # $1: command to run + # $2: variable name to store in / output + # $3: custom error message (optional) + result="$(bash -c "$1" 2>&1 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/\\n/g' | sed -e 's/\"/\\"/g')" + status=$? + if [ "$status" -eq "0" ]; then + echo "NTD_$2=\"$result\"" + else + echo "NTD_$2_CMD=\"$1\"" + # custom output result + if [ "$#" -eq "3" ]; then + echo "NTD_$2_ERROR=\"$3\"" + else + echo "NTD_$2_ERROR=\"$result\"" + fi + fi +} + +run_command "uname" "UNAME" +run_command "uname -m" "ARCH" +run_command "cat /etc/os-release" "OS_RELEASE" +run_command "cat /etc/redhat-release" "REDHAT_RELEASE" + +# cf-agent + +cfagent_path=$(command -v cf-agent) + +if ! [ $? -eq "0" ]; then + cfagent_path=$(command -v /var/cfengine/bin/cf-agent) + + if ! [ $? -eq "0" ]; then + cfagent_path="cf-agent" + fi +fi + +run_command "command -v $cfagent_path" "CFAGENT_PATH" "Cannot find cf-agent" +run_command "$cfagent_path --version" "CFAGENT_VERSION" +run_command "cat /var/cfengine/policy_server.dat" "POLICY_SERVER" + +# packages + +run_command "echo $UID" "UID" +run_command "command -v dpkg" "DPKG" "Cannot find dpkg" +run_command "command -v rpm" "RPM" "Cannot find rpm" +run_command "command -v yum" "YUM" "Cannot find yum" +run_command "command -v apt" "APT" "Cannot find apt" +run_command "command -v pkg" "PKG" "Cannot find pkg" +run_command "command -v zypper" "ZYPPER" "Cannot find zypper" + diff --git a/setup.py b/setup.py index 1a1a0cb..cec4586 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ # pip has gotten strict with version numbers # so change it to: "1.3.3+22.git.gdf81228" # See: https://peps.python.org/pep-0440/#local-version-segments - v,i,s = cf_remote_version.split("-") + v, i, s = cf_remote_version.split("-") cf_remote_version = v + "+" + i + ".git." + s assert "-" not in cf_remote_version diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..3fabcf7 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,47 @@ +from cf_remote.utils import has_unescaped_character, parse_envfile + + +def test_parse_envfile(): + data = parse_envfile('NTD_TEST="test"') + assert "NTD_TEST" in data + assert data["NTD_TEST"] == "test" + + data = parse_envfile('NTD_TEST="\\"helloworld\\""') + assert data["NTD_TEST"] == '"helloworld"' + + data = parse_envfile('NTD_TEST=""helloworld""') + assert data is None + + data = parse_envfile('NTD_TEST="\n"') + assert data is None + + data = parse_envfile('NTD_TEST="\\nhello"') + assert data["NTD_TEST"] == "\nhello" + + # 2 lines: + data = parse_envfile('NTD_TEST="test"\nNTD_HELLO="hello"') + assert len(data) == 2 + assert data["NTD_TEST"] == "test" + assert data["NTD_HELLO"] == "hello" + # Empty value is allowed: + data = parse_envfile('NTD_EMPTY=""') + assert data["NTD_EMPTY"] == "" + # Empty key is not allowed: + assert parse_envfile('="value"') is None + # Lowercase key not allowed: + assert parse_envfile('NTD_key="value"') is None + # Various cases of things which are not allowed: + assert parse_envfile('') is None + assert parse_envfile('=') is None + assert parse_envfile('""=""') is None + assert parse_envfile('=""') is None + assert parse_envfile('""=') is None + assert parse_envfile(' ') is None + assert parse_envfile('NTD_TEST="NTD_TEST_TWO="test"\\nhello"') is None + + +def test_has_unescaped_character(): + assert not has_unescaped_character(r"test", '"') + assert not has_unescaped_character(r"\"test\"", '"') + assert has_unescaped_character(r'hello"world', '"') + assert has_unescaped_character(r'hello\""world', '"')