import subprocess import unittest from mock import patch, MagicMock import charmhelpers.contrib.network.ovs as ovs from tests.helpers import patch_open # NOTE(fnordahl): some functions drectly under the ``contrib.network.ovs`` # module have their unit tests in the ``test_ovs.py`` module in the # ``tests.contrib.network.ovs`` package. GOOD_CERT = '''Certificate: Data: Version: 1 (0x0) Serial Number: 13798680962510501282 (0xbf7ec33a136235a2) Signature Algorithm: sha1WithRSAEncryption Issuer: C=US, ST=CA, L=Palo Alto, O=Open vSwitch, OU=Open vSwitch Validity Not Before: Jun 28 17:02:19 2013 GMT Not After : Jun 28 17:02:19 2019 GMT Subject: C=US, ST=CA, L=Palo Alto, O=Open vSwitch, OU=Open vSwitch Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:e8:a7:db:0a:6d:c0:16:4a:14:96:1d:74:91:15: 64:3f:ae:2a:54:be:2a:fe:10:14:9a:73:39:d8:58: 74:7f:ab:d5:f2:39:aa:9a:27:7c:31:82:f8:74:42: 46:8d:c5:3b:42:55:52:be:75:7f:a5:b1:ec:d5:29: 9f:62:0e:de:31:27:2b:95:1f:24:0d:ca:8c:48:30: 96:9f:ba:b7:9d:eb:c1:bd:93:05:e3:d8:ca:66:5a: e9:cb:a5:7a:3a:8d:27:e2:05:9d:88:fc:a9:ef:af: 47:4c:66:ce:c6:43:73:1a:85:f4:5f:b9:53:5b:29: f3:c3:23:1f:0c:20:95:11:50:71:b2:f6:01:23:3f: 66:0f:5c:43:c2:90:fb:e5:98:73:98:e9:38:bb:1f: 1b:89:97:1e:dc:d7:98:07:68:32:ec:da:1d:69:0b: e2:df:40:fb:64:52:e5:e9:40:27:b0:ca:73:21:51: f6:8f:00:20:c0:2b:1a:d4:01:c2:32:38:9d:d1:8d: 88:71:46:a9:42:0d:ee:3b:1c:88:db:27:69:49:f9: 60:34:70:61:3d:60:df:7e:e4:e1:1d:c6:16:89:05: ba:31:06:eb:88:b5:78:94:5d:8c:9d:88:fe:f2:c2: 80:a1:04:15:d3:84:85:d3:aa:5a:1d:53:5c:f8:57: ae:61 Exponent: 65537 (0x10001) Signature Algorithm: sha1WithRSAEncryption 14:7e:ca:c3:fc:93:60:9f:80:e0:65:2e:ef:41:2d:f9:af:77: da:6d:e2:e0:11:70:17:fb:e5:67:4c:f0:ad:39:ec:96:ef:fe: d5:95:94:70:e5:52:31:68:63:8c:ea:b3:a1:8e:02:e2:91:4b: a8:8c:07:86:fd:80:98:a2:b1:90:2b:9c:2e:ab:f4:73:9d:8f: fd:31:b9:8f:fe:6c:af:d6:bf:72:44:89:08:93:19:ef:2b:c3: 7c:ab:ba:bc:57:ca:f1:17:e4:e8:81:40:ca:65:df:84:be:10: 2c:42:46:af:d2:e0:0d:df:5d:56:53:65:13:e0:20:55:b4:ee: cd:5e:b5:c4:97:1d:3e:a6:c1:9c:7e:b8:87:ee:64:78:a5:59: e5:b2:79:47:9a:8e:59:fa:c4:18:ea:27:fd:a2:d5:76:d0:ae: d9:05:f6:0e:23:ca:7d:66:a1:ba:18:67:f5:6d:bb:51:5a:f5: 52:e9:17:bb:63:15:24:b4:61:25:9f:d9:9c:89:58:93:9a:c3: 74:55:72:3e:f9:ff:ef:54:7d:e8:28:78:ba:3c:c7:15:ba:b9: c6:e3:8c:61:cb:a9:ed:8d:07:16:0d:8d:f6:1c:36:11:69:08: b8:45:7d:fc:fd:d1:ab:2d:9b:4e:9c:dd:11:78:50:c7:87:9f: 4a:24:9c:a0 -----BEGIN CERTIFICATE----- MIIDwjCCAqoCCQC/fsM6E2I1ojANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC VVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDE9w ZW4gdlN3aXRjaDEfMB0GA1UECxMWT3BlbiB2U3dpdGNoIGNlcnRpZmllcjE6MDgG A1UEAxMxb3ZzY2xpZW50IGlkOjU4MTQ5N2E1LWJjMDAtNGVjYy1iNzkwLTU3NTZj ZWUxNmE0ODAeFw0xMzA2MjgxNzAyMTlaFw0xOTA2MjgxNzAyMTlaMIGiMQswCQYD VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UE ChMMT3BlbiB2U3dpdGNoMR8wHQYDVQQLExZPcGVuIHZTd2l0Y2ggY2VydGlmaWVy MTowOAYDVQQDEzFvdnNjbGllbnQgaWQ6NTgxNDk3YTUtYmMwMC00ZWNjLWI3OTAt NTc1NmNlZTE2YTQ4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Kfb Cm3AFkoUlh10kRVkP64qVL4q/hAUmnM52Fh0f6vV8jmqmid8MYL4dEJGjcU7QlVS vnV/pbHs1SmfYg7eMScrlR8kDcqMSDCWn7q3nevBvZMF49jKZlrpy6V6Oo0n4gWd iPyp769HTGbOxkNzGoX0X7lTWynzwyMfDCCVEVBxsvYBIz9mD1xDwpD75ZhzmOk4 ux8biZce3NeYB2gy7NodaQvi30D7ZFLl6UAnsMpzIVH2jwAgwCsa1AHCMjid0Y2I cUapQg3uOxyI2ydpSflgNHBhPWDffuThHcYWiQW6MQbriLV4lF2MnYj+8sKAoQQV 04SF06paHVNc+FeuYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAUfsrD/JNgn4Dg ZS7vQS35r3fabeLgEXAX++VnTPCtOeyW7/7VlZRw5VIxaGOM6rOhjgLikUuojAeG /YCYorGQK5wuq/RznY/9MbmP/myv1r9yRIkIkxnvK8N8q7q8V8rxF+TogUDKZd+E vhAsQkav0uAN311WU2UT4CBVtO7NXrXElx0+psGcfriH7mR4pVnlsnlHmo5Z+sQY 6if9otV20K7ZBfYOI8p9ZqG6GGf1bbtRWvVS6Re7YxUktGEln9mciViTmsN0VXI+ +f/vVH3oKHi6PMcVurnG44xhy6ntjQcWDY32HDYRaQi4RX38/dGrLZtOnN0ReFDH h59KJJyg -----END CERTIFICATE----- ''' PEM_ENCODED = '''-----BEGIN CERTIFICATE----- MIIDwjCCAqoCCQC/fsM6E2I1ojANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC VVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlQYWxvIEFsdG8xFTATBgNVBAoTDE9w ZW4gdlN3aXRjaDEfMB0GA1UECxMWT3BlbiB2U3dpdGNoIGNlcnRpZmllcjE6MDgG A1UEAxMxb3ZzY2xpZW50IGlkOjU4MTQ5N2E1LWJjMDAtNGVjYy1iNzkwLTU3NTZj ZWUxNmE0ODAeFw0xMzA2MjgxNzAyMTlaFw0xOTA2MjgxNzAyMTlaMIGiMQswCQYD VQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVBhbG8gQWx0bzEVMBMGA1UE ChMMT3BlbiB2U3dpdGNoMR8wHQYDVQQLExZPcGVuIHZTd2l0Y2ggY2VydGlmaWVy MTowOAYDVQQDEzFvdnNjbGllbnQgaWQ6NTgxNDk3YTUtYmMwMC00ZWNjLWI3OTAt NTc1NmNlZTE2YTQ4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Kfb Cm3AFkoUlh10kRVkP64qVL4q/hAUmnM52Fh0f6vV8jmqmid8MYL4dEJGjcU7QlVS vnV/pbHs1SmfYg7eMScrlR8kDcqMSDCWn7q3nevBvZMF49jKZlrpy6V6Oo0n4gWd iPyp769HTGbOxkNzGoX0X7lTWynzwyMfDCCVEVBxsvYBIz9mD1xDwpD75ZhzmOk4 ux8biZce3NeYB2gy7NodaQvi30D7ZFLl6UAnsMpzIVH2jwAgwCsa1AHCMjid0Y2I cUapQg3uOxyI2ydpSflgNHBhPWDffuThHcYWiQW6MQbriLV4lF2MnYj+8sKAoQQV 04SF06paHVNc+FeuYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAUfsrD/JNgn4Dg ZS7vQS35r3fabeLgEXAX++VnTPCtOeyW7/7VlZRw5VIxaGOM6rOhjgLikUuojAeG /YCYorGQK5wuq/RznY/9MbmP/myv1r9yRIkIkxnvK8N8q7q8V8rxF+TogUDKZd+E vhAsQkav0uAN311WU2UT4CBVtO7NXrXElx0+psGcfriH7mR4pVnlsnlHmo5Z+sQY 6if9otV20K7ZBfYOI8p9ZqG6GGf1bbtRWvVS6Re7YxUktGEln9mciViTmsN0VXI+ +f/vVH3oKHi6PMcVurnG44xhy6ntjQcWDY32HDYRaQi4RX38/dGrLZtOnN0ReFDH h59KJJyg -----END CERTIFICATE-----''' BAD_CERT = ''' NO MARKERS ''' TO_PATCH = [ "apt_install", "log", "hashlib", ] class OVSHelpersTest(unittest.TestCase): def setUp(self): for m in TO_PATCH: setattr(self, m, self._patch(m)) def _patch(self, method): _m = patch('charmhelpers.contrib.network.ovs.' + method) mock = _m.start() self.addCleanup(_m.stop) return mock @patch('subprocess.check_output') def test_get_bridges(self, check_output): check_output.return_value = b"br1\n br2 " self.assertEqual(ovs.get_bridges(), ['br1', 'br2']) check_output.assert_called_once_with(['ovs-vsctl', 'list-br']) @patch('subprocess.check_output') def test_get_bridge_ports(self, check_output): check_output.return_value = b"p1\n p2 \np3" self.assertEqual(ovs.get_bridge_ports('br1'), ['p1', 'p2', 'p3']) check_output.assert_called_once_with( ['ovs-vsctl', '--', 'list-ports', 'br1']) @patch.object(ovs, 'get_bridges') @patch.object(ovs, 'get_bridge_ports') def test_get_bridges_and_ports_map(self, get_bridge_ports, get_bridges): get_bridges.return_value = ['br1', 'br2'] get_bridge_ports.side_effect = [ ['p1', 'p2'], ['p3']] self.assertEqual(ovs.get_bridges_and_ports_map(), { 'br1': ['p1', 'p2'], 'br2': ['p3'], }) @patch('subprocess.check_call') def test_del_bridge(self, check_call): ovs.del_bridge('test') check_call.assert_called_with(["ovs-vsctl", "--", "--if-exists", "del-br", 'test']) self.assertTrue(self.log.call_count == 1) @patch.object(ovs, 'port_to_br') @patch.object(ovs, 'add_bridge_port') @patch.object(ovs, 'lsb_release') @patch('os.path.exists') @patch('subprocess.check_call') def test_add_ovsbridge_linuxbridge(self, check_call, exists, lsb_release, add_bridge_port, port_to_br): exists.return_value = True lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} port_to_br.return_value = None if_and_port_data = { 'external-ids': {'mycharm': 'br-ex'} } with patch_open() as (mock_open, mock_file): ovs.add_ovsbridge_linuxbridge( 'br-ex', 'br-eno1', ifdata=if_and_port_data, portdata=if_and_port_data) check_call.assert_called_with(['ifup', 'veth-br-eno1']) add_bridge_port.assert_called_with( 'br-ex', 'veth-br-eno1', ifdata=if_and_port_data, exclusive=False, portdata=if_and_port_data) @patch.object(ovs, 'port_to_br') @patch.object(ovs, 'add_bridge_port') @patch.object(ovs, 'lsb_release') @patch('os.path.exists') @patch('subprocess.check_call') def test_add_ovsbridge_linuxbridge_already_direct_wired( self, check_call, exists, lsb_release, add_bridge_port, port_to_br): exists.return_value = True lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} port_to_br.return_value = 'br-ex' ovs.add_ovsbridge_linuxbridge('br-ex', 'br-eno1') check_call.assert_not_called() add_bridge_port.assert_not_called() @patch.object(ovs, 'port_to_br') @patch.object(ovs, 'add_bridge_port') @patch.object(ovs, 'lsb_release') @patch('os.path.exists') @patch('subprocess.check_call') def test_add_ovsbridge_linuxbridge_longname(self, check_call, exists, lsb_release, add_bridge_port, port_to_br): exists.return_value = True lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} port_to_br.return_value = None mock_hasher = MagicMock() mock_hasher.hexdigest.return_value = '12345678901234578910' self.hashlib.sha256.return_value = mock_hasher with patch_open() as (mock_open, mock_file): ovs.add_ovsbridge_linuxbridge('br-ex', 'br-reallylongname') check_call.assert_called_with(['ifup', 'cvb12345678-10']) add_bridge_port.assert_called_with( 'br-ex', 'cvb12345678-10', ifdata=None, exclusive=False, portdata=None) @patch('os.path.exists') def test_is_linuxbridge_interface_false(self, exists): exists.return_value = False result = ovs.is_linuxbridge_interface('eno1') self.assertFalse(result) @patch('os.path.exists') def test_is_linuxbridge_interface_true(self, exists): exists.return_value = True result = ovs.is_linuxbridge_interface('eno1') self.assertTrue(result) @patch('subprocess.check_call') def test_set_manager(self, check_call): ovs.set_manager('manager') check_call.assert_called_with(['ovs-vsctl', 'set-manager', 'ssl:manager']) self.assertTrue(self.log.call_count == 1) @patch('subprocess.check_call') def test_set_Open_vSwitch_column_value(self, check_call): ovs.set_Open_vSwitch_column_value('other_config:foo=bar') check_call.assert_called_with(['ovs-vsctl', 'set', 'Open_vSwitch', '.', 'other_config:foo=bar']) self.assertTrue(self.log.call_count == 1) @patch('os.path.exists') def test_get_certificate_good_cert(self, exists): exists.return_value = True with patch_open() as (mock_open, mock_file): mock_file.read.return_value = GOOD_CERT self.assertEqual(ovs.get_certificate(), PEM_ENCODED) self.assertTrue(self.log.call_count == 1) @patch('os.path.exists') def test_get_certificate_bad_cert(self, exists): exists.return_value = True with patch_open() as (mock_open, mock_file): mock_file.read.return_value = BAD_CERT self.assertRaises(RuntimeError, ovs.get_certificate) self.assertTrue(self.log.call_count == 1) @patch('os.path.exists') def test_get_certificate_missing(self, exists): exists.return_value = False self.assertIsNone(ovs.get_certificate()) self.assertTrue(self.log.call_count == 1) @patch('os.path.exists') @patch.object(ovs, 'service') def test_full_restart(self, service, exists): exists.return_value = False ovs.full_restart() service.assert_called_with('force-reload-kmod', 'openvswitch-switch') @patch('os.path.exists') @patch.object(ovs, 'service') def test_full_restart_upstart(self, service, exists): exists.return_value = True ovs.full_restart() service.assert_called_with('start', 'openvswitch-force-reload-kmod') @patch('subprocess.check_output') def test_port_to_br(self, check_output): check_output.return_value = b'br-ex' self.assertEqual(ovs.port_to_br('br-lb'), 'br-ex') @patch('subprocess.check_output') def test_port_to_br_not_found(self, check_output): check_output.side_effect = subprocess.CalledProcessError(1, 'not found') self.assertEqual(ovs.port_to_br('br-lb'), None) @patch('subprocess.check_call') def test_enable_ipfix_defaults(self, check_call): ovs.enable_ipfix('br-int', '10.5.0.10:4739') check_call.assert_called_once_with([ 'ovs-vsctl', 'set', 'Bridge', 'br-int', 'ipfix=@i', '--', '--id=@i', 'create', 'IPFIX', 'targets="10.5.0.10:4739"', 'sampling=64', 'cache_active_timeout=60', 'cache_max_flows=128', ]) @patch('subprocess.check_call') def test_enable_ipfix_values(self, check_call): ovs.enable_ipfix('br-int', '10.5.0.10:4739', sampling=120, cache_max_flows=24, cache_active_timeout=120) check_call.assert_called_once_with([ 'ovs-vsctl', 'set', 'Bridge', 'br-int', 'ipfix=@i', '--', '--id=@i', 'create', 'IPFIX', 'targets="10.5.0.10:4739"', 'sampling=120', 'cache_active_timeout=120', 'cache_max_flows=24', ]) @patch('subprocess.check_call') def test_disable_ipfix(self, check_call): ovs.disable_ipfix('br-int') check_call.assert_called_once_with( ['ovs-vsctl', 'clear', 'Bridge', 'br-int', 'ipfix'] ) @patch.object(ovs, 'lsb_release') @patch('os.path.exists') def test_setup_eni_sources_eni_folder(self, exists, lsb_release): exists.return_value = True lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} with patch_open() as (_, mock_file): # Mocked initial /etc/network/interfaces file content: mock_file.__iter__.return_value = [ 'some line', 'some other line'] ovs.setup_eni() mock_file.write.assert_called_once_with( '\nsource /etc/network/interfaces.d/*') @patch.object(ovs, 'lsb_release') @patch('os.path.exists') def test_setup_eni_wont_source_eni_folder_twice(self, exists, lsb_release): exists.return_value = True lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} with patch_open() as (_, mock_file): # Mocked initial /etc/network/interfaces file content: mock_file.__iter__.return_value = [ 'some line', ' source /etc/network/interfaces.d/* ', 'some other line'] ovs.setup_eni() self.assertFalse(mock_file.write.called) @patch.object(ovs, 'lsb_release') def test_setup_eni_raises_on_focal(self, lsb_release): lsb_release.return_value = {'DISTRIB_CODENAME': 'focal'} self.assertRaises(RuntimeError, ovs.setup_eni)