Skip to content

Commit b4f1a0b

Browse files
committed
Tries to advertise valid default IP
During the first heartbeat, the heartbeater asks the agent to check its advertised address; if the advertised IP is still the default (None), the agent tries to replace it with the IP of the first network interface it finds. If it fails to find either a network interface or an IP address, the agent raises an exception. Change-Id: I6d435d39e99ed0ff5c8b4883b6aa0b356f6cb4ae Closes-Bug: #1309110
1 parent 263f97c commit b4f1a0b

File tree

6 files changed

+137
-1
lines changed

6 files changed

+137
-1
lines changed

ironic_python_agent/agent.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def run(self):
7070
# The first heartbeat happens now
7171
self.log.info('starting heartbeater')
7272
interval = 0
73+
self.agent.set_agent_advertise_addr()
7374

7475
while not self.stop_event.wait(interval):
7576
self.do_heartbeat()
@@ -100,6 +101,7 @@ def stop(self):
100101

101102
class IronicPythonAgent(base.ExecuteCommandMixin):
102103
def __init__(self, api_url, advertise_address, listen_address,
104+
ip_lookup_attempts, ip_lookup_sleep, network_interface,
103105
lookup_timeout, lookup_interval, driver_name):
104106
super(IronicPythonAgent, self).__init__()
105107
self.ext_mgr = extension.ExtensionManager(
@@ -125,6 +127,9 @@ def __init__(self, api_url, advertise_address, listen_address,
125127
# lookup timeout in seconds
126128
self.lookup_timeout = lookup_timeout
127129
self.lookup_interval = lookup_interval
130+
self.ip_lookup_attempts = ip_lookup_attempts
131+
self.ip_lookup_sleep = ip_lookup_sleep
132+
self.network_interface = network_interface
128133

129134
def get_status(self):
130135
"""Retrieve a serializable status."""
@@ -136,6 +141,45 @@ def get_status(self):
136141
def get_agent_mac_addr(self):
137142
return self.hardware.get_primary_mac_address()
138143

144+
def set_agent_advertise_addr(self):
145+
"""If agent's advertised IP address is still default (None), try to
146+
find a better one. If the agent's network interface is None, replace
147+
that as well.
148+
"""
149+
if self.advertise_address[0] is not None:
150+
return
151+
152+
if self.network_interface is None:
153+
ifaces = self.get_agent_network_interfaces()
154+
else:
155+
ifaces = [self.network_interface]
156+
157+
attempts = 0
158+
while (attempts < self.ip_lookup_attempts):
159+
for iface in ifaces:
160+
found_ip = self.hardware.get_ipv4_addr(iface)
161+
if found_ip is not None:
162+
self.advertise_address = (found_ip,
163+
self.advertise_address[1])
164+
self.network_interface = iface
165+
return
166+
attempts += 1
167+
time.sleep(self.ip_lookup_sleep)
168+
169+
raise errors.LookupAgentIPError('Agent could not find a valid IP '
170+
'address.')
171+
172+
def get_agent_network_interfaces(self):
173+
iface_list = [iface.serialize()['name'] for iface in
174+
self.hardware.list_network_interfaces()]
175+
iface_list = [name for name in iface_list if 'lo' not in name]
176+
177+
if len(iface_list) == 0:
178+
raise errors.LookupAgentInterfaceError('Agent could not find a '
179+
'valid network interface.')
180+
else:
181+
return iface_list
182+
139183
def get_node_uuid(self):
140184
if 'uuid' not in self.node:
141185
errors.HeartbeatError('Tried to heartbeat without node UUID.')

ironic_python_agent/cmd/agent.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def _get_kernel_params():
5151
help='The port to listen on'),
5252

5353
cfg.StrOpt('advertise-host',
54-
default=KPARAMS.get('ipa-advertise-host', '0.0.0.0'),
54+
default=KPARAMS.get('ipa-advertise-host', None),
5555
help='The host to tell Ironic to reply and send '
5656
'commands to.'),
5757

@@ -60,6 +60,21 @@ def _get_kernel_params():
6060
help='The port to tell Ironic to reply and send '
6161
'commands to.'),
6262

63+
cfg.IntOpt('ip-lookup-attempts',
64+
default=int(KPARAMS.get('ipa-ip-lookup-attempts', 3)),
65+
help='The number of times to try and automatically'
66+
'determine the agent IPv4 address.'),
67+
68+
cfg.IntOpt('ip-lookup-sleep',
69+
default=int(KPARAMS.get('ipa-ip-lookup-timeout', 10)),
70+
help='The amaount of time to sleep between attempts'
71+
'to determine IP address.'),
72+
73+
cfg.StrOpt('network-interface',
74+
default=KPARAMS.get('ipa-network-interface', None),
75+
help='The interface to use when looking for an IP'
76+
'address.'),
77+
6378
cfg.IntOpt('lookup-timeout',
6479
default=int(KPARAMS.get('ipa-lookup-timeout', 300)),
6580
help='The amount of time to retry the initial lookup '
@@ -88,6 +103,9 @@ def run():
88103
agent.IronicPythonAgent(CONF.api_url,
89104
(CONF.advertise_host, CONF.advertise_port),
90105
(CONF.listen_host, CONF.listen_port),
106+
CONF.ip_lookup_attempts,
107+
CONF.ip_lookup_sleep,
108+
CONF.network_interface,
91109
CONF.lookup_timeout,
92110
CONF.lookup_interval,
93111
CONF.driver_name).run()

ironic_python_agent/errors.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,24 @@ def __init__(self, details):
115115
super(LookupNodeError, self).__init__(details)
116116

117117

118+
class LookupAgentIPError(IronicAPIError):
119+
"""Error raised when automatic IP lookup fails."""
120+
121+
message = 'Error finding IP for Ironic Agent'
122+
123+
def __init__(self, details):
124+
super(IronicAPIError, self).__init__(details)
125+
126+
127+
class LookupAgentInterfaceError(IronicAPIError):
128+
"""Error raised when agent interface lookup fails."""
129+
130+
message = 'Error finding network interface for Ironic Agent'
131+
132+
def __init__(self, details):
133+
super(IronicAPIError, self).__init__(details)
134+
135+
118136
class ImageDownloadError(RESTError):
119137
"""Error raised when an image cannot be downloaded."""
120138

ironic_python_agent/hardware.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import functools
1717
import os
1818

19+
import netifaces
1920
import psutil
2021
import six
2122
import stevedore
@@ -170,6 +171,14 @@ def _get_interface_info(self, interface_name):
170171

171172
return NetworkInterface(interface_name, mac_addr)
172173

174+
def get_ipv4_addr(self, interface_id):
175+
try:
176+
addrs = netifaces.ifaddresses(interface_id)
177+
return addrs[netifaces.AF_INET][0]['addr']
178+
except (ValueError, IndexError):
179+
# No default IPv4 address found
180+
return None
181+
173182
def _is_device(self, interface_name):
174183
device_path = '{0}/class/net/{1}/device'.format(self.sys_path,
175184
interface_name)

ironic_python_agent/tests/agent.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ def setUp(self):
133133
'org:8081/',
134134
('203.0.113.1', 9990),
135135
('192.0.2.1', 9999),
136+
3,
137+
10,
138+
'eth0',
136139
300,
137140
1,
138141
'agent_ipmitool')
@@ -186,6 +189,49 @@ def test_run(self, mocked_list_hardware, wsgi_server_cls):
186189

187190
self.agent.heartbeater.start.assert_called_once_with()
188191

192+
@mock.patch('time.sleep', return_value=None)
193+
def test_ipv4_lookup(self, mock_time_sleep):
194+
homeless_agent = agent.IronicPythonAgent('https://fake_api.example.'
195+
'org:8081/',
196+
(None, 9990),
197+
('192.0.2.1', 9999),
198+
3,
199+
10,
200+
None,
201+
300,
202+
1,
203+
'agent_ipmitool')
204+
205+
homeless_agent.hardware = mock.Mock()
206+
mock_list_net = homeless_agent.hardware.list_network_interfaces
207+
mock_get_ipv4 = homeless_agent.hardware.get_ipv4_addr
208+
209+
homeless_agent.heartbeater.stop_event.wait = mock.Mock()
210+
homeless_agent.heartbeater.stop_event.wait.return_value = True
211+
212+
# Can't find network interfaces, and therefore can't find IP
213+
mock_list_net.return_value = []
214+
mock_get_ipv4.return_value = None
215+
self.assertRaises(errors.LookupAgentInterfaceError,
216+
homeless_agent.set_agent_advertise_addr)
217+
218+
# Can look up network interfaces, but not IP. Network interface not
219+
# set, because no interface yields an IP.
220+
mock_ifaces = [hardware.NetworkInterface('eth0', '00:00:00:00:00:00'),
221+
hardware.NetworkInterface('eth1', '00:00:00:00:00:01')]
222+
mock_list_net.return_value = mock_ifaces
223+
224+
self.assertRaises(errors.LookupAgentIPError,
225+
homeless_agent.set_agent_advertise_addr)
226+
self.assertEqual(6, mock_get_ipv4.call_count)
227+
self.assertEqual(None, homeless_agent.network_interface)
228+
229+
# First interface eth0 has no IP, second interface eth1 has an IP
230+
mock_get_ipv4.side_effect = [None, '1.1.1.1']
231+
homeless_agent.heartbeater.run()
232+
self.assertEqual(('1.1.1.1', 9990), homeless_agent.advertise_address)
233+
self.assertEqual('eth1', homeless_agent.network_interface)
234+
189235
def test_async_command_success(self):
190236
result = base.AsyncCommandResult('foo_command', {'fail': False},
191237
foo_execute)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Babel>=1.3
1010
iso8601>=0.1.9
1111
oslotest==1.0
1212
psutil>=1.1.1
13+
netifaces>=0.10.4

0 commit comments

Comments
 (0)