def test_invalid_subnet_data(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Arrange to deliver invalid subnet data. def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: return EtcdResponse(value=json.dumps({ 'gateway_ip': '10.28.0.1' })) if 'v6subnet-1' in key: return EtcdResponse(value=json.dumps({ 'gateway_ip': '2001:db8:1::1' })) eventlet.sleep(10) return None etcd_client.read.side_effect = etcd_client_read # Notify an endpoint. agent.etcd.on_endpoint_set(EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1' ) # Check that either the v4 or the v6 subnet was read from etcd. Since # it's invalid, the processing of the new endpoint stops at that point, # and the other subnet is not read at all. read_calls = etcd_client.read.mock_calls self.assertEqual(1, len(read_calls)) self.assertTrue(read_calls[0] in [ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True), ]) etcd_client.read.reset_mock()
def test_invalid_subnet_data(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Arrange to deliver invalid subnet data. def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: return EtcdResponse( value=json.dumps({'gateway_ip': '10.28.0.1'})) if 'v6subnet-1' in key: return EtcdResponse( value=json.dumps({'gateway_ip': '2001:db8:1::1'})) eventlet.sleep(10) return None etcd_client.read.side_effect = etcd_client_read # Notify an endpoint. agent.etcd.on_endpoint_set( EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1') # Check that either the v4 or the v6 subnet was read from etcd. Since # it's invalid, the processing of the new endpoint stops at that point, # and the other subnet is not read at all. read_calls = etcd_client.read.mock_calls self.assertEqual(1, len(read_calls)) self.assertTrue(read_calls[0] in [ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True), ]) etcd_client.read.reset_mock()
def _get_subnet(self, subnet_id): # Read data for subnet SUBNET_ID from etcd and translate it to the dict # form expected for insertion into a Neutron NetModel. LOG.debug("Read subnet %s from etcd", subnet_id) self.dirty_subnets.discard(subnet_id) subnet_key = datamodel_v1.key_for_subnet(subnet_id) response = self.client.read(subnet_key, consistent=True) data = etcdutils.safe_decode_json(response.value, 'subnet') LOG.debug("Subnet data: %s", data) if not (isinstance(data, dict) and 'cidr' in data and 'gateway_ip' in data): # Subnet data was invalid. LOG.warning("Invalid subnet data: %s => %s", response.value, data) raise ValidationFailed("Invalid subnet data") # Convert to form expected by NetModel. ip_version = 6 if ':' in data['cidr'] else 4 subnet = {'enable_dhcp': True, 'ip_version': ip_version, 'cidr': data['cidr'], 'dns_nameservers': data.get('dns_servers') or [], 'id': subnet_id, 'gateway_ip': data['gateway_ip'], 'host_routes': data.get('host_routes', []), 'network_id': data.get('network_id', NETWORK_ID)} if ip_version == 6: subnet['ipv6_address_mode'] = DHCPV6_STATEFUL subnet['ipv6_ra_mode'] = DHCPV6_STATEFUL return dhcp.DictModel(subnet)
def _get_subnet(self, subnet_id): # Read data for subnet SUBNET_ID from etcd and translate it to the dict # form expected for insertion into a Neutron NetModel. LOG.debug("Read subnet %s from etcd", subnet_id) self.dirty_subnets.discard(subnet_id) subnet_key = datamodel_v1.key_for_subnet(subnet_id) response = self.client.read(subnet_key, consistent=True) data = etcdutils.safe_decode_json(response.value, 'subnet') LOG.debug("Subnet data: %s", data) if not (isinstance(data, dict) and 'cidr' in data and 'gateway_ip' in data): # Subnet data was invalid. LOG.warning("Invalid subnet data: %s => %s", response.value, data) raise ValidationFailed("Invalid subnet data") # Convert to form expected by NetModel. ip_version = 6 if ':' in data['cidr'] else 4 subnet = { 'enable_dhcp': True, 'ip_version': ip_version, 'cidr': data['cidr'], 'dns_nameservers': data.get('dns_servers') or [], 'id': subnet_id, 'gateway_ip': data['gateway_ip'], 'host_routes': data.get('host_routes', []), 'network_id': data.get('network_id', NETWORK_ID) } if ip_version == 6: subnet['ipv6_address_mode'] = DHCPV6_STATEFUL subnet['ipv6_ra_mode'] = DHCPV6_STATEFUL return dhcp.DictModel(subnet)
def subnet_deleted(self, subnet_id): """Delete data from etcd for a subnet that is no longer wanted.""" LOG.info("Deleting subnet %s", subnet_id) # Delete the etcd key for this endpoint. key = datamodel_v1.key_for_subnet(subnet_id) try: self.client.delete(key) except etcd.EtcdKeyNotFound: # Already gone, treat as success. LOG.debug("Key %s, which we were deleting, disappeared", key)
def subnet_deleted(self, subnet_id): """Delete data from etcd for a subnet that is no longer wanted.""" LOG.info("Deleting subnet %s", subnet_id) # Delete the etcd key for this endpoint. key = datamodel_v1.key_for_subnet(subnet_id) try: self.client.delete(key) except etcd.EtcdKeyNotFound: # Already gone, treat as success. LOG.debug("Key %s, which we were deleting, disappeared", key)
def subnet_created(self, subnet, prev_index=None): """Write data to etcd to describe a DHCP-enabled subnet.""" LOG.info("Write subnet %s %s to etcd", subnet['id'], subnet['cidr']) data = subnet_etcd_data(subnet) # python-etcd doesn't keyword argument properly. kwargs = {} if prev_index is not None: kwargs['prevIndex'] = prev_index self.client.write(datamodel_v1.key_for_subnet(subnet['id']), json.dumps(data), **kwargs)
def subnet_created(self, subnet, prev_index=None): """Write data to etcd to describe a DHCP-enabled subnet.""" LOG.info("Write subnet %s %s to etcd", subnet['id'], subnet['cidr']) data = subnet_etcd_data(subnet) # python-etcd doesn't keyword argument properly. kwargs = {} if prev_index is not None: kwargs['prevIndex'] = prev_index self.client.write(datamodel_v1.key_for_subnet(subnet['id']), json.dumps(data), **kwargs)
def test_initial_snapshot(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Check that running it invokes the etcd watcher loop. with mock.patch.object(agent, 'etcd') as etcdobj: agent.run() etcdobj.loop.assert_called_with() # Arrange for subnet read to fail. etcd_client.read.side_effect = etcd.EtcdKeyNotFound with mock.patch.object(agent, 'call_driver') as call_driver: # Notify a non-empty initial snapshot. etcd_snapshot_node = mock.Mock() etcd_snapshot_node.action = 'exist' etcd_snapshot_node.key = ("/calico/v1/host/" + socket.gethostname() + "/workload/openstack" + "/workload_id/endpoint/endpoint-4") etcd_snapshot_node.value = json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-4'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] }) etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [etcd_snapshot_node] agent.etcd._on_snapshot_loaded(etcd_snapshot_response) # Check expected subnet was read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-4'), consistent=True) ]) etcd_client.read.reset_mock() # Check DHCP driver was not troubled - because the subnet data was # missing and so the port could not be processed further. call_driver.assert_not_called()
def test_initial_snapshot(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Check that running it invokes the etcd watcher loop. with mock.patch.object(agent, 'etcd') as etcdobj: agent.run() etcdobj.loop.assert_called_with() # Arrange for subnet read to fail. etcd_client.read.side_effect = etcd.EtcdKeyNotFound with mock.patch.object(agent, 'call_driver') as call_driver: # Notify a non-empty initial snapshot. etcd_snapshot_node = mock.Mock() etcd_snapshot_node.action = 'exist' etcd_snapshot_node.key = ("/calico/v1/host/" + socket.gethostname() + "/workload/openstack" + "/workload_id/endpoint/endpoint-4") etcd_snapshot_node.value = json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-4'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] }) etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [etcd_snapshot_node] agent.etcd._on_snapshot_loaded(etcd_snapshot_response) # Check expected subnet was read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-4'), consistent=True) ]) etcd_client.read.reset_mock() # Check DHCP driver was not troubled - because the subnet data was # missing and so the port could not be processed further. call_driver.assert_not_called()
def get_subnet_data(self, subnet): """Get data for an subnet out of etcd. This should be used on subnets returned from functions like ``get_subnets``. :param subnet: A ``Subnet`` class. :return: A ``Subnet`` class with ``data`` not None. """ LOG.debug("Getting subnet %s", subnet.id) result = self.client.read(datamodel_v1.key_for_subnet(subnet.id), timeout=ETCD_TIMEOUT) return Subnet( id=subnet.id, modified_index=result.modifiedIndex, data=result.value, )
def get_subnet_data(self, subnet): """Get data for an subnet out of etcd. This should be used on subnets returned from functions like ``get_subnets``. :param subnet: A ``Subnet`` class. :return: A ``Subnet`` class with ``data`` not None. """ LOG.debug("Getting subnet %s", subnet.id) result = self.client.read(datamodel_v1.key_for_subnet(subnet.id), timeout=ETCD_TIMEOUT) return Subnet( id=subnet.id, modified_index=result.modifiedIndex, data=result.value, )
def atomic_delete_subnet(self, subnet): """Atomically delete a given subnet. This method tolerates attempting to delete keys that are already missing, otherwise allows exceptions from etcd to bubble up. """ LOG.info("Atomically deleting subnet id %s, modified %s", subnet.id, subnet.modified_index) try: self.client.delete( datamodel_v1.key_for_subnet(subnet.id), prevIndex=subnet.modified_index, timeout=ETCD_TIMEOUT, ) except etcd.EtcdKeyNotFound: # Trying to delete stuff that doesn't exist is ok, but log it. LOG.info("Subnet %s was already deleted, nothing to do.", subnet.id)
def atomic_delete_subnet(self, subnet): """Atomically delete a given subnet. This method tolerates attempting to delete keys that are already missing, otherwise allows exceptions from etcd to bubble up. """ LOG.info( "Atomically deleting subnet id %s, modified %s", subnet.id, subnet.modified_index ) try: self.client.delete( datamodel_v1.key_for_subnet(subnet.id), prevIndex=subnet.modified_index, timeout=ETCD_TIMEOUT, ) except etcd.EtcdKeyNotFound: # Trying to delete stuff that doesn't exist is ok, but log it. LOG.info( "Subnet %s was already deleted, nothing to do.", subnet.id )
def test_mainline(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Check that running it invokes the etcd watcher loop. with mock.patch.object(agent, 'etcd') as etcdobj: agent.run() etcdobj.loop.assert_called_with() # Notify initial snapshot (empty). with mock.patch.object(agent, 'call_driver') as call_driver: etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [] agent.etcd._on_snapshot_loaded(etcd_snapshot_response) call_driver.assert_not_called() # Prepare subnet reads for the endpoints that we will notify. self.first_workload_read = True def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: self.assertEqual('/calico/dhcp/v1/subnet/v4subnet-1', key) return EtcdResponse(value=json.dumps({ 'cidr': '10.28.0.0/24', 'gateway_ip': '10.28.0.1', 'host_routes': [] })) if 'v6subnet-1' in key: return EtcdResponse( value=json.dumps({ 'cidr': '2001:db8:1::/80', 'gateway_ip': '2001:db8:1::1' })) if 'v4subnet-2' in key: return EtcdResponse(value=json.dumps({ 'cidr': '10.29.0.0/24', 'gateway_ip': '10.29.0.1', 'host_routes': [{ 'destination': '11.11.0.0/16', 'nexthop': '10.65.0.1' }] })) if key == '/calico/v1/host/nj-ubuntu/workload': if self.first_workload_read: # This is the recursive read that the CalicoEtcdWatcher # loop makes after we've triggered it to resync. etcd_snapshot_node = mock.Mock() etcd_snapshot_node.action = 'exist' etcd_snapshot_node.key = ( "/calico/v1/host/" + socket.gethostname() + "/workload/openstack" + "/workload_id/endpoint/endpoint-4") etcd_snapshot_node.value = json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] }) etcd_snapshot_response = mock.Mock() etcd_snapshot_response.etcd_index = 99 etcd_snapshot_response.leaves = [etcd_snapshot_node] self.first_workload_read = False return etcd_snapshot_response eventlet.sleep(10) return None etcd_client.read.side_effect = etcd_client_read with mock.patch.object(agent, 'call_driver') as call_driver: # Notify an endpoint. agent.etcd.on_endpoint_set( EtcdResponse( value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1') # Check expected subnets were read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True) ], any_order=True) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify another endpoint (using the same subnets). agent.etcd.on_endpoint_set( EtcdResponse( value=json.dumps({ 'state': 'active', 'name': 'tap5678', 'mac': 'fe:16:65:12:33:55', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.3/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::3/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1', 'fqdn': 'calico-vm17.datcon.co.uk' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-2') # Check no further etcd reads. etcd_client.read.assert_not_called() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify deletion of the first endpoint. agent.etcd.on_endpoint_delete(None, 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1') # Check no further etcd reads. etcd_client.read.assert_not_called() # Check DHCP driver was asked to reload allocations. call_driver.assert_called_with('restart', mock.ANY) # Notify another endpoint using a new subnet. agent.etcd.on_endpoint_set( EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tapABCD', 'mac': 'fe:16:65:12:33:66', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.29.0.3/32'], 'ipv4_subnet_ids': ['v4subnet-2'], 'ipv4_gateway': '10.29.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-3') # Check expected new subnet was read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-2'), consistent=True) ]) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Set the endpoint watcher loop running. eventlet.spawn(agent.etcd.loop) # Report that the subnet watcher noticed a change. agent.etcd.on_subnet_set(None, 'some-subnet-X') eventlet.sleep(0.2) # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Report that the subnet watcher loaded a new snapshot. agent.etcd.subnet_watcher._on_snapshot_loaded('ignored') eventlet.sleep(0.2) # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY)
def test_dir_delete(self, etcd_client_cls): LOG.debug('test_dir_delete') # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: return EtcdResponse(value=json.dumps({ 'cidr': '10.28.0.0/24', 'gateway_ip': '10.28.0.1' })) if 'v6subnet-1' in key: return EtcdResponse(value=json.dumps({ 'cidr': '2001:db8:1::/80', 'gateway_ip': '2001:db8:1::1' })) eventlet.sleep(10) return None LOG.debug('etcd_client=%r', etcd_client) etcd_client.read.side_effect = etcd_client_read # Notify initial snapshot (empty). etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [] LOG.debug('Call _on_snapshot_loaded') agent.etcd._on_snapshot_loaded(etcd_snapshot_response) with mock.patch.object(agent, 'call_driver') as call_driver: # Notify an endpoint. agent.etcd.on_endpoint_set(EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1' ) # Check expected subnets were read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True) ], any_order=True) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify deletion of one of that endpoint's parent directories. agent.etcd.on_dir_delete(None, hostname='hostname-ignored', orchestrator='openstack', workload_id='workload-id-ignored') self.assertTrue(agent.etcd.resync_after_current_poll)
def test_dir_delete(self, etcd_client_cls): LOG.debug('test_dir_delete') # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: return EtcdResponse(value=json.dumps({ 'cidr': '10.28.0.0/24', 'gateway_ip': '10.28.0.1' })) if 'v6subnet-1' in key: return EtcdResponse( value=json.dumps({ 'cidr': '2001:db8:1::/80', 'gateway_ip': '2001:db8:1::1' })) eventlet.sleep(10) return None LOG.debug('etcd_client=%r', etcd_client) etcd_client.read.side_effect = etcd_client_read # Notify initial snapshot (empty). etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [] LOG.debug('Call _on_snapshot_loaded') agent.etcd._on_snapshot_loaded(etcd_snapshot_response) with mock.patch.object(agent, 'call_driver') as call_driver: # Notify an endpoint. agent.etcd.on_endpoint_set( EtcdResponse( value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1') # Check expected subnets were read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True) ], any_order=True) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify deletion of one of that endpoint's parent directories. agent.etcd.on_dir_delete(None, hostname='hostname-ignored', orchestrator='openstack', workload_id='workload-id-ignored') self.assertTrue(agent.etcd.resync_after_current_poll)
def test_mainline(self, etcd_client_cls): # Create the DHCP agent. agent = CalicoDhcpAgent() etcd_client = etcd_client_cls.return_value # Check that running it invokes the etcd watcher loop. with mock.patch.object(agent, 'etcd') as etcdobj: agent.run() etcdobj.loop.assert_called_with() # Notify initial snapshot (empty). with mock.patch.object(agent, 'call_driver') as call_driver: etcd_snapshot_response = mock.Mock() etcd_snapshot_response.leaves = [] agent.etcd._on_snapshot_loaded(etcd_snapshot_response) call_driver.assert_not_called() # Prepare subnet reads for the endpoints that we will notify. self.first_workload_read = True def etcd_client_read(key, **kwargs): LOG.info('etcd_client_read %s %s', key, kwargs) if 'v4subnet-1' in key: self.assertEqual('/calico/dhcp/v1/subnet/v4subnet-1', key) return EtcdResponse(value=json.dumps({ 'cidr': '10.28.0.0/24', 'gateway_ip': '10.28.0.1', 'host_routes': [] })) if 'v6subnet-1' in key: return EtcdResponse(value=json.dumps({ 'cidr': '2001:db8:1::/80', 'gateway_ip': '2001:db8:1::1' })) if 'v4subnet-2' in key: return EtcdResponse(value=json.dumps({ 'cidr': '10.29.0.0/24', 'gateway_ip': '10.29.0.1', 'host_routes': [{'destination': '11.11.0.0/16', 'nexthop': '10.65.0.1'}] })) if key == '/calico/v1/host/nj-ubuntu/workload': if self.first_workload_read: # This is the recursive read that the CalicoEtcdWatcher # loop makes after we've triggered it to resync. etcd_snapshot_node = mock.Mock() etcd_snapshot_node.action = 'exist' etcd_snapshot_node.key = ( "/calico/v1/host/" + socket.gethostname() + "/workload/openstack" + "/workload_id/endpoint/endpoint-4" ) etcd_snapshot_node.value = json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] }) etcd_snapshot_response = mock.Mock() etcd_snapshot_response.etcd_index = 99 etcd_snapshot_response.leaves = [etcd_snapshot_node] self.first_workload_read = False return etcd_snapshot_response eventlet.sleep(10) return None etcd_client.read.side_effect = etcd_client_read with mock.patch.object(agent, 'call_driver') as call_driver: # Notify an endpoint. agent.etcd.on_endpoint_set(EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tap1234', 'mac': 'fe:16:65:12:33:44', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.2/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::2/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1' ) # Check expected subnets were read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-1'), consistent=True), mock.call(datamodel_v1.key_for_subnet('v6subnet-1'), consistent=True) ], any_order=True) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify another endpoint (using the same subnets). agent.etcd.on_endpoint_set(EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tap5678', 'mac': 'fe:16:65:12:33:55', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.28.0.3/32'], 'ipv4_subnet_ids': ['v4subnet-1'], 'ipv4_gateway': '10.28.0.1', 'ipv6_nets': ['2001:db8:1::3/128'], 'ipv6_subnet_ids': ['v6subnet-1'], 'ipv6_gateway': '2001:db8:1::1', 'fqdn': 'calico-vm17.datcon.co.uk' })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-2' ) # Check no further etcd reads. etcd_client.read.assert_not_called() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Notify deletion of the first endpoint. agent.etcd.on_endpoint_delete(None, 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-1') # Check no further etcd reads. etcd_client.read.assert_not_called() # Check DHCP driver was asked to reload allocations. call_driver.assert_called_with('restart', mock.ANY) # Notify another endpoint using a new subnet. agent.etcd.on_endpoint_set(EtcdResponse(value=json.dumps({ 'state': 'active', 'name': 'tapABCD', 'mac': 'fe:16:65:12:33:66', 'profile_ids': ['profile-1'], 'ipv4_nets': ['10.29.0.3/32'], 'ipv4_subnet_ids': ['v4subnet-2'], 'ipv4_gateway': '10.29.0.1', 'ipv6_nets': [], 'ipv6_subnet_ids': [] })), 'hostname-ignored', 'openstack', 'workload-id-ignored', 'endpoint-3' ) # Check expected new subnet was read from etcd. etcd_client.read.assert_has_calls([ mock.call(datamodel_v1.key_for_subnet('v4subnet-2'), consistent=True) ]) etcd_client.read.reset_mock() # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Set the endpoint watcher loop running. eventlet.spawn(agent.etcd.loop) # Report that the subnet watcher noticed a change. agent.etcd.on_subnet_set(None, 'some-subnet-X') eventlet.sleep(0.2) # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY) # Report that the subnet watcher loaded a new snapshot. agent.etcd.subnet_watcher._on_snapshot_loaded('ignored') eventlet.sleep(0.2) # Check DHCP driver was asked to restart. call_driver.assert_called_with('restart', mock.ANY)