From abd32cbdfe0c9457796ca601e0498eb0fe098c65 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 26 Sep 2016 21:27:41 +0000 Subject: [PATCH] Add validation for connections This patch its about to add all connections validation for flat and neutron network interface and if the user its using the OneView ml2 Driver. Change-Id: I6c47f6b09d22b530da8eeac189ff54f603850723 --- oneview_client/client.py | 139 +++++++++++------- oneview_client/tests/fixtures.py | 17 ++- .../tests/functional/test_oneview_client.py | 27 ++-- .../tests/unit/test_oneview_client.py | 117 +++++++++++---- oneview_client/tests/unit/test_utils.py | 58 +++++++- oneview_client/utils.py | 92 ++++++++++++ requirements.txt | 1 + 7 files changed, 348 insertions(+), 103 deletions(-) diff --git a/oneview_client/client.py b/oneview_client/client.py index fa285a0..34b2cdf 100644 --- a/oneview_client/client.py +++ b/oneview_client/client.py @@ -610,6 +610,84 @@ class Client(BaseClient): return complete_task.get('associatedResource').get('resourceUri') # ---- Node Validate ---- + @auditing.audit + def validate_connections(self, oneview_info, ports, network_interface): + if network_interface != 'neutron': + self._is_node_port_mac_compatible_with_server_hardware( + oneview_info, ports) + self._validate_server_profile_template_mac_type(oneview_info) + elif oneview_info.get('use_oneview_ml2_driver'): + valid_ports = utils.get_oneview_connection_ports(ports) + self._validate_node_and_port_server_hardware_uri( + oneview_info, valid_ports) + self._validate_connection_mac(oneview_info, valid_ports) + + @auditing.audit + def _is_node_port_mac_compatible_with_server_hardware( + self, node_info, ports + ): + server_hardware = self.get_server_hardware(node_info) + try: + mac = server_hardware.get_mac(nic_index=0) + except exceptions.OneViewException: + mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0) + + for port in ports: + if port.address.lower() == mac: + return + + message = ( + "The ports of the node are not compatible with its " + "server hardware %(server_hardware_uri)s." % + {'server_hardware_uri': server_hardware.uri} + ) + raise exceptions.OneViewInconsistentResource(message) + + @auditing.audit + def _validate_server_profile_template_mac_type(self, oneview_info): + server_profile_template = self.get_server_profile_template( + oneview_info + ) + if server_profile_template.mac_type != 'Physical': + message = ( + "The server profile template %s is not set to use " + "physical MAC." % server_profile_template.uri + ) + raise exceptions.OneViewInconsistentResource(message) + + @auditing.audit + def _validate_node_and_port_server_hardware_uri(self, oneview_info, ports): + node_hardware_id = utils.get_uuid_from_uri( + oneview_info.get('server_hardware_uri') + ) + for port in ports: + switch_info = utils.get_switch_info(port) + port_hardware_id = switch_info.get('server_hardware_id') + if port_hardware_id.lower() != node_hardware_id.lower(): + raise exceptions.OneViewInconsistentResource( + "The Server Hardware ID of the port %(port_id)s " + "doesn't match the Server Hardware ID %(server_hardware)s " + "of the node." % { + 'port_id': port.uuid, + 'server_hardware': node_hardware_id + } + ) + + @auditing.audit + def _validate_connection_mac(self, oneview_info, ports): + server_hardware = self.get_server_hardware(oneview_info) + for port in ports: + if port.address.lower() not in utils.get_all_macs(server_hardware): + raise exceptions.OneViewInconsistentResource( + "The MAC address %(mac)s of the port %(port_id)s doesn't" + " have a corresponding MAC address in the Server Hardware" + " %(server_hardware_uri)s" % { + 'mac': port.address, + 'port_id': port.uuid, + 'server_hardware_uri': server_hardware.uri + } + ) + @auditing.audit def validate_node_server_hardware( self, node_info, node_memorymb, node_cpus @@ -697,48 +775,16 @@ class Client(BaseClient): server_hardware = self.get_server_hardware(node_info) mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0) - is_mac_address_compatible = True for port in ports: - port_address = port.__dict__.get('_obj_address') - if port_address is None: - port_address = port.__dict__.get('_address') - if port_address.lower() != mac.lower(): - is_mac_address_compatible = False + if port.address.lower() == mac.lower(): + return - if (not is_mac_address_compatible) or len(ports) == 0: - message = ( - "The ports of the node are not compatible with its" - " server profile %(server_profile_uri)s." % - {'server_profile_uri': server_profile.uri} - ) - raise exceptions.OneViewInconsistentResource(message) - - @auditing.audit - def is_node_port_mac_compatible_with_server_hardware( - self, node_info, ports - ): - server_hardware = self.get_server_hardware(node_info) - try: - mac = server_hardware.get_mac(nic_index=0) - except exceptions.OneViewException: - mac = self.get_sh_mac_from_ilo(server_hardware.uuid, nic_index=0) - - is_mac_address_compatible = True - for port in ports: - port_address = port.__dict__.get('_obj_address') - if port_address is None: - port_address = port.__dict__.get('_address') - - if port_address.lower() != mac: - is_mac_address_compatible = False - - if (not is_mac_address_compatible) or len(ports) == 0: - message = ( - "The ports of the node are not compatible with its" - " server hardware %(server_hardware_uri)s." % - {'server_hardware_uri': server_hardware.uri} - ) - raise exceptions.OneViewInconsistentResource(message) + message = ( + "The ports of the node are not compatible with its" + " server profile %(server_profile_uri)s." % + {'server_profile_uri': server_profile.uri} + ) + raise exceptions.OneViewInconsistentResource(message) @auditing.audit def validate_node_server_profile_template(self, node_info): @@ -813,16 +859,3 @@ class Client(BaseClient): " template %s." % server_profile_template.uri ) raise exceptions.OneViewInconsistentResource(message) - - @auditing.audit - def validate_server_profile_template_mac_type(self, uuid): - server_profile_template = self.get_server_profile_template_by_uuid( - uuid - ) - - if server_profile_template.mac_type != 'Physical': - message = ( - "The server profile template %s is not set to use" - " physical MAC." % server_profile_template.uri - ) - raise exceptions.OneViewInconsistentResource(message) diff --git a/oneview_client/tests/fixtures.py b/oneview_client/tests/fixtures.py index 50df925..c7faca5 100644 --- a/oneview_client/tests/fixtures.py +++ b/oneview_client/tests/fixtures.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# # (c) Copyright 2015 Hewlett Packard Enterprise Development LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -17,7 +15,6 @@ import json - SERVER_HARDWARE_JSON = { "type": "server-hardware-1", "name": "Encl2, bay 16", @@ -6705,3 +6702,17 @@ PROPERTIES_DICT = {"cpu_arch": "x86_64", DRIVER_INFO_DICT = {'server_hardware_uri': 'fake_sh_uri', 'server_profile_template_uri': 'fake_spt_uri'} + + +class TestablePort(object): + + def __init__( + self, obj_address, bootable=False, pxe_enabled=False, sh_id='' + ): + self.address = obj_address + self.local_link_connection = { + 'switch_info': {'bootable': str(bootable), + 'server_hardware_id': sh_id} + } + self.pxe_enabled = pxe_enabled + self.uuid = 'port-uuid' diff --git a/oneview_client/tests/functional/test_oneview_client.py b/oneview_client/tests/functional/test_oneview_client.py index 2b6d00b..e4468ca 100644 --- a/oneview_client/tests/functional/test_oneview_client.py +++ b/oneview_client/tests/functional/test_oneview_client.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# # (c) Copyright 2015 Hewlett Packard Enterprise Development LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -27,7 +25,6 @@ from oneview_client import exceptions from oneview_client import ilo_utils from oneview_client import models from oneview_client.tests import fixtures -from oneview_client import utils class OneViewClientAuthTestCase(unittest.TestCase): @@ -575,14 +572,15 @@ class OneViewClientTestCase(unittest.TestCase): ) mock_get.return_value = response - spt_uuid = utils.get_uuid_from_uri( - server_profile_template_virtual_mac.get("uri") - ) + oneview_info = { + 'server_profile_template_uri': + server_profile_template_virtual_mac.get("uri") + } self.assertRaises( exceptions.OneViewInconsistentResource, - oneview_client.validate_server_profile_template_mac_type, - spt_uuid + oneview_client._validate_server_profile_template_mac_type, + oneview_info ) @mock.patch.object(requests, 'get', autospec=True) @@ -603,11 +601,12 @@ class OneViewClientTestCase(unittest.TestCase): ) mock_get.return_value = response - spt_uuid = utils.get_uuid_from_uri( - server_profile_template_physical_mac.get("uri") - ) + oneview_info = { + 'server_profile_template_uri': + server_profile_template_physical_mac.get("uri") + } - oneview_client.validate_server_profile_template_mac_type(spt_uuid) + oneview_client._validate_server_profile_template_mac_type(oneview_info) @mock.patch.object(client.ClientV2, '_authenticate', autospec=True) @@ -927,7 +926,3 @@ class OneViewClientV2TestCase(unittest.TestCase): headers=mock.ANY, verify=True ) - - -if __name__ == '__main__': - unittest.main() diff --git a/oneview_client/tests/unit/test_oneview_client.py b/oneview_client/tests/unit/test_oneview_client.py index a662e01..4d02b06 100644 --- a/oneview_client/tests/unit/test_oneview_client.py +++ b/oneview_client/tests/unit/test_oneview_client.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# # (c) Copyright 2015 Hewlett Packard Enterprise Development LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -29,13 +27,7 @@ from oneview_client import exceptions from oneview_client import models from oneview_client import states from oneview_client.tests import fixtures - - -class TestablePort(object): - - def __init__(self, obj_address): - self.obj_address = obj_address - self._obj_address = obj_address +from oneview_client import utils class OneViewClientAuthTestCase(unittest.TestCase): @@ -542,6 +534,73 @@ class OneViewClientTestCase(unittest.TestCase): ) self.assertEqual(mock_get.call_count, 2) + def test__validate_node_and_port_server_hardware_uri_without_ports(self): + driver_info = {"server_hardware_uri": "/rest/server-hardware/uri"} + self.oneview_client._validate_node_and_port_server_hardware_uri( + driver_info, [] + ) + + def test__validate_node_and_port_server_hardware_uri(self): + sh_id = 'sh-id' + driver_info = {"server_hardware_uri": sh_id} + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', sh_id=sh_id) + ports = [port1] + self.oneview_client._validate_node_and_port_server_hardware_uri( + driver_info, ports + ) + + def test__dont_validate_node_and_port_server_hardware_uri(self): + sh_id = 'sh-id' + wrong_sh_id = 'wrong-id' + driver_info = {"server_hardware_uri": sh_id} + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', sh_id=sh_id) + port2 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', sh_id=wrong_sh_id) + ports = [port1, port2] + with self.assertRaises(exceptions.OneViewInconsistentResource): + self.oneview_client._validate_node_and_port_server_hardware_uri( + driver_info, ports + ) + + @mock.patch.object(client.Client, 'get_server_hardware', autospec=True) + @mock.patch.object(utils, 'get_all_macs', autospec=True) + def test__validate_connection_mac( + self, mock_get_macs, mock_get_server_hardware + ): + sh_id = 'sh-id' + sh_uri = "/rest/server-hardware/" + sh_id + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', sh_id=sh_id) + ports = [port1] + + driver_info = {"server_hardware_uri": sh_uri} + + server_hardware_mock = models.ServerHardware() + setattr(server_hardware_mock, "uri", "uri") + mock_get_server_hardware.return_value = server_hardware_mock + mock_get_macs.return_value = {'aa:bb:cc:dd:ee:fa'} + + self.oneview_client._validate_connection_mac(driver_info, ports) + + @mock.patch.object(client.Client, 'get_server_hardware', autospec=True) + @mock.patch.object(utils, 'get_all_macs', autospec=True) + def test__dont_validate_connection_mac( + self, mock_get_macs, mock_get_server_hardware + ): + sh_id = 'sh-id' + sh_uri = "/rest/server-hardware/" + sh_id + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', sh_id=sh_id) + port2 = fixtures.TestablePort('AA:BB:CC:DD:EE:FB', sh_id=sh_id) + ports = [port1, port2] + + driver_info = {"server_hardware_uri": sh_uri} + + server_hardware_mock = models.ServerHardware() + setattr(server_hardware_mock, "uri", "uri") + mock_get_server_hardware.return_value = server_hardware_mock + mock_get_macs.return_value = {'aa:bb:cc:dd:ee:fb'} + + with self.assertRaises(exceptions.OneViewInconsistentResource): + self.oneview_client._validate_connection_mac(driver_info, ports) + @mock.patch.object(client.Client, 'get_server_hardware', autospec=True) def test_validate_node_server_hardware_inconsistent_memorymb_value( self, mock_get_server_hardware @@ -668,9 +727,9 @@ class OneViewClientTestCase(unittest.TestCase): mock_server_hardware.return_value = server_hardware_mock mock_server_hardware_by_uuid.return_value = server_hardware_mock - self.oneview_client.is_node_port_mac_compatible_with_server_hardware( + self.oneview_client._is_node_port_mac_compatible_with_server_hardware( {}, - [type('obj', (object,), {'_address': 'D8:9D:67:73:54:00'})] + [type('obj', (object,), {'address': 'D8:9D:67:73:54:00'})] ) mock_server_hardware.assert_called_once_with(self.oneview_client, {}) @@ -702,9 +761,9 @@ class OneViewClientTestCase(unittest.TestCase): exceptions.OneViewInconsistentResource, exc_expected_msg, self.oneview_client - .is_node_port_mac_compatible_with_server_hardware, + ._is_node_port_mac_compatible_with_server_hardware, {}, - [type('obj', (object,), {'_address': 'AA:BB:CC:DD:EE:FF'})] + [type('obj', (object,), {'address': 'AA:BB:CC:DD:EE:FF'})] ) @mock.patch.object(client.Client, 'get_server_profile_from_hardware', @@ -886,35 +945,38 @@ class OneViewClientTestCase(unittest.TestCase): server_profile_template ) - @mock.patch.object(client.Client, 'get_server_profile_template_by_uuid', + @mock.patch.object(client.Client, 'get_server_profile_template', autospec=True) def test_validate_server_profile_template_mac_type( self, server_template_mock): - uuid = 123 - profile_template_mock = models.ServerProfileTemplate() setattr(profile_template_mock, "mac_type", "Physical") + setattr(profile_template_mock, "uri", + "/rest/server-profile-templates/%s" % '111-222-333') + + oneview_info = {'server_profile_template_uri': '/rest/111-222-333'} server_template_mock.return_value = profile_template_mock - self.oneview_client.validate_server_profile_template_mac_type(uuid) + self.oneview_client._validate_server_profile_template_mac_type( + oneview_info + ) - @mock.patch.object(client.Client, 'get_server_profile_template_by_uuid', + @mock.patch.object(client.Client, 'get_server_profile_template', autospec=True) def test_validate_server_profile_template_mac_type_negative( self, server_template_mock): - uuid = 123 - - # Negative case profile_template_mock = models.ServerProfileTemplate() setattr(profile_template_mock, "mac_type", "Virtual") setattr(profile_template_mock, "uri", - "/rest/server-profile-templates/%s" % uuid) + "/rest/server-profile-templates/%s" % '123') + oneview_info = {'server_profile_template_uri': '/rest/123'} server_template_mock.return_value = profile_template_mock self.assertRaises( exceptions.OneViewInconsistentResource, - self.oneview_client.validate_server_profile_template_mac_type, - uuid) + self.oneview_client._validate_server_profile_template_mac_type, + oneview_info + ) @mock.patch.object(client.Client, 'get_oneview_version') def test_verify_oneview_version(self, mock_get_oneview_version): @@ -1030,7 +1092,7 @@ class OneViewClientTestCase(unittest.TestCase): mock_get_server_profile_from_hardware.return_value = \ server_profile_mock - port = TestablePort('AA:BB:CC:DD:EE:FF') + port = fixtures.TestablePort('AA:BB:CC:DD:EE:FF') node_info = None ports = [port] self.oneview_client.is_node_port_mac_compatible_with_server_profile( @@ -1100,7 +1162,7 @@ class OneViewClientTestCase(unittest.TestCase): mock_get_sh_mac_from_ilo.return_value = 'aa:bb:cc:dd:ee' node_info = {'server_hardware_uri': '/rest/111-222-333'} - ports = [TestablePort('aa:bb:cc:dd:ee')] + ports = [fixtures.TestablePort('aa:bb:cc:dd:ee')] self.oneview_client.is_node_port_mac_compatible_with_server_profile( node_info, ports @@ -1322,6 +1384,3 @@ class OneViewClientTestCase(unittest.TestCase): self.assertTrue(mock__prepare_do_request.called) self.assertTrue(mock__wait_for_task.called) - -if __name__ == '__main__': - unittest.main() diff --git a/oneview_client/tests/unit/test_utils.py b/oneview_client/tests/unit/test_utils.py index 8934d35..33f1e7d 100644 --- a/oneview_client/tests/unit/test_utils.py +++ b/oneview_client/tests/unit/test_utils.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# # (c) Copyright 2016 Hewlett Packard Enterprise Development LP # Copyright 2016 Universidade Federal de Campina Grande # @@ -17,6 +15,8 @@ import unittest +from oneview_client import exceptions +from oneview_client.tests import fixtures from oneview_client import utils @@ -47,3 +47,57 @@ class UtilsTestCase(unittest.TestCase): prefix = '/rest/resource/' uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa?' self.assertEqual(None, utils.get_uri_from_uuid(prefix, uuid)) + + def test__get_empty_bootable_ports(self): + port = fixtures.TestablePort('AA:BB:CC:DD:EE:FF', bootable=False) + ports = [port] + with self.assertRaises(exceptions.OneViewInconsistentResource): + utils.get_bootable_ports(ports, bootable='true') + + def test__get_empty_not_bootable_ports(self): + port = fixtures.TestablePort('AA:BB:CC:DD:EE:FF', bootable=True) + ports = [port] + with self.assertRaises(exceptions.OneViewInconsistentResource): + utils.get_bootable_ports(ports, bootable='false') + + def test__get_multiple_bootable_ports(self): + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', bootable=True) + port2 = fixtures.TestablePort('AA:BB:CC:DD:EE:FB', bootable=False) + port3 = fixtures.TestablePort('AA:BB:CC:DD:EE:FC', bootable=True) + ports = [port1, port2, port3] + self.assertEqual( + [port1, port3], + utils.get_bootable_ports(ports) + ) + + def test__get_multiple_not_bootable_ports(self): + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', bootable=False) + port2 = fixtures.TestablePort('AA:BB:CC:DD:EE:FB', bootable=True) + port3 = fixtures.TestablePort('AA:BB:CC:DD:EE:FC', bootable=False) + ports = [port1, port2, port3] + self.assertEqual( + [port1, port3], + utils.get_bootable_ports(ports, bootable='false') + ) + + def test__get_no_pxe_enabled_ports(self): + port = fixtures.TestablePort('AA:BB:CC:DD:EE:FF', pxe_enabled=False) + ports = [port] + with self.assertRaises(exceptions.OneViewInconsistentResource): + utils.get_ports_with_llc_and_pxe_enabled(ports) + + def test__get_pxe_enabled_ports(self): + port = fixtures.TestablePort('AA:BB:CC:DD:EE:FF', pxe_enabled=True) + ports = [port] + self.assertEqual( + ports, utils.get_ports_with_llc_and_pxe_enabled(ports) + ) + + def test__get_multiple_pxe_enabled_ports(self): + port1 = fixtures.TestablePort('AA:BB:CC:DD:EE:FA', pxe_enabled=True) + port2 = fixtures.TestablePort('AA:BB:CC:DD:EE:FB', pxe_enabled=False) + port3 = fixtures.TestablePort('AA:BB:CC:DD:EE:FC', pxe_enabled=True) + ports = [port1, port2, port3] + self.assertEqual( + [port1, port3], utils.get_ports_with_llc_and_pxe_enabled(ports) + ) diff --git a/oneview_client/utils.py b/oneview_client/utils.py index 52aed4c..c4df791 100644 --- a/oneview_client/utils.py +++ b/oneview_client/utils.py @@ -14,6 +14,11 @@ # limitations under the License. import re +import six + +from oslo_serialization import jsonutils + +from oneview_client import exceptions UUID_PATTERN = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" +\ "[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" @@ -33,3 +38,90 @@ def get_uuid_from_uri(uri): def get_uri_from_uuid(resource_prefix, uuid): if uuid and _is_uuid_valid(uuid): return str(resource_prefix) + str(uuid) + + +def get_oneview_connection_ports(ports): + llc_pxe_ports = get_ports_with_llc_and_pxe_enabled(ports) + bootable_ports = get_bootable_ports(llc_pxe_ports) + + return bootable_ports + + +def get_ports_with_llc_and_pxe_enabled(ports): + llc_pxe_ports = [port for port in ports if ( + port.local_link_connection and port.pxe_enabled) + ] + if not llc_pxe_ports: + raise exceptions.OneViewInconsistentResource( + "There must exist at least one port with local link " + "connection information and pxe_enabled = True at the node." + ) + + return llc_pxe_ports + + +def get_bootable_ports(ports, bootable='true'): + bootable_ports = [] + for port in ports: + switch_info = get_switch_info(port) + if switch_info and switch_info.get('bootable').lower() == bootable: + bootable_ports.append(port) + + if not bootable_ports: + raise exceptions.OneViewInconsistentResource( + "In the local_link_connection of the port must exist " + "the switch_info with bootable = true" + ) + + return bootable_ports + + +def get_switch_info(port): + switch_info = port.local_link_connection.get('switch_info') + if switch_info and isinstance(switch_info, six.text_type): + switch_info = jsonutils.loads(switch_info) + + return switch_info + + +def get_all_macs(server_hardware): + macs = [] + device_slots = server_hardware.port_map.get('deviceSlots') + physical_ports = get_physical_ports(device_slots) + virtual_ports = get_virtual_ports(physical_ports) + + macs.extend(get_physical_macs(physical_ports)) + macs.extend(get_virtual_macs(virtual_ports)) + return set(macs) + + +def get_physical_ports(device_slots): + physical_ports = [] + for device_slot in device_slots: + if device_slot and device_slot.get('physicalPorts'): + physical_ports.extend(device_slot.get('physicalPorts')) + return physical_ports + + +def get_virtual_ports(physical_ports): + virtual_ports = [] + for physical_port in physical_ports: + if physical_port and physical_port.get('virtualPorts'): + virtual_ports.extend(physical_port.get('virtualPorts')) + return virtual_ports + + +def get_physical_macs(physical_ports): + physical_macs = [] + for physical_port in physical_ports: + if physical_port and physical_port.get('mac'): + physical_macs.append(physical_port.get('mac').lower()) + return physical_macs + + +def get_virtual_macs(virtual_ports): + virtual_macs = [] + for virtual_port in virtual_ports: + if virtual_port and virtual_port.get('mac'): + virtual_macs.append(virtual_port.get('mac').lower()) + return virtual_macs diff --git a/requirements.txt b/requirements.txt index 712a554..69d90f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ pbr>=1.6 # Apache-2.0 Babel>=2.3.4 # BSD +oslo.serialization>=1.10.0 # Apache-2.0 retrying!=1.3.0,>=1.2.3 # Apache-2.0 six>=1.9.0 # MIT