Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block extensions disallowed by policy #3259

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c2cc2c6
Block disallowed extension processing
mgunnala Nov 8, 2024
151081d
Enable policy e2e tests
mgunnala Nov 8, 2024
edec2af
Pylint
mgunnala Nov 8, 2024
a37508f
Fix e2e test failures
mgunnala Nov 11, 2024
b0da554
Address review comments
mgunnala Nov 18, 2024
a4f5cab
Merge branch 'develop' into allowlist_2
mgunnala Nov 18, 2024
699b9ba
Address review comments
mgunnala Nov 20, 2024
86de0c5
Address test review comments
mgunnala Nov 21, 2024
c3e9b89
Remove status file for single-config
mgunnala Nov 22, 2024
65d7034
Add back status file for single-config
mgunnala Nov 22, 2024
95f247a
Run e2e tests on all endorsed
mgunnala Nov 22, 2024
3b18519
Fix UT failures
mgunnala Nov 23, 2024
63da127
Pylint
mgunnala Nov 26, 2024
471cd59
Merge branch 'develop' into allowlist_2
narrieta Nov 26, 2024
8ea989b
Address review comments for agent code
mgunnala Dec 3, 2024
83f6ff0
Tests
mgunnala Dec 3, 2024
b037e41
Revert "Tests"
mgunnala Dec 3, 2024
ba3869c
Address test comments
mgunnala Dec 6, 2024
dfcc158
Address test comments
mgunnala Dec 9, 2024
fe07ffa
Merge branch 'develop' into allowlist_2
mgunnala Dec 9, 2024
a31bdcf
Address test comments
mgunnala Dec 10, 2024
5198cf8
Cleanup existing extensions on test VMs
mgunnala Dec 12, 2024
4a0a4ef
Address comments and disable dependencies e2e tests
mgunnala Dec 16, 2024
daa8017
Merge branch 'develop' into allowlist_2
mgunnala Dec 16, 2024
bacc425
Add fixes for e2e tests
mgunnala Dec 17, 2024
3319916
Add back delete failure test case
mgunnala Dec 17, 2024
8c31798
Address comments round 3
mgunnala Dec 17, 2024
32ef5c1
Address comments
mgunnala Dec 17, 2024
f0895b7
Merge branch 'develop' into allowlist_2
mgunnala Dec 17, 2024
0c9f1c7
Pylint
mgunnala Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion azurelinuxagent/ga/exthandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_extensions, \
SupportedFeatureNames, get_supported_feature_by_name, get_agent_supported_features_list_for_crp
from azurelinuxagent.ga.cgroupconfigurator import CGroupConfigurator
from azurelinuxagent.ga.policy.policy_engine import ExtensionPolicyEngine, ExtensionPolicyError
from azurelinuxagent.common.datacontract import get_properties, set_properties
from azurelinuxagent.common.errorstate import ErrorState
from azurelinuxagent.common.event import add_event, elapsed_milliseconds, WALAEventOperation, \
Expand Down Expand Up @@ -482,10 +483,47 @@ def handle_ext_handlers(self, goal_state_id):
depends_on_err_msg = None
extensions_enabled = conf.get_extensions_enabled()

# Instantiate policy engine, and use same engine to handle all extension handlers.
narrieta marked this conversation as resolved.
Show resolved Hide resolved
# If an error is thrown during policy engine initialization, we block all extensions and report the error via handler/extension status for
mgunnala marked this conversation as resolved.
Show resolved Hide resolved
# each extension.
policy_error = None
try:
policy_engine = ExtensionPolicyEngine()
except Exception as ex:
policy_error = ex

for extension, ext_handler in all_extensions:

handler_i = ExtHandlerInstance(ext_handler, self.protocol, extension=extension)

# Invoke policy engine to determine if extension is allowed. If not, block extension and report error on
# behalf of the extension.
policy_err_map = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this is a constant... define it at the class level?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

ExtensionRequestedState.Enabled: ('enable', ExtensionErrorCodes.PluginEnableProcessingFailed),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a comment describing the elements in the tuple?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added and moved this to the class level.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'enable' and 'disable' are internal CRP/Agent operations; users are not aware of them. They should not be propagated to error messages displayed to the user

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to "run" and "uninstall"

# TODO: CRP does not currently have a terminal error code for uninstall. Once CRP adds
# an error code for uninstall or for policy, use this code instead of PluginDisableProcessingFailed
# Note that currently, CRP waits for 90 minutes to time out for a failed uninstall operation, instead of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add some more detail to this comment?
Something like:

Note that currently, CRP will poll until the agent does not report a status for an extension that should be uninstalled. In the case of a policy error, the agent will report a failed status on behalf of the extension, which will cause CRP to poll for the full timeout period, instead of failing fast.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

# failing fast.
ExtensionRequestedState.Uninstall: ('uninstall', ExtensionErrorCodes.PluginDisableProcessingFailed),
ExtensionRequestedState.Disabled: ('disable', ExtensionErrorCodes.PluginDisableProcessingFailed),
}
policy_op, policy_err_code = policy_err_map.get(ext_handler.state)
if policy_error is not None:
err = ExtensionPolicyError(msg="", inner=policy_error, code=policy_err_code)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the intention of creating an exception object here? seems like it is only used to pass the error code, but it is never raised

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially implemented the ExtensionPolicyError class to have a centralized error message for extensions blocked by policy, and also to pass the code. But you make a good point - since we never actually raise the exception, I've removed the ExtensionPolicyError class and now pass the code/message directly into the reporting function.

self.__handle_and_report_policy_error(handler_i, err, report_op=handler_i.operation, message=ustr(err),
extension=extension, report=True)
continue

extension_allowed = policy_engine.should_allow_extension(ext_handler.name)
if not extension_allowed:
msg = "failed to {0} extension '{1}' because extension is not specified in allowlist. To {0}, " \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg = "failed to {0} extension '{1}' because extension is not specified in allowlist. To {0}, " \
msg = "failed to {0} extension '{1}' because it is not specified in allowlist. To {0}, " \

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

"add extension to the allowed list in the policy file ('{2}').".format(policy_op,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"add extension to the allowed list in the policy file ('{2}').".format(policy_op,
"add the extension to the allowed list in the policy file ('{2}').".format(policy_op,

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

ext_handler.name,
conf.get_policy_file_path())
err = ExtensionPolicyError(msg, code=policy_err_code)
self.__handle_and_report_policy_error(handler_i, err, report_op=handler_i.operation, message=ustr(err),
extension=extension, report=True)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like we are missing a continue statement here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think continue statement would break the dependency logic.

It's ok to use continue in the other condition because we know all extensions will fail (dependencies don't matter)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the case where a specific extension is disallowed by policy, we should log an error for dependencies as well (using the existing code). Adding a continue statement would skip this logic.

In the case of a policy failure, where all extensions should be blocked regardless of dependencies, we can skip this logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. To make this clearer, can you do 'if not extension_allowed' after 'if depends_on_err_msg is not None'?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've updated

# In case of extensions disabled, we skip processing extensions. But CRP is still waiting for some status
# back for the skipped extensions. In order to propagate the status back to CRP, we will report status back
# here with an error message.
Expand Down Expand Up @@ -527,7 +565,11 @@ def handle_ext_handlers(self, goal_state_id):
continue

# Process extensions and get if it was successfully executed or not
extension_success = self.handle_ext_handler(handler_i, extension, goal_state_id)
# If extension was blocked by policy, treat the extension as failed and do not process the handler.
if not extension_allowed:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge this 'if not extension_allowed:' with the one just above it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, made this change, thanks!

extension_success = False
else:
extension_success = self.handle_ext_handler(handler_i, extension, goal_state_id)

dep_level = self.__get_dependency_level((extension, ext_handler))
if 0 <= dep_level < max_dep_level:
Expand Down Expand Up @@ -692,6 +734,26 @@ def __handle_and_report_ext_handler_errors(ext_handler_i, error, report_op, mess
add_event(name=name, version=handler_version, op=report_op, is_success=False, log_event=True,
message=message)

@staticmethod
def __handle_and_report_policy_error(ext_handler_i, error, report_op, message, report=True, extension=None):
maddieford marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def __handle_and_report_policy_error(ext_handler_i, error, report_op, message, report=True, extension=None):
def _report_policy_error(ext_handler_i, error, report_op, message, report=True, extension=None):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

# TODO: Consider merging this function with __handle_and_report_ext_handler_errors() above.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please leave some comment explaining why we broke this into a separate function? For policy related failures, we want to fail extensions fast. CRP will continue to poll for single-config ext status until timeout, so agent should write a status for single-config extensions. The other function does not create that status and we didn't want to touch the other function without investigating the impact of that change further

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added


# Set handler status for all extensions
ext_handler_i.set_handler_status(message=message, code=error.code)

# Create status file for only extensions with settings. Since extensions are not processed in the case of
# policy-related failures, no extension status file is created. For CRP to report status, we need to create the
# file with failure on behalf of the extension. This should be done for both multi-config and single-config extensions.
if extension is not None:
ext_handler_i.create_status_file_if_not_exist(extension, status=ExtensionStatusValue.error, code=error.code,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_status_file_if_not_exist() will not overwrite existing status file (for the current sequence number). Is this behavior acceptable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should overwrite the existing file with the policy error

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now overwrite the existing file with policy error. I've added an "overwrite" parameter and changed the function name to create_status_file( ).

operation=report_op, message=message)

if report:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would report be False?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it isn't ever false, I initially wrote it this way because I was copying the exact structure of __handle_and_report_ext_handler_errors(). But I've removed it since that parameter isn't being used for now.

name = ext_handler_i.get_extension_full_name(extension)
handler_version = ext_handler_i.ext_handler.version
add_event(name=name, version=handler_version, op=report_op, is_success=False, log_event=True,
message=message)

def handle_enable(self, ext_handler_i, extension):
"""
1- Ensure the handler is installed
Expand Down
17 changes: 10 additions & 7 deletions azurelinuxagent/ga/policy/policy_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from azurelinuxagent.common import logger
from azurelinuxagent.common.event import WALAEventOperation, add_event
from azurelinuxagent.common import conf
from azurelinuxagent.common.exception import AgentError
from azurelinuxagent.common.exception import AgentError, ExtensionError
from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import _CaseFoldedDict
from azurelinuxagent.common.utils.flexible_version import FlexibleVersion

Expand All @@ -36,12 +36,6 @@
_MAX_SUPPORTED_POLICY_VERSION = "0.1.0"


class PolicyError(AgentError):
"""
Error raised during agent policy enforcement.
"""


class InvalidPolicyError(AgentError):
"""
Error raised if user-provided policy is invalid.
Expand All @@ -51,6 +45,15 @@ def __init__(self, msg, inner=None):
super(InvalidPolicyError, self).__init__(msg, inner)


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an INFO message just after the check for enabled stating that we are using Policy? This makes clearer the fact that we are now processing policies.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__read_policy() is called right after the check for enabled, and it logs the following statement:

Policy enforcement is enabled. Enforcing policy using policy file found at '<path>'. File contents: <policy>

Is that sufficient, or do you think we need an additional log message?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's sufficient, but the message should probably be in the caller instead of read_policy. Who knows, as code evolves we may add other code before read_policy, or call read_policy multiple times.

Alternatively, the caller can log "Policy enforcement is enabled." and read_policy "Enforcing policy using policy file found at ''. File contents: "

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

class ExtensionPolicyError(ExtensionError):
"""
Error raised during agent extension policy enforcement.
"""
def __init__(self, msg, code, inner=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'code' and 'inner' parameters are not in the same order as in the base class, which can lead to subtle coding errors.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote it this way because I wanted "code" to be a required parameter in ExtensionPolicyEngine, but not "inner". But I can set a default value for "code", to keep them in the same order as in the base class.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up removing this class, based on the other comments

msg = "Extension will not be processed: {0}".format(msg)
super(ExtensionPolicyError, self).__init__(msg, inner, code)


class _PolicyEngine(object):
"""
Implements base policy engine API.
Expand Down
144 changes: 143 additions & 1 deletion tests/ga/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def test_migration_ignores_tree_remove_errors(self, shutil_mock): # pylint: dis
class TestExtensionBase(AgentTestCase):
def _assert_handler_status(self, report_vm_status, expected_status,
expected_ext_count, version,
expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None):
expected_handler_name="OSTCExtensions.ExampleHandlerLinux", expected_msg=None, expected_code=None):
self.assertTrue(report_vm_status.called)
args, kw = report_vm_status.call_args # pylint: disable=unused-variable
vm_status = args[0]
Expand All @@ -443,6 +443,9 @@ def _assert_handler_status(self, report_vm_status, expected_status,
if expected_msg is not None:
self.assertIn(expected_msg, handler_status.message)

if expected_code is not None:
self.assertEqual(expected_code, handler_status.code)


# Deprecated. New tests should be added to the TestExtension class
@patch('time.sleep', side_effect=lambda _: mock_sleep(0.001))
Expand Down Expand Up @@ -3507,5 +3510,144 @@ def test_report_msg_if_handler_manifest_contains_invalid_values(self):
self.assertIn("'supportsMultipleExtensions' has a non-boolean value", kw_messages[2]['message'])


class TestExtensionPolicy(TestExtensionBase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a test case for extension is allowed by policy

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

def setUp(self):
AgentTestCase.setUp(self)
self.policy_path = os.path.join(self.tmp_dir, "waagent_policy.json")

# Patch attributes to enable policy feature
self.patch_policy_path = patch('azurelinuxagent.common.conf.get_policy_file_path',
return_value=str(self.policy_path))
self.patch_policy_path.start()
self.patch_conf_flag = patch('azurelinuxagent.ga.policy.policy_engine.conf.get_extension_policy_enabled',
return_value=True)
self.patch_conf_flag.start()
self.maxDiff = None # When long error messages don't match, display the entire diff.

def tearDown(self):
patch.stopall()
AgentTestCase.tearDown(self)

def _create_policy_file(self, policy):
with open(self.policy_path, mode='w') as policy_file:
if isinstance(policy, dict):
json.dump(policy, policy_file, indent=4)
else:
policy_file.write(policy)
policy_file.flush()

def _test_policy_failure(self, policy, op, expected_status_code, expected_handler_status,
expected_status_msg=None):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add some comments explaining the setup done by this function? (e.g. why incarnation 2?) thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comments explaining the setup here.

Thanks to @maddieford for helping me figure out why updating the incarnation is required :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. I'd suggest instead adding a comment "Generate a new mock goal state to uninstall the extension" just before

            protocol.mock_wire_data.set_incarnation(2)
            protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall)
            protocol.client.update_goal_state()

with mock_wire_protocol(wire_protocol_data.DATA_FILE) as protocol:
if op == ExtensionRequestedState.Uninstall:
protocol.mock_wire_data.set_incarnation(2)
protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall)
protocol.client.update_goal_state()
protocol.aggregate_status = None
protocol.report_vm_status = MagicMock()
exthandlers_handler = get_exthandlers_handler(protocol)

self._create_policy_file(policy)
exthandlers_handler.run()
exthandlers_handler.report_ext_handlers_status()

report_vm_status = protocol.report_vm_status
self.assertTrue(report_vm_status.called)
self._assert_handler_status(report_vm_status, expected_handler_status, 0, "1.0.0", 'OSTCExtensions.ExampleHandlerLinux',
expected_msg=expected_status_msg, expected_code=expected_status_code)

def test_should_fail_enable_if_extension_disallowed(self):
policy = \
{
"policyVersion": "0.1.0",
"extensionPolicies": {
"allowListedExtensionsOnly": True,
}
}
expected_msg = "failed to enable extension 'OSTCExtensions.ExampleHandlerLinux' because extension is not specified in allowlist."
self._test_policy_failure(policy=policy, op=ExtensionRequestedState.Enabled, expected_status_code=ExtensionErrorCodes.PluginEnableProcessingFailed,
expected_handler_status='NotReady', expected_status_msg=expected_msg)

def test_should_fail_enable_for_invalid_policy(self):
policy = \
{
"policyVersion": "0.1.0",
"extensionPolicies": {
"allowListedExtensionsOnly": "False"
}
}
expected_msg = "attribute 'extensionPolicies.allowListedExtensionsOnly'; must be 'boolean'"
self._test_policy_failure(policy=policy, op=ExtensionRequestedState.Enabled, expected_status_code=ExtensionErrorCodes.PluginEnableProcessingFailed,
expected_handler_status='NotReady', expected_status_msg=expected_msg)

def test_should_fail_extension_if_error_thrown_during_policy_engine_init(self):
policy = \
{
"policyVersion": "0.1.0"
}
with patch('azurelinuxagent.ga.policy.policy_engine.ExtensionPolicyEngine.__init__',
side_effect=Exception("mock exception")):
expected_msg = "Extension will not be processed: \nInner error: mock exception"
self._test_policy_failure(policy=policy, op=ExtensionRequestedState.Enabled,
expected_status_code=ExtensionErrorCodes.PluginEnableProcessingFailed,
expected_handler_status='NotReady', expected_status_msg=expected_msg)

def test_should_fail_uninstall_if_extension_disallowed(self):
policy = \
{
"policyVersion": "0.1.0",
"extensionPolicies": {
"allowListedExtensionsOnly": True,
"signatureRequired": False,
"extensions": {}
},
}
expected_msg = "failed to uninstall extension 'OSTCExtensions.ExampleHandlerLinux' because extension is not specified in allowlist."
self._test_policy_failure(policy=policy, op=ExtensionRequestedState.Uninstall, expected_status_code=ExtensionErrorCodes.PluginDisableProcessingFailed,
expected_handler_status='NotReady', expected_status_msg=expected_msg)

def test_should_fail_enable_if_dependent_extension_disallowed(self):
self._create_policy_file({
"policyVersion": "0.1.0",
"extensionPolicies": {
"allowListedExtensionsOnly": True,
"extensions": {
"OSTCExtensions.ExampleHandlerLinux": {}
}
}
})
with mock_wire_protocol(wire_protocol_data.DATA_FILE_EXT_SEQUENCING) as protocol:
protocol.aggregate_status = None
protocol.report_vm_status = MagicMock()
exthandlers_handler = get_exthandlers_handler(protocol)
dep_ext_level_2 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux")
dep_ext_level_1 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux")

exthandlers_handler.run()
exthandlers_handler.report_ext_handlers_status()

# OtherExampleHandlerLinux should be disallowed by policy, ExampleHandlerLinux should be skipped because
# dependent extension failed
self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0",
expected_handler_name="OSTCExtensions.OtherExampleHandlerLinux",
expected_msg=("failed to enable extension 'OSTCExtensions.OtherExampleHandlerLinux' "
"because extension is not specified in allowlist."))

self._assert_handler_status(protocol.report_vm_status, "NotReady", 0, "1.0.0",
expected_handler_name="OSTCExtensions.ExampleHandlerLinux",
expected_msg="Skipping processing of extensions since execution of dependent "
"extension OSTCExtensions.OtherExampleHandlerLinux failed")

# check handler list and dependency levels
self.assertTrue(exthandlers_handler.ext_handlers is not None)
self.assertTrue(exthandlers_handler.ext_handlers is not None)
self.assertEqual(len(exthandlers_handler.ext_handlers), 2)
self.assertEqual(1, next(handler for handler in exthandlers_handler.ext_handlers if
handler.name == dep_ext_level_1.name).settings[0].dependencyLevel)
self.assertEqual(2, next(handler for handler in exthandlers_handler.ext_handlers if
handler.name == dep_ext_level_2.name).settings[0].dependencyLevel)


if __name__ == '__main__':
unittest.main()
Loading
Loading