import pytest from collections import namedtuple from unittest import mock from unittest.mock import call from cloudinit.cmd.devel.hotplug_hook import handle_hotplug from cloudinit.distros import Distro from cloudinit.event import EventType from cloudinit.net.activators import NetworkActivator from cloudinit.net.network_state import NetworkState from cloudinit.sources import DataSource from cloudinit.stages import Init hotplug_args = namedtuple('hotplug_args', 'udevaction, subsystem, devpath') FAKE_MAC = '11:22:33:44:55:66' @pytest.yield_fixture def mocks(): m_init = mock.MagicMock(spec=Init) m_distro = mock.MagicMock(spec=Distro) m_datasource = mock.MagicMock(spec=DataSource) m_datasource.distro = m_distro m_init.datasource = m_datasource m_init.fetch.return_value = m_datasource read_sys_net = mock.patch( 'cloudinit.cmd.devel.hotplug_hook.read_sys_net_safe', return_value=FAKE_MAC ) update_event_enabled = mock.patch( 'cloudinit.stages.update_event_enabled', return_value=True, ) m_network_state = mock.MagicMock(spec=NetworkState) parse_net = mock.patch( 'cloudinit.cmd.devel.hotplug_hook.parse_net_config_data', return_value=m_network_state ) m_activator = mock.MagicMock(spec=NetworkActivator) select_activator = mock.patch( 'cloudinit.cmd.devel.hotplug_hook.activators.select_activator', return_value=m_activator ) sleep = mock.patch('time.sleep') read_sys_net.start() update_event_enabled.start() parse_net.start() select_activator.start() m_sleep = sleep.start() yield namedtuple('mocks', 'm_init m_network_state m_activator m_sleep')( m_init=m_init, m_network_state=m_network_state, m_activator=m_activator, m_sleep=m_sleep, ) read_sys_net.stop() update_event_enabled.stop() parse_net.stop() select_activator.stop() sleep.stop() class TestUnsupportedActions: def test_unsupported_subsystem(self, mocks): with pytest.raises( Exception, match='cannot handle events for subsystem: not_real' ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', subsystem='not_real', udevaction='add' ) def test_unsupported_udevaction(self, mocks): with pytest.raises(ValueError, match='Unknown action: not_real'): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='not_real', subsystem='net' ) class TestHotplug: def test_succcessful_add(self, mocks): init = mocks.m_init mocks.m_network_state.iter_interfaces.return_value = [{ 'mac_address': FAKE_MAC, }] handle_hotplug( hotplug_init=init, devpath='/dev/fake', udevaction='add', subsystem='net' ) init.datasource.update_metadata_if_supported.assert_called_once_with([ EventType.HOTPLUG ]) mocks.m_activator.bring_up_interface.assert_called_once_with('fake') mocks.m_activator.bring_down_interface.assert_not_called() init._write_to_cache.assert_called_once_with() def test_successful_remove(self, mocks): init = mocks.m_init mocks.m_network_state.iter_interfaces.return_value = [{}] handle_hotplug( hotplug_init=init, devpath='/dev/fake', udevaction='remove', subsystem='net' ) init.datasource.update_metadata_if_supported.assert_called_once_with([ EventType.HOTPLUG ]) mocks.m_activator.bring_down_interface.assert_called_once_with('fake') mocks.m_activator.bring_up_interface.assert_not_called() init._write_to_cache.assert_called_once_with() def test_update_event_disabled(self, mocks, caplog): init = mocks.m_init with mock.patch( 'cloudinit.stages.update_event_enabled', return_value=False ): handle_hotplug( hotplug_init=init, devpath='/dev/fake', udevaction='remove', subsystem='net' ) assert 'hotplug not enabled for event of type' in caplog.text init.datasource.update_metadata_if_supported.assert_not_called() mocks.m_activator.bring_up_interface.assert_not_called() mocks.m_activator.bring_down_interface.assert_not_called() init._write_to_cache.assert_not_called() def test_update_metadata_failed(self, mocks): mocks.m_init.datasource.update_metadata_if_supported.return_value = \ False with pytest.raises( RuntimeError, match='Datasource .* not updated for event hotplug' ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='remove', subsystem='net' ) def test_detect_hotplugged_device_not_detected_on_add(self, mocks): mocks.m_network_state.iter_interfaces.return_value = [{}] with pytest.raises( RuntimeError, match='Failed to detect {} in updated metadata'.format(FAKE_MAC) ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='add', subsystem='net' ) def test_detect_hotplugged_device_detected_on_remove(self, mocks): mocks.m_network_state.iter_interfaces.return_value = [{ 'mac_address': FAKE_MAC, }] with pytest.raises( RuntimeError, match='Failed to detect .* in updated metadata' ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='remove', subsystem='net' ) def test_apply_failed_on_add(self, mocks): mocks.m_network_state.iter_interfaces.return_value = [{ 'mac_address': FAKE_MAC, }] mocks.m_activator.bring_up_interface.return_value = False with pytest.raises( RuntimeError, match='Failed to bring up device: /dev/fake' ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='add', subsystem='net' ) def test_apply_failed_on_remove(self, mocks): mocks.m_network_state.iter_interfaces.return_value = [{}] mocks.m_activator.bring_down_interface.return_value = False with pytest.raises( RuntimeError, match='Failed to bring down device: /dev/fake' ): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='remove', subsystem='net' ) def test_retry(self, mocks): with pytest.raises(RuntimeError): handle_hotplug( hotplug_init=mocks.m_init, devpath='/dev/fake', udevaction='add', subsystem='net' ) assert mocks.m_sleep.call_count == 5 assert mocks.m_sleep.call_args_list == [ call(1), call(3), call(5), call(10), call(30) ]