From 53ac6823fa3d8b618578b9cf101b53a00d9b75d4 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Sat, 30 Jul 2016 23:59:34 +0000 Subject: [PATCH] Fixes Octavia handling of subnets without DHCP Currently Octavia assumes that DHCP service is available on the VIP and member subnets. This is not the case at all operators. This patch makes Octavia use the IP information provided when the ports are created, if available. If the IP information is not available on the ports it will fall back to relying on DHCP. Change-Id: I08a93d4318bbce48128019376320782d1a334369 Closes-Bug: #1607900 --- .../backends/agent/api_server/plug.py | 83 +++++++++++-- .../backends/agent/api_server/server.py | 6 +- .../templates/plug_port_ethX.conf.j2 | 12 +- .../templates/plug_vip_ethX.conf.j2 | 13 +- .../drivers/haproxy/rest_api_driver.py | 14 ++- .../backend/agent/api_server/test_server.py | 111 +++++++++++++++++- .../agent/api_server/test_server_sysvinit.py | 8 +- .../drivers/haproxy/test_rest_api_driver.py | 39 ++++-- ...etworks-without-dhcp-3458a063333ab7a8.yaml | 5 + requirements.txt | 2 +- 10 files changed, 260 insertions(+), 33 deletions(-) create mode 100644 releasenotes/notes/support-networks-without-dhcp-3458a063333ab7a8.yaml diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 58c2274176..8a94f72066 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -1,4 +1,5 @@ # Copyright 2015 Hewlett-Packard Development Company, L.P. +# Copyright 2016 Rackspace # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -20,9 +21,12 @@ import stat import subprocess import flask +import ipaddress import jinja2 import netifaces +from oslo_config import cfg import pyroute2 +import six from werkzeug import exceptions from octavia.amphorae.backends.agent.api_server import util @@ -30,6 +34,9 @@ from octavia.common import constants as consts from octavia.i18n import _LE, _LI +CONF = cfg.CONF +CONF.import_group('amphora_agent', 'octavia.common.config') + ETH_PORT_CONF = 'plug_vip_ethX.conf.j2' ETH_X_VIP_CONF = 'plug_port_ethX.conf.j2' @@ -42,7 +49,7 @@ template_port = j2_env.get_template(ETH_X_VIP_CONF) template_vip = j2_env.get_template(ETH_PORT_CONF) -def plug_vip(vip, subnet_cidr, gateway, mac_address): +def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip): # validate vip try: socket.inet_aton(vip) @@ -72,15 +79,25 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC # mode 00644 mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - with os.fdopen(os.open(name, flags, mode), 'w') as file: - file.write('auto lo\n') - file.write('iface lo inet loopback\n') - file.write('source /etc/netns/{}/network/interfaces.d/*.cfg\n'.format( - consts.AMPHORA_NAMESPACE)) + with os.fdopen(os.open(name, flags, mode), 'w') as int_file: + int_file.write('auto lo\n') + int_file.write('iface lo inet loopback\n') + if not CONF.amphora_agent.agent_server_network_file: + int_file.write('source /etc/netns/{}/network/' + 'interfaces.d/*.cfg\n'.format( + consts.AMPHORA_NAMESPACE)) # write interface file + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + # If we are using a consolidated interfaces file, just append + # otherwise clear the per interface file as we are rewriting it + # TODO(johnsom): We need a way to clean out old interfaces records + if CONF.amphora_agent.agent_server_network_file: + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + else: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: @@ -89,7 +106,9 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address): vip=vip, broadcast=broadcast, # assume for now only a fixed subnet size - netmask='255.255.255.0') + netmask='255.255.255.0', + gateway=gateway, + vrrp_ip=vrrp_ip) text_file.write(text) # Update the list of interfaces to add to the namespace @@ -117,7 +136,7 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address): vip=vip, interface=interface))), 202) -def plug_network(mac_address): +def plug_network(mac_address, fixed_ips): # This is the interface as it was initially plugged into the # default network namespace, this will likely always be eth1 default_netns_interface = _interface_by_mac(mac_address) @@ -138,14 +157,54 @@ def plug_network(mac_address): interface_file_path = util.get_network_interface_file(netns_interface) # write interface file - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + # If we are using a consolidated interfaces file, just append + # otherwise clear the per interface file as we are rewriting it + # TODO(johnsom): We need a way to clean out old interfaces records + if CONF.amphora_agent.agent_server_network_file: + flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND + else: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + # mode 00644 mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: - text = template_port.render(interface=netns_interface) - text_file.write(text) + if fixed_ips is None: + text = template_port.render(interface=netns_interface) + text_file.write(text) + else: + ip_count = -1 + for fixed_ip in fixed_ips: + if ip_count == -1: + netns_ip_interface = netns_interface + ip_count += 1 + else: + netns_ip_interface = "{int}:{ip}".format( + int=netns_interface, ip=ip_count) + ip_count += 1 + try: + ip_addr = fixed_ip['ip_address'] + cidr = fixed_ip['subnet_cidr'] + ip = ipaddress.ip_address( + ip_addr if six.text_type == type( + ip_addr) else six.u(ip_addr)) + network = ipaddress.ip_network( + cidr if six.text_type == type( + cidr) else six.u(cidr)) + broadcast = network.broadcast_address.exploded + netmask = (network.prefixlen if ip.version is 6 + else network.netmask.exploded) + except ValueError: + return flask.make_response(flask.jsonify(dict( + message="Invalid network IP")), 400) + text = template_port.render(interface=netns_ip_interface, + ipv6=ip.version is 6, + ip_address=ip.exploded, + broadcast=broadcast, + netmask=netmask) + text_file.write(text) # Update the list of interfaces to add to the namespace _update_plugged_interfaces_file(netns_interface, mac_address) diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index faada5e8b9..dc1532e2fc 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -122,7 +122,8 @@ def plug_vip(vip): return plug.plug_vip(vip, net_info['subnet_cidr'], net_info['gateway'], - net_info['mac_address']) + net_info['mac_address'], + net_info.get('vrrp_ip')) @app.route('/' + api_server.VERSION + '/plug/network', methods=['POST']) @@ -133,7 +134,8 @@ def plug_network(): assert 'mac_address' in port_info except Exception: raise exceptions.BadRequest(description='Invalid port information') - return plug.plug_network(port_info['mac_address']) + return plug.plug_network(port_info['mac_address'], + port_info.get('fixed_ips')) @app.route('/' + api_server.VERSION + '/certificate', methods=['PUT']) diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 index 8a4f57245d..6f0a45e957 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 @@ -14,5 +14,15 @@ # under the License. #} # Generated by Octavia agent -auto {{ interface }} {{ interface }}:0 +auto {{ interface }} +{%- if ip_address %} +iface {{ interface }} inet{{ '6' if ipv6 }} static +address {{ ip_address }} +broadcast {{ broadcast }} +netmask {{ netmask }} +{%- else %} iface {{ interface }} inet dhcp +auto {{ interface }}:0 +iface {{ interface }}:0 inet6 auto +{%- endif %} + diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 index 9b8f497ac9..43985e5b08 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 @@ -1,5 +1,6 @@ {# # Copyright 2015 Hewlett-Packard Development Company, L.P. +# Copyright 2016 Rackspace # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -15,8 +16,18 @@ #} # Generated by Octavia agent auto {{ interface }} {{ interface }}:0 + +{%- if vrrp_ip %} +iface {{ interface }} inet static +address {{ vrrp_ip }} +broadcast {{ broadcast }} +netmask {{ netmask }} +gateway {{ gateway }} +{%- else %} iface {{ interface }} inet dhcp +{%- endif %} + iface {{ interface }}:0 inet static address {{ vip }} broadcast {{ broadcast }} -netmask {{ netmask }} \ No newline at end of file +netmask {{ netmask }} diff --git a/octavia/amphorae/drivers/haproxy/rest_api_driver.py b/octavia/amphorae/drivers/haproxy/rest_api_driver.py index f20cbc533a..779fdee508 100644 --- a/octavia/amphorae/drivers/haproxy/rest_api_driver.py +++ b/octavia/amphorae/drivers/haproxy/rest_api_driver.py @@ -123,16 +123,26 @@ class HaproxyAmphoraLoadBalancerDriver( # tight coupling between the network driver and amphora # driver. We will need to revisit this to try and remove # this tight coupling. + # NOTE (johnsom): I am loading the vrrp_ip into the + # net_info structure here so that I don't break + # compatibility with old amphora agent versions. port = amphorae_network_config.get(amp.id).vrrp_port net_info = {'subnet_cidr': subnet.cidr, 'gateway': subnet.gateway_ip, - 'mac_address': port.mac_address} + 'mac_address': port.mac_address, + 'vrrp_ip': amp.vrrp_ip} self.client.plug_vip(amp, load_balancer.vip.ip_address, net_info) def post_network_plug(self, amphora, port): - port_info = {'mac_address': port.mac_address} + fixed_ips = [] + for fixed_ip in port.fixed_ips: + ip = {'ip_address': fixed_ip.ip_address, + 'subnet_cidr': fixed_ip.subnet.cidr} + fixed_ips.append(ip) + port_info = {'mac_address': port.mac_address, + 'fixed_ips': fixed_ips} self.client.plug_network(amphora, port_info) def get_vrrp_interface(self, amphora): diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index 1983305ccb..719d91d35b 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -552,13 +552,118 @@ class ServerTestCase(base.TestCase): handle = m() handle.write.assert_any_call( '\n# Generated by Octavia agent\n' - 'auto eth' + test_int_num + ' eth' + test_int_num + - ':0\niface eth' + test_int_num + ' inet dhcp') + 'auto eth' + test_int_num + + '\niface eth' + test_int_num + ' inet dhcp\n' + 'auto eth' + test_int_num + ':0\n' + 'iface eth' + test_int_num + ':0 inet6 auto\n') mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, 'ifup', 'eth' + test_int_num], stderr=-2) + # fixed IPs happy path + port_info = {'mac_address': '123', 'fixed_ips': [ + {'ip_address': '10.0.0.5', 'subnet_cidr': '10.0.0.0/24'}]} + mock_interfaces.side_effect = [['blah']] + mock_ifaddress.side_effect = [[netifaces.AF_LINK], + {netifaces.AF_LINK: [{'addr': '123'}]}] + + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( + consts.AMPHORA_NAMESPACE, test_int_num) + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open + with mock.patch('os.open') as mock_open, mock.patch.object( + os, 'fdopen', m) as mock_fdopen: + mock_open.return_value = 123 + rv = self.app.post('/' + api_server.VERSION + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + self.assertEqual(202, rv.status_code) + + mock_open.assert_any_call(file_name, flags, mode) + mock_fdopen.assert_any_call(123, 'w') + + plug_inf_file = '/var/lib/octavia/plugged_interfaces' + flags = os.O_RDWR | os.O_CREAT + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + mock_open.assert_any_call(plug_inf_file, flags, mode) + mock_fdopen.assert_any_call(123, 'r+') + + handle = m() + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto eth' + test_int_num + + '\niface eth' + test_int_num + ' inet static\n' + + 'address 10.0.0.5\nbroadcast 10.0.0.255\n' + + 'netmask 255.255.255.0\n') + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'ifup', 'eth' + test_int_num], stderr=-2) + + # fixed IPs happy path IPv6 + port_info = {'mac_address': '123', 'fixed_ips': [ + {'ip_address': '2001:db8::2', 'subnet_cidr': '2001:db8::/32'}]} + mock_interfaces.side_effect = [['blah']] + mock_ifaddress.side_effect = [[netifaces.AF_LINK], + {netifaces.AF_LINK: [{'addr': '123'}]}] + + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( + consts.AMPHORA_NAMESPACE, test_int_num) + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open + with mock.patch('os.open') as mock_open, mock.patch.object( + os, 'fdopen', m) as mock_fdopen: + mock_open.return_value = 123 + rv = self.app.post('/' + api_server.VERSION + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + self.assertEqual(202, rv.status_code) + + mock_open.assert_any_call(file_name, flags, mode) + mock_fdopen.assert_any_call(123, 'w') + + plug_inf_file = '/var/lib/octavia/plugged_interfaces' + flags = os.O_RDWR | os.O_CREAT + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + mock_open.assert_any_call(plug_inf_file, flags, mode) + mock_fdopen.assert_any_call(123, 'r+') + + handle = m() + handle.write.assert_any_call( + '\n# Generated by Octavia agent\n' + 'auto eth' + test_int_num + + '\niface eth' + test_int_num + ' inet6 static\n' + + 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' + 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' + + 'netmask 32\n') + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'ifup', 'eth' + test_int_num], stderr=-2) + + # fixed IPs, bogus IP + port_info = {'mac_address': '123', 'fixed_ips': [ + {'ip_address': '10005', 'subnet_cidr': '10.0.0.0/24'}]} + mock_interfaces.side_effect = [['blah']] + mock_ifaddress.side_effect = [[netifaces.AF_LINK], + {netifaces.AF_LINK: [{'addr': '123'}]}] + + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( + consts.AMPHORA_NAMESPACE, test_int_num) + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open + with mock.patch('os.open') as mock_open, mock.patch.object( + os, 'fdopen', m) as mock_fdopen: + mock_open.return_value = 123 + rv = self.app.post('/' + api_server.VERSION + "/plug/network", + content_type='application/json', + data=json.dumps(port_info)) + self.assertEqual(400, rv.status_code) + # same as above but ifup fails + port_info = {'mac_address': '123', 'fixed_ips': [ + {'ip_address': '10.0.0.5', 'subnet_cidr': '10.0.0.0/24'}]} mock_interfaces.side_effect = [['blah']] mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] @@ -654,7 +759,7 @@ class ServerTestCase(base.TestCase): handle.write.assert_any_call( '\n# Generated by Octavia agent\n' 'auto blah blah:0\n' - 'iface blah inet dhcp\n' + 'iface blah inet dhcp\n\n' 'iface blah:0 inet static\n' 'address 203.0.113.2\n' 'broadcast 203.0.113.255\n' diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py index 59735fcd81..71fb20cc44 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py @@ -558,8 +558,10 @@ class ServerTestCase(base.TestCase): handle = m() handle.write.assert_any_call( '\n# Generated by Octavia agent\n' - 'auto eth' + test_int_num + ' eth' + test_int_num + ':0\n' - 'iface eth' + test_int_num + ' inet dhcp') + 'auto eth' + test_int_num + '\n' + 'iface eth' + test_int_num + ' inet dhcp\n' + 'auto eth' + test_int_num + ':0\n' + 'iface eth' + test_int_num + ':0 inet6 auto\n') mock_check_output.assert_called_with( ['ip', 'netns', 'exec', 'amphora-haproxy', 'ifup', 'eth' + test_int_num], stderr=-2) @@ -660,7 +662,7 @@ class ServerTestCase(base.TestCase): handle.write.assert_any_call( '\n# Generated by Octavia agent\n' 'auto blah blah:0\n' - 'iface blah inet dhcp\n' + 'iface blah inet dhcp\n\n' 'iface blah:0 inet static\n' 'address 203.0.113.2\n' 'broadcast 203.0.113.255\n' diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py index 7b9bf3faf2..e6d888f9bf 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver.py @@ -32,11 +32,9 @@ FAKE_CIDR = '10.0.0.0/24' FAKE_GATEWAY = '10.0.0.1' FAKE_IP = 'fake' FAKE_PEM_FILENAME = "file_name" -FAKE_SUBNET_INFO = {'subnet_cidr': FAKE_CIDR, - 'gateway': FAKE_GATEWAY, - 'mac_address': '123'} FAKE_UUID_1 = uuidutils.generate_uuid() FAKE_VRRP_IP = '10.1.0.1' +FAKE_MAC_ADDRESS = '123' class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): @@ -55,7 +53,16 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.amp = self.sl.load_balancer.amphorae[0] self.sv = sample_configs.sample_vip_tuple() self.lb = self.sl.load_balancer - self.port = network_models.Port(mac_address='123') + self.fixed_ip = mock.MagicMock() + self.fixed_ip.ip_address = '10.0.0.5' + self.fixed_ip.subnet.cidr = '10.0.0.0/24' + self.port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[self.fixed_ip]) + + self.subnet_info = {'subnet_cidr': FAKE_CIDR, + 'gateway': FAKE_GATEWAY, + 'mac_address': FAKE_MAC_ADDRESS, + 'vrrp_ip': self.amp.vrrp_ip} @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') @mock.patch('octavia.common.tls_utils.cert_parser.get_host_names') @@ -154,12 +161,23 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): amphorae_network_config.get().vrrp_port = self.port self.driver.post_vip_plug(self.lb, amphorae_network_config) self.driver.client.plug_vip.assert_called_once_with( - self.amp, self.lb.vip.ip_address, FAKE_SUBNET_INFO) + self.amp, self.lb.vip.ip_address, self.subnet_info) def test_post_network_plug(self): + # Test dhcp path + port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[]) + self.driver.post_network_plug(self.amp, port) + self.driver.client.plug_network.assert_called_once_with( + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[])) + + self.driver.client.plug_network.reset_mock() + + # Test fixed IP path self.driver.post_network_plug(self.amp, self.port) self.driver.client.plug_network.assert_called_once_with( - self.amp, dict(mac_address='123')) + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[dict(ip_address='10.0.0.5', + subnet_cidr='10.0.0.0/24')])) def test_get_vrrp_interface(self): self.driver.get_vrrp_interface(self.amp) @@ -174,11 +192,16 @@ class TestAmphoraAPIClientTest(base.TestCase): self.driver = driver.AmphoraAPIClient() self.base_url = "https://127.0.0.1:9443/0.5" self.amp = models.Amphora(lb_network_ip='127.0.0.1', compute_id='123') - self.port_info = dict(mac_address='123') + self.port_info = dict(mac_address=FAKE_MAC_ADDRESS) # Override with much lower values for testing purposes.. conf = oslo_fixture.Config(cfg.CONF) conf.config(group="haproxy_amphora", connection_max_retries=2) + self.subnet_info = {'subnet_cidr': FAKE_CIDR, + 'gateway': FAKE_GATEWAY, + 'mac_address': FAKE_MAC_ADDRESS, + 'vrrp_ip': self.amp.vrrp_ip} + def test_request(self): self.assertRaises(driver_except.TimeOutException, self.driver.request, @@ -691,7 +714,7 @@ class TestAmphoraAPIClientTest(base.TestCase): m.post("{base}/plug/vip/{vip}".format( base=self.base_url, vip=FAKE_IP) ) - self.driver.plug_vip(self.amp, FAKE_IP, FAKE_SUBNET_INFO) + self.driver.plug_vip(self.amp, FAKE_IP, self.subnet_info) self.assertTrue(m.called) @requests_mock.mock() diff --git a/releasenotes/notes/support-networks-without-dhcp-3458a063333ab7a8.yaml b/releasenotes/notes/support-networks-without-dhcp-3458a063333ab7a8.yaml new file mode 100644 index 0000000000..287c516e90 --- /dev/null +++ b/releasenotes/notes/support-networks-without-dhcp-3458a063333ab7a8.yaml @@ -0,0 +1,5 @@ +--- +features: + - Adds support for networks that do not have DHCP services enabled. +upgrade: + - To support networks without DHCP you must upgrade your amphora image. diff --git a/requirements.txt b/requirements.txt index de26d83a9e..550a9ecb49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,5 +38,5 @@ taskflow>=1.26.0 # Apache-2.0 #for the amphora api Flask!=0.11,<1.0,>=0.10 # BSD netifaces>=0.10.4 # MIT - +ipaddress>=1.0.7;python_version<'3.3' # PSF cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0