diff --git a/ansible/main.yml b/ansible/main.yml index ca3f688..b329bd4 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -21,6 +21,7 @@ - include: tasks/base.yml - include: tasks/astara.yml - include: tasks/bird.yml + - include: tasks/conntrackd.yml - include: tasks/dnsmasq.yml - include: tasks/extras.yml when: install_extras diff --git a/ansible/tasks/conntrackd.yml b/ansible/tasks/conntrackd.yml new file mode 100644 index 0000000..4ed6e52 --- /dev/null +++ b/ansible/tasks/conntrackd.yml @@ -0,0 +1,7 @@ +--- + +- name: install conntrackd + apt: name=conntrackd state=installed install_recommends=no + +- name: install conntrackd notify script to /etc/conntrackd + copy: src=/usr/share/doc/conntrackd/examples/sync/primary-backup.sh dest=/etc/conntrackd/primary-backup.sh mode=0755 diff --git a/astara_router/drivers/conntrackd.conf.template b/astara_router/drivers/conntrackd.conf.template new file mode 100644 index 0000000..0af321a --- /dev/null +++ b/astara_router/drivers/conntrackd.conf.template @@ -0,0 +1,38 @@ +General { + HashSize 8192 + HashLimit 65535 + Syslog on + LockFile /var/lock/conntrackd.lock + UNIX { + Path /var/run/conntrackd.sock + Backlog 20 + } + SocketBufferSize 262142 + SocketBufferSizeMaxGrown 655355 + Filter { + Protocol Accept { + TCP + } + Address Ignore { + IPv4_address 127.0.0.1 + } + } +} +Sync { + Mode FTFW { + } + UDP Default { + {%- if management_ip_version == 4 %} + IPv4_address {{ source_address }} + IPv4_Destination_Address {{ destination_address }} + {%- else %} + IPv6_address {{ source_address }} + IPv6_Destination_Address {{ destination_address }} + {%- endif %} + Port 3780 + Interface {{ interface }} + SndSocketBuffer 24985600 + RcvSocketBuffer 24985600 + Checksum on + } +} diff --git a/astara_router/drivers/conntrackd.py b/astara_router/drivers/conntrackd.py new file mode 100644 index 0000000..b82c282 --- /dev/null +++ b/astara_router/drivers/conntrackd.py @@ -0,0 +1,93 @@ +# Copyright (c) 2016 Akanda, Inc. All Rights Reserved. +# +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import os + +from astara_router.drivers import base +from astara_router import utils + + +class ConntrackdManager(base.Manager): + """ + A class to provide facilities to interact with the conntrackd daemon. + """ + EXECUTABLE = 'service' + CONFIG_FILE_TEMPLATE = os.path.join( + os.path.dirname(__file__), 'conntrackd.conf.template') + + # Debian defaults + CONFIG_FILE = '/etc/conntrackd/conntrackd.conf' + + # Debian installs this to /usr/share/doc/examples/sync but our + # DIB recipe will install it here. + NOTIFY_SCRIPT = '/etc/conntrackd/primary-backup.sh' + + def __init__(self, root_helper='sudo astara-rootwrap /etc/rootwrap.conf'): + """ + Initializes ConntrackdManager class. + + :type root_helper: str + :param root_helper: System utility used to gain escalate privileges. + """ + super(ConntrackdManager, self).__init__(root_helper) + self._config_templ = utils.load_template(self.CONFIG_FILE_TEMPLATE) + self._should_restart = False + + def save_config(self, config, generic_to_host): + """ + Renders template and writes to the conntrackd file + + :type config: astara_router.models.Configuration + :param config: An astara_router.models.Configuration object containing + the ha_config configuration. + :param generic_to_host: A callable used to resolve generic interface + name to system interface name. + """ + + mgt_interface = None + for interface in config.interfaces: + if interface.management: + mgt_interface = interface + break + mgt_addr = mgt_interface.first_v6 or mgt_interface.first_v4 + ctxt = { + 'source_address': str(mgt_addr), + 'management_ip_version': mgt_addr.version, + 'destination_address': config.ha_config['peers'][0], + 'interface': generic_to_host(interface.ifname), + } + + try: + old_config_hash = utils.hash_file(self.CONFIG_FILE) + except IOError: + old_config_hash = None + + utils.replace_file( + '/tmp/conntrackd.conf', + self._config_templ.render(ctxt)) + utils.execute( + ['mv', '/tmp/conntrackd.conf', self.CONFIG_FILE], + self.root_helper) + + if old_config_hash != utils.hash_file(self.CONFIG_FILE): + self._should_restart = True + + def restart(self): + """ + Restarts the conntrackd daemon if config has been changed + """ + if not self._should_restart: + return + self.sudo('conntrackd', 'restart') diff --git a/astara_router/drivers/keepalived.conf.template b/astara_router/drivers/keepalived.conf.template index 97ab142..0618a81 100644 --- a/astara_router/drivers/keepalived.conf.template +++ b/astara_router/drivers/keepalived.conf.template @@ -1,3 +1,14 @@ +vrrp_sync_group astara_vrrp_group { + group { + {%- for instance in vrrp_instances %} + {{ instance.name }} + {%- endfor %} + } + notify_master "{{ notify_script }} primary" + notify_backup "{{ notify_script }} backup" + notify_fault "{{ notify_script }} fault" +} + {%- for instance in vrrp_instances %} vrrp_instance {{ instance.name }} { native_ipv6 diff --git a/astara_router/drivers/keepalived.py b/astara_router/drivers/keepalived.py index d698139..a47d6d3 100644 --- a/astara_router/drivers/keepalived.py +++ b/astara_router/drivers/keepalived.py @@ -14,7 +14,7 @@ import os -from astara_router.drivers import base +from astara_router.drivers import base, conntrackd from astara_router import utils @@ -84,6 +84,7 @@ class KeepalivedManager(base.Manager): self.config_tmpl = utils.load_template(self.CONFIG_FILE_TEMPLATE) self.peers = [] self.priority = 0 + self.notify_script = conntrackd.ConntrackdManager.NOTIFY_SCRIPT self._last_config_hash = None def set_management_address(self, address): @@ -123,6 +124,7 @@ class KeepalivedManager(base.Manager): return self.config_tmpl.render( priority=self.priority, peers=self.peers, + notify_script=self.notify_script, vrrp_instances=self.instances.values()) def reload(self): diff --git a/astara_router/manager.py b/astara_router/manager.py index 58b7120..3f14864 100644 --- a/astara_router/manager.py +++ b/astara_router/manager.py @@ -20,7 +20,7 @@ import re from astara_router import models from astara_router import settings -from astara_router.drivers import (bird, dnsmasq, ip, metadata, +from astara_router.drivers import (bird, conntrackd, dnsmasq, ip, metadata, iptables, arp, hostname, loadbalancer) @@ -113,8 +113,16 @@ class RouterManager(ServiceManagerBase): self.update_firewall() self.update_routes(cache) self.update_arp() + self.update_conntrackd() self.reload_config() + def update_conntrackd(self): + if not self._config.ha: + return + mgr = conntrackd.ConntrackdManager() + mgr.save_config(self._config, self.ip_mgr.generic_to_host) + mgr.restart() + def update_dhcp(self): mgr = dnsmasq.DHCPManager() mgr.delete_all_config() diff --git a/etc/rootwrap.d/network.filters b/etc/rootwrap.d/network.filters index ad26a03..4ba983d 100644 --- a/etc/rootwrap.d/network.filters +++ b/etc/rootwrap.d/network.filters @@ -9,6 +9,9 @@ mv_bird: RegExpFilter, mv, root, mv, /tmp/bird6\.conf, /etc/bird/bird6\.conf arp: CommandFilter, /usr/sbin/arp, root astara_gratuitous_arp: CommandFilter, astara-gratuitous-arp, root +# astara_router/drivers/conntrackd.py: +mv_conntrackd: RegExpFilter, mv, root, mv, /tmp/conntrackd\.conf, /etc/conntrackd/conntrackd\.conf + # astara_router/drivers/dnsmasq.py: mv_dnsmasq: RegExpFilter, mv, root, mv, /tmp/dnsmasq\.conf, /etc/dnsmasq\.d/.*\.conf rm: CommandFilter, rm, root diff --git a/releasenotes/notes/conntrackd-c179372033f4134e.yaml b/releasenotes/notes/conntrackd-c179372033f4134e.yaml new file mode 100644 index 0000000..914b59d --- /dev/null +++ b/releasenotes/notes/conntrackd-c179372033f4134e.yaml @@ -0,0 +1,4 @@ +--- +features: + - The appliance is now built with conntrackd installed and supports configuring + the connection tracking service among pairs of clustered HA router appliances. diff --git a/test/unit/drivers/test_conntrackd.py b/test/unit/drivers/test_conntrackd.py new file mode 100644 index 0000000..a032b1e --- /dev/null +++ b/test/unit/drivers/test_conntrackd.py @@ -0,0 +1,75 @@ +# Copyright 2016 Akanda, Inc. +# +# Author: Akanda, Inc. +# +# 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 +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import __builtin__ + +from unittest2 import TestCase +import mock + +from test.unit import fakes + +from astara_router.drivers import conntrackd + + +class ConntrackddManagerTestCase(TestCase): + def setUp(self): + super(ConntrackddManagerTestCase, self).setUp() + self.mgr = conntrackd.ConntrackdManager() + self.mgr._config_templ = mock.Mock( + render=mock.Mock() + ) + + @mock.patch('astara_router.utils.execute') + @mock.patch('astara_router.utils.replace_file') + @mock.patch('astara_router.utils.hash_file') + def test_save_config(self, fake_hash, fake_replace, fake_execute): + fake_generic_to_host = mock.Mock(return_value='eth0') + fake_interface = fakes.fake_interface() + fake_mgt_interface = fakes.fake_mgt_interface() + ha_config = { + 'peers': ['10.0.0.2'], + } + fake_config = mock.Mock( + interfaces=[fake_interface, fake_mgt_interface], + ha_config=ha_config, + ) + + fake_hash.side_effect = ['hash1', 'hash2'] + self.mgr._config_templ.render.return_value = 'new_config' + self.mgr.save_config(fake_config, fake_generic_to_host) + self.mgr._config_templ.render.assert_called_with(dict( + source_address=str(fake_mgt_interface.addresses[0].ip), + management_ip_version=4, + destination_address='10.0.0.2', + interface='eth0', + )) + self.assertTrue(self.mgr._should_restart) + fake_replace.assert_called_with('/tmp/conntrackd.conf', 'new_config') + fake_execute.assert_called_with( + ['mv', '/tmp/conntrackd.conf', '/etc/conntrackd/conntrackd.conf'], + self.mgr.root_helper) + + @mock.patch.object(conntrackd.ConntrackdManager, 'sudo') + def test_restart(self, fake_sudo): + self.mgr._should_restart = True + self.mgr.restart() + fake_sudo.assert_called_with('conntrackd', 'restart') + + @mock.patch.object(conntrackd.ConntrackdManager, 'sudo') + def test_restart_skip(self, fake_sudo): + self.mgr._should_restart = False + self.mgr.restart() + self.assertFalse(fake_sudo.called)