diff --git a/tests_e2e/orchestrator/lib/agent_test_loader.py b/tests_e2e/orchestrator/lib/agent_test_loader.py index a1ac6c2a4..11e665c13 100644 --- a/tests_e2e/orchestrator/lib/agent_test_loader.py +++ b/tests_e2e/orchestrator/lib/agent_test_loader.py @@ -65,6 +65,8 @@ class TestSuiteInfo(object): template: str # skip test suite if the test not supposed to run on specific clouds skip_on_clouds: List[str] + # skip test suite if test suite not suppose to run on specific images + skip_on_images: List[str] def __str__(self): return self.name @@ -168,6 +170,12 @@ def _parse_image(image: str) -> str: if suite_skip_cloud not in ["AzureCloud", "AzureChinaCloud", "AzureUSGovernment"]: raise Exception(f"Invalid cloud {suite_skip_cloud} for in {suite.name}") + # if the suite specifies skip images, validate that images used in our tests + for suite_skip_image in suite.skip_on_images: + if suite_skip_image not in self.images: + raise Exception(f"Invalid image reference in test suite {suite.name}: Can't find {suite_skip_image} in images.yml") + + @staticmethod def _load_test_suites(test_suites: str) -> List[TestSuiteInfo]: # @@ -205,6 +213,8 @@ def _load_test_suite(description_file: Path) -> TestSuiteInfo: owns_vm: true install_test_agent: true template: "bvts/template.py" + skip_on_clouds: "AzureChinaCloud" + skip_on_images: "ubuntu_2004" * name - A string used to identify the test suite * tests - A list of the tests in the suite. Each test can be specified by a string (the path for its source code relative to @@ -231,7 +241,9 @@ def _load_test_suite(description_file: Path) -> TestSuiteInfo: * skip_on_clouds - [Optional; string or list of strings] If given, the test suite will be skipped in the specified cloud(e.g. "AzureCloud"). If not specified, the test suite will be executed in all the clouds that we use. This is useful if you want to skip a test suite validation in a particular cloud when certain feature is not available in that cloud. - + # skip_on_images - [Optional; string or list of strings] If given, the test suite will be skipped on the specified images or image sets(e.g. "ubuntu_2004"). + If not specified, the test suite will be executed on all the images that we use. This is useful + if you want to skip a test suite validation on a particular images or image sets when certain feature is not available on that image. """ test_suite: Dict[str, Any] = AgentTestLoader._load_file(description_file) @@ -286,6 +298,15 @@ def _load_test_suite(description_file: Path) -> TestSuiteInfo: else: test_suite_info.skip_on_clouds = [] + skip_on_images = test_suite.get("skip_on_images") + if skip_on_images is not None: + if isinstance(skip_on_images, str): + test_suite_info.skip_on_images = [skip_on_images] + else: + test_suite_info.skip_on_images = skip_on_images + else: + test_suite_info.skip_on_images = [] + return test_suite_info @staticmethod diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index b3d84a121..ad25151b5 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -140,6 +140,7 @@ def create_environment_list(self) -> List[Dict[str, Any]]: runbook_images = self._get_runbook_images(loader) skip_test_suites: List[str] = [] + skip_test_suites_images: List[str] = [] for test_suite_info in loader.test_suites: if self.runbook.cloud in test_suite_info.skip_on_clouds: skip_test_suites.append(test_suite_info.name) @@ -149,7 +150,14 @@ def create_environment_list(self) -> List[Dict[str, Any]]: else: images_info: List[VmImageInfo] = self._get_test_suite_images(test_suite_info, loader) + skip_images_info: List[VmImageInfo] = self._get_test_suite_skip_images(test_suite_info, loader) + if len(skip_images_info) > 0: + skip_test_suite_image = f"{test_suite_info.name}: {','.join([i.urn for i in skip_images_info])}" + skip_test_suites_images.append(skip_test_suite_image) + for image in images_info: + if image in skip_images_info: + continue # 'image.urn' can actually be the URL to a VHD if the runbook provided it in the 'image' parameter if self._is_vhd(image.urn): marketplace_image = "" @@ -238,6 +246,9 @@ def create_environment_list(self) -> List[Dict[str, Any]]: if len(skip_test_suites) > 0: self._log.info("Skipping test suites %s", skip_test_suites) + if len(skip_test_suites_images) > 0: + self._log.info("Skipping test suits run on images \n %s", '\n'.join([f"\t{skip}" for skip in skip_test_suites_images])) + return environments def create_existing_vm_environment(self) -> Dict[str, Any]: @@ -440,6 +451,20 @@ def _get_test_suite_images(suite: TestSuiteInfo, loader: AgentTestLoader) -> Lis unique[i.urn] = i return [v for k, v in unique.items()] + @staticmethod + def _get_test_suite_skip_images(suite: TestSuiteInfo, loader: AgentTestLoader) -> List[VmImageInfo]: + """ + Returns images that need to be skipped by the suite. + + A test suite may reference multiple image sets and sets can intersect; this method eliminates any duplicates. + """ + skip_unique: Dict[str, VmImageInfo] = {} + for image in suite.skip_on_images: + image_list = loader.images[image] + for i in image_list: + skip_unique[i.urn] = i + return [v for k, v in skip_unique.items()] + def _get_location(self, suite_info: TestSuiteInfo, image: VmImageInfo) -> str: """ Returns the location on which the test VM for the given test suite and image should be created. diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 336d22cf6..5bc48a5df 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -29,7 +29,7 @@ variable: # Test suites to execute # - name: test_suites - value: "agent_bvt, no_outbound_connections, extensions_disabled, agent_not_provisioned, fips, agent_ext_workflow, agent_status, multi_config_ext, agent_cgroups, ext_cgroups, agent_firewall, ext_telemetry_pipeline, ext_sequencing" + value: "agent_bvt, no_outbound_connections, extensions_disabled, agent_not_provisioned, fips, agent_ext_workflow, agent_status, multi_config_ext, agent_cgroups, ext_cgroups, agent_firewall, ext_telemetry_pipeline, ext_sequencing, agent_persist_firewall" # # Parameters used to create test VMs diff --git a/tests_e2e/orchestrator/scripts/agent-service b/tests_e2e/orchestrator/scripts/agent-service index d740ef8f4..5c4c7ee09 100755 --- a/tests_e2e/orchestrator/scripts/agent-service +++ b/tests_e2e/orchestrator/scripts/agent-service @@ -43,11 +43,13 @@ if command -v systemctl &> /dev/null; then service-stop() { systemctl stop $1; } service-restart() { systemctl restart $1; } service-start() { systemctl start $1; } + service-disable() { systemctl disable $1; } else service-status() { service $1 status; } service-stop() { service $1 stop; } service-restart() { service $1 restart; } service-start() { service $1 start; } + service-disable() { service $1 disable; } fi python=$(get-agent-python) @@ -83,3 +85,8 @@ if [[ "$cmd" == "status" ]]; then echo "Service status..." service-status $service_name fi + +if [[ "$cmd" == "disable" ]]; then + echo "Disabling service..." + service-disable $service_name +fi diff --git a/tests_e2e/test_suites/agent_persist_firewall.yml b/tests_e2e/test_suites/agent_persist_firewall.yml new file mode 100644 index 000000000..137f3af87 --- /dev/null +++ b/tests_e2e/test_suites/agent_persist_firewall.yml @@ -0,0 +1,19 @@ +# +# Iptable rules that agent add not persisted on reboot. So we use firewalld service if distro supports it otherwise agent creates custom service and only runs on boot before network up. +# so that attacker will not have room to contact the wireserver +# This test verifies that either of the service is active. Ensure those rules are added on boot and working as expected. +# +name: "AgentPersistFirewall" +tests: + - "agent_persist_firewall/agent_persist_firewall.py" +images: + - "endorsed" + - "endorsed-arm64" +owns_vm: true # This vm cannot be shared with other tests because it modifies the firewall rules and agent status. +# agent persist firewall service not running on flatcar distro since agent can't install custom service due to read only filesystem. +# so skipping the test run on flatcar distro. +# (2023-11-14T19:04:13.738695Z ERROR ExtHandler ExtHandler Unable to setup the persistent firewall rules: [Errno 30] Read-only file system: '/lib/systemd/system/waagent-network-setup.service) +skip_on_images: + - "flatcar" + - "flatcar_arm64" + - "debian_9" # TODO: Reboot is slow on debian_9. Need to investigate further. \ No newline at end of file diff --git a/tests_e2e/tests/agent_persist_firewall/agent_persist_firewall.py b/tests_e2e/tests/agent_persist_firewall/agent_persist_firewall.py new file mode 100644 index 000000000..5bfeb403a --- /dev/null +++ b/tests_e2e/tests/agent_persist_firewall/agent_persist_firewall.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from tests_e2e.tests.lib.agent_test import AgentVmTest +from tests_e2e.tests.lib.agent_test_context import AgentVmTestContext +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.ssh_client import SshClient + + +class AgentPersistFirewallTest(AgentVmTest): + """ + This test verifies agent setup persist firewall rules using custom network setup service or firewalld service. Ensure those rules are added on boot and working as expected. + """ + + def __init__(self, context: AgentVmTestContext): + super().__init__(context) + self._ssh_client: SshClient = self._context.create_ssh_client() + + def run(self): + self._test_setup() + # Test case 1: After test agent install, verify firewalld or network.setup is running + self._verify_persist_firewall_service_running() + # Test case 2: Perform reboot and ensure firewall rules added on boot and working as expected + self._context.vm.restart(wait_for_boot=True, ssh_client=self._ssh_client) + self._verify_persist_firewall_service_running() + self._verify_firewall_rules_on_boot("first_boot") + # Test case 3: Disable the agent(so that agent won't get started after reboot) + # perform reboot and ensure firewall rules added on boot even after agent is disabled + self._disable_agent() + self._context.vm.restart(wait_for_boot=True, ssh_client=self._ssh_client) + self._verify_persist_firewall_service_running() + self._verify_firewall_rules_on_boot("second_boot") + # Test case 4: perform firewalld rules deletion and ensure deleted rules added back to rule set after agent start + self._verify_firewall_rules_readded() + + def _test_setup(self): + log.info("Doing test setup") + self._run_remote_test(self._ssh_client, f"agent_persist_firewall-test_setup {self._context.username}", + use_sudo=True) + log.info("Successfully completed test setup\n") + + def _verify_persist_firewall_service_running(self): + log.info("Verifying persist firewall service is running") + self._run_remote_test(self._ssh_client, "agent_persist_firewall-verify_persist_firewall_service_running.py", + use_sudo=True) + log.info("Successfully verified persist firewall service is running\n") + + def _verify_firewall_rules_on_boot(self, boot_name): + log.info("Verifying firewall rules on {0}".format(boot_name)) + self._run_remote_test(self._ssh_client, f"agent_persist_firewall-verify_firewall_rules_on_boot.py --user {self._context.username} --boot_name {boot_name}", + use_sudo=True) + log.info("Successfully verified firewall rules on {0}".format(boot_name)) + + def _disable_agent(self): + log.info("Disabling agent") + self._run_remote_test(self._ssh_client, "agent-service disable", use_sudo=True) + log.info("Successfully disabled agent\n") + + def _verify_firewall_rules_readded(self): + log.info("Verifying firewall rules readded") + self._run_remote_test(self._ssh_client, "agent_persist_firewall-verify_firewalld_rules_readded.py", + use_sudo=True) + log.info("Successfully verified firewall rules readded\n") diff --git a/tests_e2e/tests/lib/firewall_helpers.py b/tests_e2e/tests/lib/firewall_helpers.py new file mode 100644 index 000000000..0e6ddd405 --- /dev/null +++ b/tests_e2e/tests/lib/firewall_helpers.py @@ -0,0 +1,209 @@ +from typing import List, Tuple + +from assertpy import fail + +from azurelinuxagent.common.future import ustr +from azurelinuxagent.common.utils import shellutil +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import retry_if_false + +WIRESERVER_ENDPOINT_FILE = '/var/lib/waagent/WireServerEndpoint' +WIRESERVER_IP = '168.63.129.16' +FIREWALL_PERIOD = 30 + +# helper methods shared by multiple tests + +class IPTableRules(object): + # -D deletes the specific rule in the iptable chain + DELETE_COMMAND = "-D" + + # -C checks if a specific rule exists + CHECK_COMMAND = "-C" + + +class FirewalldRules(object): + # checks if a specific rule exists + QUERY_PASSTHROUGH = "--query-passthrough" + + # removes a specific rule + REMOVE_PASSTHROUGH = "--remove-passthrough" + + +def get_wireserver_ip() -> str: + try: + with open(WIRESERVER_ENDPOINT_FILE, 'r') as f: + wireserver_ip = f.read() + except Exception: + wireserver_ip = WIRESERVER_IP + return wireserver_ip + + +def get_root_accept_rule_command(command: str) -> List[str]: + return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', '-m', + 'owner', + '--uid-owner', + '0', '-j', 'ACCEPT', '-w'] + + +def get_non_root_accept_rule_command(command: str) -> List[str]: + return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', + '--destination-port', '53', '-j', + 'ACCEPT', '-w'] + + +def get_non_root_drop_rule_command(command: str) -> List[str]: + return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', '-m', + 'conntrack', '--ctstate', + 'INVALID,NEW', '-j', 'DROP', '-w'] + + +def get_non_root_accept_tcp_firewalld_rule(command): + return ["firewall-cmd", "--permanent", "--direct", command, "ipv4", "-t", "security", "-A", "OUTPUT", "-d", + get_wireserver_ip(), + "-p", "tcp", "--destination-port", "53", "-j", "ACCEPT"] + + +def get_root_accept_firewalld_rule(command): + return ["firewall-cmd", "--permanent", "--direct", command, "ipv4", "-t", "security", "-A", "OUTPUT", "-d", + get_wireserver_ip(), + "-p", "tcp", "-m", "owner", "--uid-owner", "0", "-j", "ACCEPT"] + + +def get_non_root_drop_firewalld_rule(command): + return ["firewall-cmd", "--permanent", "--direct", command, "ipv4", "-t", "security", "-A", "OUTPUT", "-d", + get_wireserver_ip(), + "-p", "tcp", "-m", "conntrack", "--ctstate", "INVALID,NEW", "-j", "DROP"] + + +def execute_cmd(cmd: List[str]): + """ + Note: The shellutil.run_command return stdout if exit_code=0, otherwise returns Exception + """ + return shellutil.run_command(cmd, track_process=False) + + +def execute_cmd_return_err_code(cmd: List[str]): + """ + Note: The shellutil.run_command return err_code plus stdout/stderr + """ + try: + stdout = execute_cmd(cmd) + return 0, stdout + except Exception as error: + return -1, ustr(error) + + +def check_if_iptable_rule_is_available(full_command: List[str]) -> bool: + """ + This function is used to check if given rule is present in iptable rule set + "-C" return exit code 0 if the rule is available. + """ + exit_code, _ = execute_cmd_return_err_code(full_command) + return exit_code == 0 + + +def print_current_iptable_rules() -> None: + """ + This function prints the current iptable rules + """ + try: + cmd = ["sudo", "iptables", "-t", "security", "-L", "-nxv"] + stdout = execute_cmd(cmd) + for line in stdout.splitlines(): + log.info(str(line)) + except Exception as error: + log.warning("Error -- Failed to fetch the ip table rule set {0}".format(error)) + + +def get_all_iptable_rule_commands(command: str) -> Tuple[List[str], List[str], List[str]]: + return get_root_accept_rule_command(command), get_non_root_accept_rule_command(command), get_non_root_drop_rule_command(command) + + +def verify_all_rules_exist() -> None: + """ + This function is used to verify all the iptable rules are present in the rule set + """ + def check_all_iptables() -> bool: + root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands(IPTableRules.CHECK_COMMAND) + found: bool = check_if_iptable_rule_is_available(root_accept) and check_if_iptable_rule_is_available( + non_root_accept) and check_if_iptable_rule_is_available(non_root_drop) + return found + + log.info("Verifying all ip table rules are present in rule set") + # Agent will re-add rules within OS.EnableFirewallPeriod, So waiting that time + some buffer + found: bool = retry_if_false(check_all_iptables, attempts=2, delay=FIREWALL_PERIOD+15) + + if not found: + fail("IP table rules missing in rule set.\n Current iptable rules: {0}".format( + print_current_iptable_rules())) + + log.info("verified All ip table rules are present in rule set") + + +def firewalld_service_running(): + """ + Checks if firewalld service is running on the VM + Eg: firewall-cmd --state + > running + """ + cmd = ["firewall-cmd", "--state"] + exit_code, output = execute_cmd_return_err_code(cmd) + if exit_code != 0: + log.warning("Firewall service not running: {0}".format(output)) + return exit_code == 0 and output.rstrip() == "running" + + +def get_all_firewalld_rule_commands(command): + return get_root_accept_firewalld_rule(command), get_non_root_accept_tcp_firewalld_rule( + command), get_non_root_drop_firewalld_rule(command) + + +def check_if_firewalld_rule_is_available(command): + """ + This function is used to check if given firewalld rule is present in rule set + --query-passthrough return exit code 0 if the rule is available + """ + exit_code, _ = execute_cmd_return_err_code(command) + if exit_code == 0: + return True + return False + + +def verify_all_firewalld_rules_exist(): + """ + This function is used to verify all the firewalld rules are present in the rule set + """ + + def check_all_firewalld_rules(): + root_accept, non_root_accept, non_root_drop = get_all_firewalld_rule_commands(FirewalldRules.QUERY_PASSTHROUGH) + found = check_if_firewalld_rule_is_available(root_accept) and check_if_firewalld_rule_is_available( + non_root_accept) and check_if_firewalld_rule_is_available(non_root_drop) + return found + + log.info("Verifying all firewalld rules are present in rule set") + found = retry_if_false(check_all_firewalld_rules, attempts=2) + + if not found: + fail("Firewalld rules missing in rule set. {0}".format( + print_current_firewalld_rules())) + + print_current_firewalld_rules() + log.info("verified All firewalld rules are present in rule set") + + +def print_current_firewalld_rules(): + """ + This function prints the current firewalld rules + """ + try: + cmd = ["firewall-cmd", "--permanent", "--direct", "--get-all-passthroughs"] + exit_code, stdout = execute_cmd_return_err_code(cmd) + if exit_code != 0: + log.warning("Warning -- Failed to fetch firewalld rules with error code: %s and error: %s", exit_code, + stdout) + else: + log.info("Current firewalld rules:") + for line in stdout.splitlines(): + log.info(str(line)) + except Exception as error: + raise Exception("Error -- Failed to fetch the firewalld rule set {0}".format(error)) diff --git a/tests_e2e/tests/lib/retry.py b/tests_e2e/tests/lib/retry.py index 3996b3ba3..db0a52fcf 100644 --- a/tests_e2e/tests/lib/retry.py +++ b/tests_e2e/tests/lib/retry.py @@ -50,7 +50,8 @@ def retry_ssh_run(operation: Callable[[], Any], attempts: int, attempt_delay: in try: return operation() except CommandError as e: - retryable = e.exit_code == 255 and ("Connection timed out" in e.stderr or "Connection refused" in e.stderr) + retryable = ((e.exit_code == 255 and ("Connection timed out" in e.stderr or "Connection refused" in e.stderr)) or + "Unprivileged users are not permitted to log in yet" in e.stderr) if not retryable or i >= attempts: raise log.warning("The SSH operation failed, retrying in %s secs [Attempt %s/%s].\n%s", attempt_delay, i, attempts, e) diff --git a/tests_e2e/tests/lib/virtual_machine_client.py b/tests_e2e/tests/lib/virtual_machine_client.py index 37dcfaef1..bc38b1b35 100644 --- a/tests_e2e/tests/lib/virtual_machine_client.py +++ b/tests_e2e/tests/lib/virtual_machine_client.py @@ -174,7 +174,7 @@ def restart( return log.info("The VM has not rebooted yet. Restart time: %s. Boot time: %s", before_restart, boot_time) except CommandError as e: - if e.exit_code == 255 and "Connection refused" in str(e): + if (e.exit_code == 255 and "Connection refused" in str(e)) or "Unprivileged users are not permitted to log in yet" in str(e): log.info("VM %s is not yet accepting SSH connections", self) else: raise diff --git a/tests_e2e/tests/scripts/agent_firewall-verify_all_firewall_rules.py b/tests_e2e/tests/scripts/agent_firewall-verify_all_firewall_rules.py index 2ef8454fd..2d165bc17 100755 --- a/tests_e2e/tests/scripts/agent_firewall-verify_all_firewall_rules.py +++ b/tests_e2e/tests/scripts/agent_firewall-verify_all_firewall_rules.py @@ -22,41 +22,22 @@ import os import pwd import socket -from typing import List, Tuple +from typing import List -from assertpy import fail from azurelinuxagent.common.utils import shellutil -from azurelinuxagent.common.utils.shellutil import CommandError from azurelinuxagent.common.utils.textutil import format_exception +from tests_e2e.tests.lib.firewall_helpers import get_root_accept_rule_command, get_non_root_accept_rule_command, \ + get_non_root_drop_rule_command, print_current_iptable_rules, get_wireserver_ip, get_all_iptable_rule_commands, \ + check_if_iptable_rule_is_available, IPTableRules, verify_all_rules_exist, FIREWALL_PERIOD, execute_cmd from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.remote_test import run_remote_test import http.client as httpclient -from tests_e2e.tests.lib.retry import retry_if_false, retry +from tests_e2e.tests.lib.retry import retry ROOT_USER = 'root' -WIRESERVER_ENDPOINT_FILE = '/var/lib/waagent/WireServerEndpoint' -WIRESERVER_IP = '168.63.129.16' VERSIONS_PATH = '/?comp=versions' -FIREWALL_PERIOD = 30 - - -class FirewallRules(object): - # -D deletes the specific rule in the iptable chain - DELETE_COMMAND = "-D" - - # -C checks if a specific rule exists - CHECK_COMMAND = "-C" - - -def get_wireserver_ip() -> str: - try: - with open(WIRESERVER_ENDPOINT_FILE, 'r') as f: - wireserver_ip = f.read() - except Exception: - wireserver_ip = WIRESERVER_IP - return wireserver_ip def switch_user(user: str) -> None: @@ -71,88 +52,6 @@ def switch_user(user: str) -> None: raise Exception("Error -- failed to switch user to {0} : Failed with exception {1}".format(user, e)) -def get_root_accept_rule_command(command: str) -> List[str]: - return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', '-m', - 'owner', - '--uid-owner', - '0', '-j', 'ACCEPT', '-w'] - - -def get_non_root_accept_rule_command(command: str) -> List[str]: - return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', - '--destination-port', '53', '-j', - 'ACCEPT', '-w'] - - -def get_non_root_drop_rule_command(command: str) -> List[str]: - return ['sudo', 'iptables', '-t', 'security', command, 'OUTPUT', '-d', get_wireserver_ip(), '-p', 'tcp', '-m', - 'conntrack', '--ctstate', - 'INVALID,NEW', '-j', 'DROP', '-w'] - - -def execute_cmd(cmd: List[str]): - """ - Note: The shellutil.run_command return stdout if exit_code=0, otherwise returns commanderror - """ - try: - stdout = shellutil.run_command(cmd) - except CommandError as e: - return e.returncode, e.stdout, e.stderr - return 0, stdout, "" - - -def check_if_iptable_rule_is_available(full_command: List[str]) -> bool: - """ - This function is used to check if given rule is present in iptable rule set - "-C" return exit code 0 if the rule is available. - """ - exit_code, _, _ = execute_cmd(full_command) - if exit_code == 0: - return True - return False - - -def print_current_iptable_rules(): - """ - This function prints the current iptable rules - """ - try: - cmd = ['sudo', 'iptables', '-L', 'OUTPUT', '-t', 'security', '-nxv'] - exit_code, stdout, stderr = execute_cmd(cmd) - if exit_code != 0: - log.warning("Warning -- Failed to fetch the ip table rules with error code: %s and error: %s", exit_code, stderr) - else: - for line in stdout.splitlines(): - log.info(str(line)) - except Exception as error: - raise Exception("Error -- Failed to fetch the ip table rule set {0}".format(error)) - - -def get_all_iptable_rule_commands(command: str) -> Tuple[List[str], List[str], List[str]]: - return get_root_accept_rule_command(command), get_non_root_accept_rule_command(command), get_non_root_drop_rule_command(command) - - -def verify_all_rules_exist() -> None: - """ - This function is used to verify all the iptable rules are present in the rule set - """ - def check_all_iptables() -> bool: - root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands(FirewallRules.CHECK_COMMAND) - found: bool = check_if_iptable_rule_is_available(root_accept) and check_if_iptable_rule_is_available( - non_root_accept) and check_if_iptable_rule_is_available(non_root_drop) - return found - - log.info("-----Verifying all ip table rules are present in rule set") - # Agent will re-add rules within OS.EnableFirewallPeriod, So waiting that time + some buffer - found: bool = retry_if_false(check_all_iptables, attempts=2, delay=FIREWALL_PERIOD+15) - - if not found: - fail("IP table rules missing in rule set.\n Current iptable rules:\n {0}".format( - print_current_iptable_rules())) - - log.info("verified All ip table rules are present in rule set") - - def verify_rules_deleted_successfully(commands: List[List[str]] = None) -> None: """ This function is used to verify if provided rule or all(if not specified) iptable rules are deleted successfully. @@ -163,7 +62,7 @@ def verify_rules_deleted_successfully(commands: List[List[str]] = None) -> None: commands = [] if not commands: - root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands("-C") + root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands(IPTableRules.CHECK_COMMAND) commands.extend([root_accept, non_root_accept, non_root_drop]) # "-C" return error code 1 when not available which is expected after deletion @@ -183,7 +82,7 @@ def delete_iptable_rules(commands: List[List[str]] = None) -> None: if commands is None: commands = [] if not commands: - root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands(FirewallRules.CHECK_COMMAND) + root_accept, non_root_accept, non_root_drop = get_all_iptable_rule_commands(IPTableRules.DELETE_COMMAND) commands.extend([root_accept, non_root_accept, non_root_drop]) log.info("-----Deleting ip table rules \n %s", commands) @@ -297,10 +196,10 @@ def verify_non_root_accept_rule(): shellutil.run_command(stop_agent) # deleting non root accept rule - non_root_accept_delete_cmd = get_non_root_accept_rule_command(FirewallRules.DELETE_COMMAND) + non_root_accept_delete_cmd = get_non_root_accept_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([non_root_accept_delete_cmd]) # verifying deletion successful - non_root_accept_check_cmd = get_non_root_accept_rule_command(FirewallRules.CHECK_COMMAND) + non_root_accept_check_cmd = get_non_root_accept_rule_command(IPTableRules.CHECK_COMMAND) verify_rules_deleted_successfully([non_root_accept_check_cmd]) log.info("** Current IP table rules\n") @@ -326,7 +225,7 @@ def verify_non_root_accept_rule(): log.info("Ensuring missing rules are re-added by the running agent") # deleting non root accept rule - non_root_accept_delete_cmd = get_non_root_accept_rule_command(FirewallRules.DELETE_COMMAND) + non_root_accept_delete_cmd = get_non_root_accept_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([non_root_accept_delete_cmd]) verify_all_rules_exist() @@ -354,13 +253,13 @@ def verify_root_accept_rule(): shellutil.run_command(stop_agent) # deleting root accept rule - root_accept_delete_cmd = get_root_accept_rule_command(FirewallRules.DELETE_COMMAND) + root_accept_delete_cmd = get_root_accept_rule_command(IPTableRules.DELETE_COMMAND) # deleting drop rule too otherwise after restart, the daemon will go into loop since it cannot connect to wireserver. This would block the agent initialization - drop_delete_cmd = get_non_root_drop_rule_command(FirewallRules.DELETE_COMMAND) + drop_delete_cmd = get_non_root_drop_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([root_accept_delete_cmd, drop_delete_cmd]) # verifying deletion successful - root_accept_check_cmd = get_root_accept_rule_command(FirewallRules.CHECK_COMMAND) - drop_check_cmd = get_non_root_drop_rule_command(FirewallRules.CHECK_COMMAND) + root_accept_check_cmd = get_root_accept_rule_command(IPTableRules.CHECK_COMMAND) + drop_check_cmd = get_non_root_drop_rule_command(IPTableRules.CHECK_COMMAND) verify_rules_deleted_successfully([root_accept_check_cmd, drop_check_cmd]) log.info("** Current IP table rules\n") @@ -383,7 +282,7 @@ def verify_root_accept_rule(): log.info("Ensuring missing rules are re-added by the running agent") # deleting root accept rule - root_accept_delete_cmd = get_root_accept_rule_command(FirewallRules.DELETE_COMMAND) + root_accept_delete_cmd = get_root_accept_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([root_accept_delete_cmd]) verify_all_rules_exist() @@ -393,7 +292,7 @@ def verify_root_accept_rule(): log.info("root accept rule verified successfully\n") -def verify_non_root_dcp_rule(): +def verify_non_root_drop_rule(): """ This function verifies drop rule and make sure it is re added by agent after deletion """ @@ -407,10 +306,10 @@ def verify_non_root_dcp_rule(): shellutil.run_command(stop_agent) # deleting non root delete rule - non_root_drop_delete_cmd = get_non_root_drop_rule_command(FirewallRules.DELETE_COMMAND) + non_root_drop_delete_cmd = get_non_root_drop_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([non_root_drop_delete_cmd]) # verifying deletion successful - non_root_drop_check_cmd = get_non_root_drop_rule_command(FirewallRules.CHECK_COMMAND) + non_root_drop_check_cmd = get_non_root_drop_rule_command(IPTableRules.CHECK_COMMAND) verify_rules_deleted_successfully([non_root_drop_check_cmd]) log.info("** Current IP table rules\n") @@ -436,7 +335,7 @@ def verify_non_root_dcp_rule(): log.info("Ensuring missing rules are re-added by the running agent") # deleting non root delete rule - non_root_drop_delete_cmd = get_non_root_drop_rule_command(FirewallRules.DELETE_COMMAND) + non_root_drop_delete_cmd = get_non_root_drop_rule_command(IPTableRules.DELETE_COMMAND) delete_iptable_rules([non_root_drop_delete_cmd]) verify_all_rules_exist() @@ -462,7 +361,7 @@ def main(): verify_non_root_accept_rule() verify_root_accept_rule() - verify_non_root_dcp_rule() + verify_non_root_drop_rule() parser = argparse.ArgumentParser() diff --git a/tests_e2e/tests/scripts/agent_persist_firewall-access_wireserver b/tests_e2e/tests/scripts/agent_persist_firewall-access_wireserver new file mode 100755 index 000000000..c38e0a570 --- /dev/null +++ b/tests_e2e/tests/scripts/agent_persist_firewall-access_wireserver @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Helper script which tries to access Wireserver on system reboot. Also prints out iptable rules if non-root and still +# able to access Wireserver + +USER=$(whoami) +echo "$(date --utc +%FT%T.%3NZ): Running as user: $USER" + +function check_online +{ + ping 8.8.8.8 -c 1 -i .2 -t 30 > /dev/null 2>&1 && echo 0 || echo 1 +} + +# Check more, sleep less +MAX_CHECKS=10 +# Initial starting value for checks +CHECKS=0 +IS_ONLINE=$(check_online) + +# Loop while we're not online. +while [ "$IS_ONLINE" -eq 1 ]; do + + CHECKS=$((CHECKS + 1)) + if [ $CHECKS -gt $MAX_CHECKS ]; then + break + fi + + echo "$(date --utc +%FT%T.%3NZ): Network still not accessible" + # We're offline. Sleep for a bit, then check again + sleep 1; + IS_ONLINE=$(check_online) + +done + +if [ "$IS_ONLINE" -eq 1 ]; then + # We will never be able to get online. Kill script. + echo "Unable to connect to network, exiting now" + echo "ExitCode: 1" + exit 1 +fi + +echo "Finally online, Time: $(date --utc +%FT%T.%3NZ)" +echo "Trying to contact Wireserver as $USER to see if accessible" + +echo "" +echo "IPTables before accessing Wireserver" +sudo iptables -t security -L -nxv +echo "" + +WIRE_IP=$(cat /var/lib/waagent/WireServerEndpoint 2>/dev/null || echo '168.63.129.16' | tr -d '[:space:]') +if command -v wget >/dev/null 2>&1; then + wget --tries=3 "http://$WIRE_IP/?comp=versions" --timeout=5 -O "/tmp/wire-versions-$USER.xml" +else + curl --retry 3 --retry-delay 5 --connect-timeout 5 "http://$WIRE_IP/?comp=versions" -o "/tmp/wire-versions-$USER.xml" +fi +WIRE_EC=$? +echo "ExitCode: $WIRE_EC" + +if [[ "$USER" != "root" && "$WIRE_EC" == 0 ]]; then + echo "Wireserver should not be accessible for non-root user ($USER)" +fi + +if [[ "$USER" != "root" ]]; then +echo "" +echo "checking tcp traffic to wireserver port 53 for non-root user ($USER)" +echo -n 2>/dev/null < /dev/tcp/$WIRE_IP/53 && echo 0 || echo 1 # Establish network connection for port 53 +TCP_EC=$? +echo "TCP 53 Connection ExitCode: $TCP_EC" +fi \ No newline at end of file diff --git a/tests_e2e/tests/scripts/agent_persist_firewall-test_setup b/tests_e2e/tests/scripts/agent_persist_firewall-test_setup new file mode 100755 index 000000000..a157e58cb --- /dev/null +++ b/tests_e2e/tests/scripts/agent_persist_firewall-test_setup @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Script adds cron job on reboot to make sure iptables rules are added to allow access to Wireserver and also, enable the firewall config flag +# + +if [[ $# -ne 1 ]]; then + echo "Usage: agent_persist_firewall-test_setup " + exit 1 +fi + +echo "@reboot /home/$1/bin/agent_persist_firewall-access_wireserver > /tmp/reboot-cron-root.log 2>&1" | crontab -u root - +echo "@reboot /home/$1/bin/agent_persist_firewall-access_wireserver > /tmp/reboot-cron-$1.log 2>&1" | crontab -u $1 - +update-waagent-conf OS.EnableFirewall=y \ No newline at end of file diff --git a/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewall_rules_on_boot.py b/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewall_rules_on_boot.py new file mode 100755 index 000000000..549e368b2 --- /dev/null +++ b/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewall_rules_on_boot.py @@ -0,0 +1,176 @@ +#!/usr/bin/env pypy3 + +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script checks firewall rules are set on boot through cron job logs.And also capture the logs for debugging purposes. +# +import argparse +import os +import re +import shutil + +from assertpy import fail + +from azurelinuxagent.common.utils import shellutil +from tests_e2e.tests.lib.firewall_helpers import verify_all_rules_exist +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import retry + + +def move_cron_logs_to_var_log(): + # Move the cron logs to /var/log + log.info("Moving cron logs to /var/log for debugging purposes") + for cron_log in [ROOT_CRON_LOG, NON_ROOT_CRON_LOG, NON_ROOT_WIRE_XML, ROOT_WIRE_XML]: + try: + shutil.move(src=cron_log, dst=os.path.join("/var", "log", + "{0}.{1}".format(os.path.basename(cron_log), + BOOT_NAME))) + except Exception as e: + log.info("Unable to move cron log to /var/log; {0}".format(e)) + + +def check_wireserver_versions_file_exist(wire_version_file): + log.info("Checking wire-versions file exist: {0}".format(wire_version_file)) + if not os.path.exists(wire_version_file): + log.info("File: {0} not found".format(wire_version_file)) + return False + + if os.stat(wire_version_file).st_size > 0: + return True + + return False + + +def verify_data_in_cron_logs(cron_log, verify, err_msg): + log.info("Verifying Cron logs") + + def cron_log_checks(): + + if not os.path.exists(cron_log): + raise Exception("Cron log file not found: {0}".format(cron_log)) + with open(cron_log) as f: + cron_logs_lines = list(map(lambda _: _.strip(), f.readlines())) + if not cron_logs_lines: + raise Exception("Empty cron file, looks like cronjob didnt run") + + if any("Unable to connect to network, exiting now" in line for line in cron_logs_lines): + raise Exception("VM was unable to connect to network on startup. Skipping test validation") + + if not any("ExitCode" in line for line in cron_logs_lines): + raise Exception("Cron logs still incomplete, will try again in a minute") + + if not any(verify(line) for line in cron_logs_lines): + fail("Verification failed! (UNEXPECTED): {0}".format(err_msg)) + + log.info("Verification succeeded. Cron logs as expected") + + retry(cron_log_checks) + + +def verify_wireserver_ip_reachable_for_root(): + """ + For root logs - + Ensure the /var/log/wire-versions-root.xml is not-empty (generated by the cron job) + Ensure the exit code in the /var/log/reboot-cron-root.log file is 0 + """ + log.info("Verifying Wireserver IP is reachable from root user") + + def check_exit_code(line): + match = re.match("ExitCode:\\s(\\d+)", line) + return match is not None and int(match.groups()[0]) == 0 + + verify_data_in_cron_logs(cron_log=ROOT_CRON_LOG, verify=check_exit_code, + err_msg="Exit Code should be 0 for root based cron job!") + + if not check_wireserver_versions_file_exist(ROOT_WIRE_XML): + fail("Wire version file should not be empty for root user!") + + +def verify_wireserver_ip_unreachable_for_non_root(): + """ + For non-root - + Ensure the /tmp/wire-versions-non-root.xml is empty (generated by the cron job) + Ensure the exit code in the /tmp/reboot-cron-non-root.log file is non-0 + """ + log.info("Verifying WireServer IP is unreachable from non-root user") + + def check_exit_code(line): + match = re.match("ExitCode:\\s(\\d+)", line) + return match is not None and int(match.groups()[0]) != 0 + + verify_data_in_cron_logs(cron_log=NON_ROOT_CRON_LOG, verify=check_exit_code, + err_msg="Exit Code should be non-0 for non-root cron job!") + + if check_wireserver_versions_file_exist(NON_ROOT_WIRE_XML): + fail("Wire version file should be empty for non-root user!") + + +def verify_tcp_connection_to_wireserver_for_non_root(): + """ + For non-root - + Ensure the TCP 53 Connection exit code in the /tmp/reboot-cron-non-root.log file is 0 + """ + log.info("Verifying TCP connection to Wireserver port for non-root user") + + def check_exit_code(line): + match = re.match("TCP 53 Connection ExitCode:\\s(\\d+)", line) + return match is not None and int(match.groups()[0]) == 0 + + verify_data_in_cron_logs(cron_log=NON_ROOT_CRON_LOG, verify=check_exit_code, + err_msg="TCP 53 Connection Exit Code should be 0 for non-root cron job!") + + +def generate_svg(): + """ + This is a good to have, but not must have. Not failing tests if we're unable to generate a SVG + """ + log.info("Running systemd-analyze plot command to get the svg for boot execution order") + dest_dir = os.path.join("/var", "log", "svgs") + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + svg_name = os.path.join(dest_dir, "{0}.svg".format(BOOT_NAME)) + cmd = ["systemd-analyze plot > {0}".format(svg_name)] + err_code, stdout = shellutil.run_get_output(cmd) + if err_code != 0: + log.info("Unable to generate svg: {0}".format(stdout)) + log.info("SVG generated successfully") + + +def main(): + try: + # Verify firewall rules are set on boot through cron job logs + verify_wireserver_ip_unreachable_for_non_root() + verify_wireserver_ip_reachable_for_root() + verify_tcp_connection_to_wireserver_for_non_root() + verify_all_rules_exist() + finally: + # save the logs to /var/log to capture by collect-logs, this might be useful for debugging + move_cron_logs_to_var_log() + generate_svg() + + +parser = argparse.ArgumentParser() +parser.add_argument('-u', '--user', required=True, help="Non root user") +parser.add_argument('-bn', '--boot_name', required=True, help="Boot Name") +args = parser.parse_args() +NON_ROOT_USER = args.user +BOOT_NAME = args.boot_name +ROOT_CRON_LOG = "/tmp/reboot-cron-root.log" +NON_ROOT_CRON_LOG = f"/tmp/reboot-cron-{NON_ROOT_USER}.log" +NON_ROOT_WIRE_XML = f"/tmp/wire-versions-{NON_ROOT_USER}.xml" +ROOT_WIRE_XML = "/tmp/wire-versions-root.xml" +main() diff --git a/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewalld_rules_readded.py b/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewalld_rules_readded.py new file mode 100755 index 000000000..5cec654a1 --- /dev/null +++ b/tests_e2e/tests/scripts/agent_persist_firewall-verify_firewalld_rules_readded.py @@ -0,0 +1,170 @@ +#!/usr/bin/env pypy3 +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script deleting the firewalld rules and ensure deleted rules added back to the firewalld rule set after agent start +# + +from azurelinuxagent.common.osutil import get_osutil +from tests_e2e.tests.lib.firewall_helpers import firewalld_service_running, print_current_firewalld_rules, \ + get_non_root_accept_tcp_firewalld_rule, get_all_firewalld_rule_commands, FirewalldRules, execute_cmd, \ + check_if_firewalld_rule_is_available, verify_all_firewalld_rules_exist, get_root_accept_firewalld_rule, \ + get_non_root_drop_firewalld_rule +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import retry + + +def delete_firewalld_rules(commands=None): + """ + This function is used to delete the provided rule or all(if not specified) from the firewalld rules + """ + if commands is None: + commands = [] + if not commands: + root_accept, non_root_accept, non_root_drop = get_all_firewalld_rule_commands(FirewalldRules.REMOVE_PASSTHROUGH) + commands.extend([root_accept, non_root_accept, non_root_drop]) + + log.info("Deleting firewalld rules \n %s", commands) + + try: + cmd = None + for command in commands: + cmd = command + retry(lambda: execute_cmd(cmd=cmd), attempts=3) + except Exception as e: + raise Exception("Error -- Failed to Delete the firewalld rule set {0}".format(e)) + + log.info("Success --Deletion of firewalld rule") + + +def verify_rules_deleted_successfully(commands=None): + """ + This function is used to verify if provided rule or all(if not specified) rules are deleted successfully. + """ + log.info("Verifying requested rules deleted successfully") + + if commands is None: + commands = [] + + if not commands: + root_accept, non_root_accept, non_root_drop = get_all_firewalld_rule_commands(FirewalldRules.QUERY_PASSTHROUGH) + commands.extend([root_accept, non_root_accept, non_root_drop]) + + # "--QUERY-PASSTHROUGH" return error code 1 when not available which is expected after deletion + for command in commands: + if not check_if_firewalld_rule_is_available(command): + pass + else: + raise Exception("Deletion of firewalld rules not successful\n.Current firewalld rules:\n" + print_current_firewalld_rules()) + + log.info("firewalld rules deleted successfully \n %s", commands) + + +def verify_non_root_accept_rule(): + """ + This function verifies the non root accept rule and make sure it is re added by agent after deletion + """ + log.info("verifying non root accept rule") + agent_name = get_osutil().get_service_name() + # stop the agent, so that it won't re-add rules while checking + log.info("stop the agent, so that it won't re-add rules while checking") + cmd = ["systemctl", "stop", agent_name] + execute_cmd(cmd) + + # deleting tcp rule + accept_tcp_rule_with_delete = get_non_root_accept_tcp_firewalld_rule(FirewalldRules.REMOVE_PASSTHROUGH) + delete_firewalld_rules([accept_tcp_rule_with_delete]) + + # verifying deletion successful + accept_tcp_rule_with_check = get_non_root_accept_tcp_firewalld_rule(FirewalldRules.QUERY_PASSTHROUGH) + verify_rules_deleted_successfully([accept_tcp_rule_with_check]) + + # restart the agent to re-add the deleted rules + log.info("restart the agent to re-add the deleted rules") + cmd = ["systemctl", "restart", agent_name] + execute_cmd(cmd=cmd) + + verify_all_firewalld_rules_exist() + + +def verify_root_accept_rule(): + """ + This function verifies the root accept rule and make sure it is re added by agent after deletion + """ + log.info("Verifying root accept rule") + agent_name = get_osutil().get_service_name() + # stop the agent, so that it won't re-add rules while checking + log.info("stop the agent, so that it won't re-add rules while checking") + cmd = ["systemctl", "stop", agent_name] + execute_cmd(cmd) + + # deleting root accept rule + root_accept_rule_with_delete = get_root_accept_firewalld_rule(FirewalldRules.REMOVE_PASSTHROUGH) + delete_firewalld_rules([root_accept_rule_with_delete]) + + # verifying deletion successful + root_accept_rule_with_check = get_root_accept_firewalld_rule(FirewalldRules.QUERY_PASSTHROUGH) + verify_rules_deleted_successfully([root_accept_rule_with_check]) + + # restart the agent to re-add the deleted rules + log.info("restart the agent to re-add the deleted rules") + cmd = ["systemctl", "restart", agent_name] + execute_cmd(cmd=cmd) + + verify_all_firewalld_rules_exist() + + +def verify_non_root_drop_rule(): + """ + This function verifies drop rule and make sure it is re added by agent after deletion + """ + log.info("Verifying non root drop rule") + agent_name = get_osutil().get_service_name() + # stop the agent, so that it won't re-add rules while checking + log.info("stop the agent, so that it won't re-add rules while checking") + cmd = ["systemctl", "stop", agent_name] + execute_cmd(cmd) + + # deleting non-root drop rule + non_root_drop_with_delete = get_non_root_drop_firewalld_rule(FirewalldRules.REMOVE_PASSTHROUGH) + delete_firewalld_rules([non_root_drop_with_delete]) + + # verifying deletion successful + non_root_drop_with_check = get_non_root_drop_firewalld_rule(FirewalldRules.QUERY_PASSTHROUGH) + verify_rules_deleted_successfully([non_root_drop_with_check]) + + # restart the agent to re-add the deleted rules + log.info("restart the agent to re-add the deleted rules") + cmd = ["systemctl", "restart", agent_name] + execute_cmd(cmd=cmd) + + verify_all_firewalld_rules_exist() + + +def main(): + + if firewalld_service_running(): + log.info("Displaying current firewalld rules") + print_current_firewalld_rules() + verify_non_root_accept_rule() + verify_root_accept_rule() + verify_non_root_drop_rule() + else: + log.info("firewalld.service is not running and skipping test") + + +if __name__ == "__main__": + main() diff --git a/tests_e2e/tests/scripts/agent_persist_firewall-verify_persist_firewall_service_running.py b/tests_e2e/tests/scripts/agent_persist_firewall-verify_persist_firewall_service_running.py new file mode 100755 index 000000000..87e1e29e1 --- /dev/null +++ b/tests_e2e/tests/scripts/agent_persist_firewall-verify_persist_firewall_service_running.py @@ -0,0 +1,70 @@ +#!/usr/bin/env pypy3 + +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script verifies firewalld rules set on the vm if firewalld service is running and if it's not running, it verifies network-setup service is enabled by the agent +# +from assertpy import fail + +from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.utils import shellutil +from tests_e2e.tests.lib.firewall_helpers import execute_cmd_return_err_code, \ + firewalld_service_running, verify_all_firewalld_rules_exist +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import retry_if_false + + +def verify_network_setup_service_enabled(): + """ + Checks if network-setup service is enabled in the vm + """ + agent_name = get_osutil().get_service_name() + service_name = "{0}-network-setup.service".format(agent_name) + cmd = ["systemctl", "is-enabled", service_name] + + def op(cmd): + exit_code, output = execute_cmd_return_err_code(cmd) + return exit_code == 0 and output.rstrip() == "enabled" + + try: + status = retry_if_false(lambda: op(cmd), attempts=5, delay=30) + except Exception as e: + log.warning("Error -- while checking network.service is-enabled status {0}".format(e)) + status = False + if not status: + cmd = ["systemctl", "status", service_name] + fail("network-setup.service is not enabled!. Current status: {0}".format(shellutil.run_command(cmd))) + + log.info("network-setup.service is enabled") + + +def verify_firewall_service_running(): + log.info("Ensure test agent initialize the firewalld/network service setup") + + # Check if firewall active on the Vm + log.info("Checking if firewall service is active on the VM") + if firewalld_service_running(): + # Checking if firewalld rules are present in rule set if firewall service is active + verify_all_firewalld_rules_exist() + else: + # Checking if network-setup service is enabled if firewall service is not active + log.info("Checking if network-setup service is enabled by the agent since firewall service is not active") + verify_network_setup_service_enabled() + + +if __name__ == "__main__": + verify_firewall_service_running()