Skip to content

Commit 9f5f664

Browse files
committed
Advertise the correct address when using IPv6
Parse the output of "ip route get $IP" taking IPv6 into consideration. Also wrap the IP address in square brackets if it is IPv6. Change-Id: Ifc44e5aa3c5b814b6ceba04461bb68fe1d75c22b Closes-Bug: #1650533
1 parent a6b09f2 commit 9f5f664

File tree

7 files changed

+85
-2
lines changed

7 files changed

+85
-2
lines changed

ironic_python_agent/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _get_route_source(self, dest):
210210
return
211211

212212
try:
213-
return out.strip().split('\n')[0].split('src')[1].strip()
213+
return out.strip().split('\n')[0].split('src')[1].split()[0]
214214
except IndexError:
215215
LOG.warning('No route to host %(dest)s, route record: %(rec)s',
216216
{'dest': dest, 'rec': out})

ironic_python_agent/ironic_api_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from ironic_python_agent import encoding
2222
from ironic_python_agent import errors
23+
from ironic_python_agent import netutils
2324

2425

2526
LOG = log.getLogger(__name__)
@@ -141,5 +142,5 @@ def _do_lookup(self, hardware_info, node_uuid):
141142
raise loopingcall.LoopingCallDone(retvalue=content)
142143

143144
def _get_agent_url(self, advertise_address):
144-
return 'http://{}:{}'.format(advertise_address[0],
145+
return 'http://{}:{}'.format(netutils.wrap_ipv6(advertise_address[0]),
145146
advertise_address[1])

ironic_python_agent/netutils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import netifaces
2323
from oslo_config import cfg
2424
from oslo_log import log as logging
25+
from oslo_utils import netutils
2526

2627
LOG = logging.getLogger(__name__)
2728
CONF = cfg.CONF
@@ -213,3 +214,9 @@ def interface_has_carrier(interface_name):
213214
LOG.debug('No carrier information for interface %s',
214215
interface_name)
215216
return False
217+
218+
219+
def wrap_ipv6(ip):
220+
if netutils.is_valid_ipv6(ip):
221+
return "[%s]" % ip
222+
return ip

ironic_python_agent/tests/unit/test_agent.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,22 @@ def test_get_node_uuid_invalid_node(self):
466466
self.assertRaises(errors.UnknownNodeError,
467467
self.agent.get_node_uuid)
468468

469+
@mock.patch.object(utils, 'execute', autospec=True)
470+
def test_get_route_source(self, mock_execute):
471+
mock_execute.return_value = ('XXX src 1.2.3.4 XXX\n cache', None)
472+
473+
source = self.agent._get_route_source('XXX')
474+
self.assertEqual('1.2.3.4', source)
475+
476+
@mock.patch.object(agent, 'LOG', autospec=True)
477+
@mock.patch.object(utils, 'execute', autospec=True)
478+
def test_get_route_source_indexerror(self, mock_execute, mock_log):
479+
mock_execute.return_value = ('XXX src \n cache', None)
480+
481+
source = self.agent._get_route_source('XXX')
482+
self.assertIsNone(source)
483+
mock_log.warning.assert_called_once()
484+
469485

470486
@mock.patch.object(hardware.GenericHardwareManager, '_wait_for_disks',
471487
lambda self: None)
@@ -597,6 +613,22 @@ def test_route_with_ip(self, mock_exec, mock_gethostbyname):
597613
mock_exec.assert_called_once_with('ip', 'route', 'get', '1.2.1.2')
598614
mock_gethostbyname.assert_called_once_with('1.2.1.2')
599615

616+
def test_route_with_ipv6(self, mock_exec, mock_gethostbyname):
617+
self.agent.api_url = 'http://[fc00:1111::1]:8081/v1'
618+
mock_gethostbyname.side_effect = socket.gaierror()
619+
mock_exec.return_value = (
620+
"""fc00:101::1 dev br-ctlplane src fc00:101::4 metric 0
621+
cache """,
622+
""
623+
)
624+
625+
self.agent.set_agent_advertise_addr()
626+
627+
self.assertEqual(('fc00:101::4', 9990),
628+
self.agent.advertise_address)
629+
mock_exec.assert_called_once_with('ip', 'route', 'get', 'fc00:1111::1')
630+
mock_gethostbyname.assert_called_once_with('fc00:1111::1')
631+
600632
def test_route_with_host(self, mock_exec, mock_gethostbyname):
601633
mock_gethostbyname.return_value = '1.2.1.2'
602634
mock_exec.return_value = (

ironic_python_agent/tests/unit/test_ironic_api_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,29 @@ def test_successful_heartbeat(self):
7070

7171
heartbeat_path = 'v1/heartbeat/deadbeef-dabb-ad00-b105-f00d00bab10c'
7272
request_args = self.api_client.session.request.call_args[0]
73+
data = self.api_client.session.request.call_args[1]['data']
7374
self.assertEqual('POST', request_args[0])
7475
self.assertEqual(API_URL + heartbeat_path, request_args[1])
76+
self.assertEqual('{"callback_url": "http://192.0.2.1:9999"}', data)
77+
78+
def test_successful_heartbeat_ip6(self):
79+
response = FakeResponse(status_code=202)
80+
81+
self.api_client.session.request = mock.Mock()
82+
self.api_client.session.request.return_value = response
83+
84+
self.api_client.heartbeat(
85+
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
86+
advertise_address=('fc00:1111::4', '9999')
87+
)
88+
89+
heartbeat_path = 'v1/heartbeat/deadbeef-dabb-ad00-b105-f00d00bab10c'
90+
request_args = self.api_client.session.request.call_args[0]
91+
data = self.api_client.session.request.call_args[1]['data']
92+
self.assertEqual('POST', request_args[0])
93+
self.assertEqual(API_URL + heartbeat_path, request_args[1])
94+
self.assertEqual('{"callback_url": "http://[fc00:1111::4]:9999"}',
95+
data)
7596

7697
def test_heartbeat_requests_exception(self):
7798
self.api_client.session.request = mock.Mock()
@@ -240,3 +261,11 @@ def test_do_lookup_bad_response_body(self):
240261
node_uuid=None)
241262

242263
self.assertFalse(error)
264+
265+
def test_get_agent_url_ipv4(self):
266+
url = self.api_client._get_agent_url(('1.2.3.4', '9999'))
267+
self.assertEqual('http://1.2.3.4:9999', url)
268+
269+
def test_get_agent_url_ipv6(self):
270+
url = self.api_client._get_agent_url(('1:2::3:4', '9999'))
271+
self.assertEqual('http://[1:2::3:4]:9999', url)

ironic_python_agent/tests/unit/test_netutils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,11 @@ def _run_with_exception():
336336

337337
sock1.close.assert_called_once_with()
338338
sock2.close.assert_called_once_with()
339+
340+
def test_wrap_ipv6(self):
341+
res = netutils.wrap_ipv6('1:2::3:4')
342+
self.assertEqual('[1:2::3:4]', res)
343+
344+
def test_wrap_ipv6_with_ipv4(self):
345+
res = netutils.wrap_ipv6('1.2.3.4')
346+
self.assertEqual('1.2.3.4', res)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- Ironic Python Agent now correctly detects its IPv6 address
4+
if communicating with ironic over IPv6. This address is also
5+
wrapped in square brackets if the address being advertised
6+
is IPv6.

0 commit comments

Comments
 (0)