Support for associating and disassociating neutron floating IPs

This adds support for creating and removing DNS A records when
floating IPs are associated and disassociated in neutron.
novajoin-install and functional tests are enhanced to test it.

Change-Id: I82c83ad9e8c84ddfd4ecfc4d5c3b31a418af97a7
This commit is contained in:
Grzegorz Grasza 2018-11-22 11:57:39 +01:00
parent 7fa5789e51
commit 4d997dddc6
5 changed files with 85 additions and 26 deletions

View File

@ -511,16 +511,27 @@ class IPAClient(IPANovaJoinBase):
LOG.debug('IPA is not configured') LOG.debug('IPA is not configured')
return return
params = [{"__dns_name__": get_domain() + "."}, params = [six.text_type(get_domain() + '.'),
{"__dns_name__": hostname}] six.text_type(hostname)]
kw = {'a_part_ip_address': floating_ip} kw = {'a_part_ip_address': six.text_type(floating_ip)}
try: try:
self._call_ipa('dnsrecord_add', *params, **kw) self._call_ipa('dnsrecord_add', *params, **kw)
except (errors.DuplicateEntry, errors.ValidationError): except (errors.DuplicateEntry, errors.ValidationError):
pass pass
def remove_ip(self, hostname, floating_ip): def find_record(self, floating_ip):
"""Find DNS A record for floating IP address"""
LOG.debug('looking up host for floating ip' + floating_ip)
params = [six.text_type(get_domain() + '.')]
service_args = {'arecord': six.text_type(floating_ip)}
result = self._call_ipa('dnsrecord_find', *params, **service_args)
if result['count'] == 0:
return
assert(result['count'] == 1)
return result['result'][0]['idnsname'][0].to_unicode()
def remove_ip(self, floating_ip):
"""Remove a floating IP from a given hostname.""" """Remove a floating IP from a given hostname."""
LOG.debug('In remove_ip') LOG.debug('In remove_ip')
@ -528,4 +539,12 @@ class IPAClient(IPANovaJoinBase):
LOG.debug('IPA is not configured') LOG.debug('IPA is not configured')
return return
LOG.debug('Current a no-op') hostname = self.find_record(floating_ip)
if not hostname:
LOG.debug('floating IP record not found')
return
params = [six.text_type(get_domain() + '.'), hostname]
service_args = {'arecord': six.text_type(floating_ip)}
self._call_ipa('dnsrecord_del', *params, **service_args)

View File

@ -172,13 +172,7 @@ class NotificationEndpoint(object):
floating_ip = payload.get('floating_ip') floating_ip = payload.get('floating_ip')
LOG.info("Disassociate floating IP %s" % floating_ip) LOG.info("Disassociate floating IP %s" % floating_ip)
ipa = ipaclient() ipa = ipaclient()
nova = novaclient() ipa.remove_ip(floating_ip)
server = nova.servers.get(payload.get('instance_id'))
if server:
ipa.remove_ip(server.name, floating_ip)
else:
LOG.error("Could not resolve %s into a hostname",
payload.get('instance_id'))
@event_handlers('floatingip.update.end') @event_handlers('floatingip.update.end')
def floating_ip_update(self, payload): def floating_ip_update(self, payload):
@ -186,20 +180,24 @@ class NotificationEndpoint(object):
floatingip = payload.get('floatingip') floatingip = payload.get('floatingip')
floating_ip = floatingip.get('floating_ip_address') floating_ip = floatingip.get('floating_ip_address')
port_id = floatingip.get('port_id') port_id = floatingip.get('port_id')
LOG.info("Neutron floating IP associate: %s" % floating_ip)
ipa = ipaclient() ipa = ipaclient()
nova = novaclient() if port_id:
neutron = neutronclient() LOG.info("Neutron floating IP associate: %s" % floating_ip)
search_opts = {'id': port_id} nova = novaclient()
ports = neutron.list_ports(**search_opts).get('ports') neutron = neutronclient()
if len(ports) == 1: search_opts = {'id': port_id}
device_id = ports[0].get('device_id') ports = neutron.list_ports(**search_opts).get('ports')
if device_id: if len(ports) == 1:
server = nova.servers.get(device_id) device_id = ports[0].get('device_id')
if server: if device_id:
ipa.add_ip(server.name, floating_ip) server = nova.servers.get(device_id)
if server:
ipa.add_ip(server.name, floating_ip)
else:
LOG.error("Expected 1 port, got %d", len(ports))
else: else:
LOG.error("Expected 1 port, got %d", len(ports)) LOG.info("Neutron floating IP disassociate: %s" % floating_ip)
ipa.remove_ip(floating_ip)
def delete_subhosts(self, ipa, hostname_short, metadata): def delete_subhosts(self, ipa, hostname_short, metadata):
"""Delete subhosts and remove VIPs if possible. """Delete subhosts and remove VIPs if possible.

View File

@ -97,10 +97,16 @@ class TestEnrollment(testtools.TestCase):
metadata = {"ipa_enroll": "True"}) metadata = {"ipa_enroll": "True"})
server = self.conn.compute.wait_for_server(self._server) server = self.conn.compute.wait_for_server(self._server)
self.conn.compute.add_floating_ip_to_server(
server, self._ip.floating_ip_address)
return server return server
def _associate_floating_ip(self):
self.conn.compute.add_floating_ip_to_server(
self._server, self._ip.floating_ip_address)
def _disassociate_floating_ip(self):
self.conn.compute.remove_floating_ip_from_server(
self._server, self._ip.floating_ip_address)
def _delete_server(self): def _delete_server(self):
if self._server: if self._server:
self.conn.compute.delete_server(self._server) self.conn.compute.delete_server(self._server)
@ -116,8 +122,25 @@ class TestEnrollment(testtools.TestCase):
self.assertFalse( self.assertFalse(
self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN)) self.ipaclient.find_host(TEST_INSTANCE + EXAMPLE_DOMAIN))
@loopingcall.RetryDecorator(50, 5, 5, (AssertionError,))
def _check_ip_record_added(self):
self.assertTrue(
self.ipaclient.find_record(self._ip.floating_ip_address))
@loopingcall.RetryDecorator(50, 5, 5, (AssertionError,))
def _check_ip_record_removed(self):
self.assertFalse(
self.ipaclient.find_record(self._ip.floating_ip_address))
def test_enroll_server(self): def test_enroll_server(self):
self._create_server() self._create_server()
self._associate_floating_ip()
self._check_ipa_client_created() self._check_ipa_client_created()
self._check_ip_record_added()
self._disassociate_floating_ip()
self._check_ip_record_removed()
self._associate_floating_ip()
self._check_ip_record_added()
self._delete_server() self._delete_server()
self._check_ipa_client_deleted() self._check_ipa_client_deleted()
self._check_ip_record_removed()

View File

@ -36,6 +36,10 @@
become: true become: true
become_user: stack become_user: stack
- name: Restart neutron services
command: systemctl restart devstack@q-*
become: true
- name: Restart nova services - name: Restart nova services
command: systemctl restart devstack@n-* command: systemctl restart devstack@n-*
become: true become: true

View File

@ -33,6 +33,7 @@ from urllib3.util import parse_url
IPACONF = '/etc/ipa/default.conf' IPACONF = '/etc/ipa/default.conf'
NOVACONF = '/etc/nova/nova.conf' NOVACONF = '/etc/nova/nova.conf'
NOVACPUCONF = '/etc/nova/nova-cpu.conf' NOVACPUCONF = '/etc/nova/nova-cpu.conf'
NEUTRONCONF = '/etc/neutron/neutron.conf'
JOINCONF = '/etc/novajoin/join.conf' JOINCONF = '/etc/novajoin/join.conf'
@ -202,6 +203,17 @@ def install(opts):
with open(conf, 'w') as f: with open(conf, 'w') as f:
config.write(f) config.write(f)
config = SafeConfigParser()
config.read(opts.neutron_conf)
config.set('oslo_messaging_notifications',
'driver',
'messagingv2')
config.set('oslo_messaging_notifications',
'topics',
'notifications,novajoin_notifications')
with open(opts.neutron_conf, 'w') as f:
config.write(f)
if transport_url: if transport_url:
join_config = SafeConfigParser() join_config = SafeConfigParser()
@ -241,6 +253,9 @@ def parse_args():
parser.add_argument('--nova-cpu-conf', dest='nova_cpu_conf', parser.add_argument('--nova-cpu-conf', dest='nova_cpu_conf',
help='nova compute configuration file', help='nova compute configuration file',
default=NOVACPUCONF) default=NOVACPUCONF)
parser.add_argument('--neutron-conf', dest='neutron_conf',
help='neutron configuration file',
default=NEUTRONCONF)
parser.add_argument('--novajoin-conf', dest='novajoin_conf', parser.add_argument('--novajoin-conf', dest='novajoin_conf',
help='novajoin configuration file', help='novajoin configuration file',
default=JOINCONF) default=JOINCONF)