From 8846e55a79aced90accac7e2935e0a5f550be25a Mon Sep 17 00:00:00 2001
From: Swaminathan Vasudevan <SVasudevan@suse.com>
Date: Wed, 7 Feb 2018 09:09:28 -0800
Subject: [PATCH] Update osutil support for SUSE distro

Provide osutil support for SUSE distro to create networks
in a SUSE distro based Amphora Image.
This is a generic patch that can support different SUSE
flavors.
This patch will be the base patch and the diskimage builder
and devstack related changes to include the elements to build
the required services will depend on this patch

Change-Id: I29eeebb15a58cc54789edcb11c64ba47373cfb7b
---
 .../amphorae/backends/agent/api_server/osutils.py  | 149 +++++++++++++++++++++
 .../templates/suse_plug_port_ethX.conf.j2          |  45 +++++++
 .../templates/suse_plug_vip_ethX.conf.j2           |  59 ++++++++
 .../api_server/templates/suse_route_ethX.conf.j2   |  23 ++++
 octavia/common/constants.py                        |   1 +
 .../backends/agent/api_server/test_osutils.py      |  66 +++++++++
 6 files changed, 343 insertions(+)
 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/suse_plug_port_ethX.conf.j2
 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/suse_plug_vip_ethX.conf.j2
 create mode 100644 octavia/amphorae/backends/agent/api_server/templates/suse_route_ethX.conf.j2

diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py
index b95a531d..40cfb6d1 100644
--- a/octavia/amphorae/backends/agent/api_server/osutils.py
+++ b/octavia/amphorae/backends/agent/api_server/osutils.py
@@ -539,3 +539,152 @@ class CentOS(RH):
     @classmethod
     def is_os_name(cls, os_name):
         return os_name in ['centos']
+
+
+class SUSE(BaseOS):
+
+    ETH_X_PORT_CONF = 'suse_plug_port_ethX.conf.j2'
+    ETH_X_VIP_CONF = 'suse_plug_vip_ethX.conf.j2'
+    ROUTE_ETH_X_CONF = 'suse_route_ethX.conf.j2'
+
+    @classmethod
+    def is_os_name(cls, os_name):
+        return os_name.lower() in ['opensuse', 'suse', 'sles']
+
+    def cmd_get_version_of_installed_package(self, package_name):
+        name = self._map_package_name(package_name)
+        return "rpm -q --queryformat %{{VERSION}} {name}".format(name=name)
+
+    def get_network_interface_file(self, interface):
+        if CONF.amphora_agent.agent_server_network_file:
+            LOG.warning('CONF.amphora_agent.agent_server_network_file: %s '
+                        'will be ignored as it is not supported by the SUSE '
+                        'platforms. Please use '
+                        'CONF.amphora_agent.agent_server_network_dir option '
+                        'instead.',
+                        CONF.amphora_agent.agent_server_network_file)
+        ifcfg_prefix = 'ifcfg-' if 'ifroute' not in interface else ''
+        if CONF.amphora_agent.agent_server_network_dir:
+            network_dir = CONF.amphora_agent.agent_server_network_dir
+        else:
+            network_dir = consts.SUSE_AMP_NET_DIR_TEMPLATE.format(
+                netns=consts.AMPHORA_NAMESPACE)
+        return os.path.join(network_dir, ifcfg_prefix + interface)
+
+    def get_network_path(self):
+        return '/etc/sysconfig/network'
+
+    def get_netns_network_dir(self):
+        return 'sysconfig/network'
+
+    def get_static_routes_interface_file(self, interface):
+        return self.get_network_interface_file('ifroute-' + interface)
+
+    def create_netns_dir(
+            self, network_dir=None, netns_network_dir=None, ignore=None):
+        if not netns_network_dir:
+            netns_network_dir = self.get_netns_network_dir()
+            LOG.debug("Netns network directory: %s", netns_network_dir)
+        if not network_dir:
+            network_dir = self.get_network_path()
+            LOG.debug("Network directory: %s", network_dir)
+        if not ignore:
+            ignore = shutil.ignore_patterns('ifcfg-eth0*')
+        super(SUSE, self).create_netns_dir(
+            network_dir, netns_network_dir, ignore)
+
+    def write_interfaces_file(self):
+        # No interfaces file in SUSE distros.
+        return
+
+    def write_vip_interface_file(self, interface_file_path,
+                                 primary_interface, vip, ip, broadcast,
+                                 netmask, gateway, mtu, vrrp_ip, vrrp_version,
+                                 render_host_routes, template_vip=None):
+        if not template_vip:
+            template_vip = j2_env.get_template(self.ETH_X_VIP_CONF)
+        super(SUSE, self).write_vip_interface_file(
+            interface_file_path, primary_interface, vip, ip, broadcast,
+            netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes,
+            template_vip)
+
+        routes_interface_file_path = (
+            self.get_static_routes_interface_file(primary_interface))
+        template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF)
+
+        self.write_static_routes_interface_file(
+            routes_interface_file_path, primary_interface,
+            render_host_routes, template_routes, gateway, vip)
+        # NOTE: Copy{netns}/etc/sysconfig/network/ifroute-* file to
+        # /etc/sysconfig/network to overcome an issue with the
+        # ifup-route and namespace.
+        dst = self.get_network_path()
+        shutil.copy2(routes_interface_file_path, dst)
+
+    def write_static_routes_interface_file(self, interface_file_path,
+                                           interface, host_routes,
+                                           template_routes, gateway, vip):
+        # write static routes interface file
+
+        mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
+
+        # 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:
+            text = template_routes.render(
+                interface=interface,
+                host_routes=host_routes,
+                gateway=gateway,
+                vip=vip,
+            )
+            text_file.write(text)
+
+    def write_port_interface_file(self, netns_interface, fixed_ips, mtu,
+                                  interface_file_path=None,
+                                  template_port=None):
+        if not interface_file_path:
+            interface_file_path = self.get_network_interface_file(
+                netns_interface)
+        if not template_port:
+            template_port = j2_env.get_template(self.ETH_X_PORT_CONF)
+        super(SUSE, self).write_port_interface_file(
+            netns_interface, fixed_ips, mtu, interface_file_path,
+            template_port)
+
+    def _bring_ns_if_up(self, interface, what):
+        # Note, we are not using pyroute2 for this as it is not /etc/netns
+        # aware.
+        os.chdir(self.get_network_path())
+        cmd = ("ip netns exec {ns} ifup.ns {params}".format(
+            ns=consts.AMPHORA_NAMESPACE, params=interface))
+        try:
+            subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as e:
+            LOG.error('Failed to if up %s due to error: %s', interface, e)
+            raise exceptions.HTTPException(
+                response=webob.Response(json=dict(
+                    message='Error plugging {0}'.format(what),
+                    details=e.output), status=500))
+
+    def _bring_ns_if_down(self, interface):
+        # Note, we are not using pyroute2 for this as it is not /etc/netns
+        # aware.
+        os.chdir(self.get_network_path())
+        cmd = ("ip netns exec {ns} ifdown.ns {params}".format(
+            ns=consts.AMPHORA_NAMESPACE, params=interface))
+        try:
+            subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError:
+            pass
+
+    def bring_interfaces_up(self, ip, primary_interface, secondary_interface):
+        self._bring_ns_if_down(primary_interface)
+        self._bring_ns_if_up(primary_interface, 'VIP')
+
+    def has_ifup_all(self):
+        return True
diff --git a/octavia/amphorae/backends/agent/api_server/templates/suse_plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/suse_plug_port_ethX.conf.j2
new file mode 100644
index 00000000..e89617e4
--- /dev/null
+++ b/octavia/amphorae/backends/agent/api_server/templates/suse_plug_port_ethX.conf.j2
@@ -0,0 +1,45 @@
+{#
+# Copyright 2019 SUSE LLC
+#
+# 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.
+#}
+# Generated by Octavia agent
+DEVICE="{{ interface }}"
+ONBOOT="yes"
+STARTMODE="auto"
+TYPE="Ethernet"
+{%- if ipv6 %}
+IPV6INIT="yes"
+{%- if mtu %}
+IPV6_MTU="{{ mtu }}"
+{%- endif %}
+{%- if ip_address %}
+IPV6_AUTOCONF="no"
+IPV6ADDR="{{ ip_address }}"
+{%- else %}
+IPV6_AUTOCONF="yes"
+{%- endif %}
+{%- else %}
+IPV6INIT="no"
+{%- if mtu %}
+MTU="{{ mtu }}"
+{%- endif %}
+{%- if ip_address %}
+BOOTPROTO="static"
+IPADDR="{{ ip_address }}"
+NETMASK="{{ netmask }}"
+{%- else %}
+BOOTPROTO="dhcp"
+{%- endif %}
+{%- endif %}
+
diff --git a/octavia/amphorae/backends/agent/api_server/templates/suse_plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/suse_plug_vip_ethX.conf.j2
new file mode 100644
index 00000000..e637b18f
--- /dev/null
+++ b/octavia/amphorae/backends/agent/api_server/templates/suse_plug_vip_ethX.conf.j2
@@ -0,0 +1,59 @@
+{#
+# Copyright 2019 SUSE LLC
+#
+# 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.
+#}
+# Generated by Octavia agent
+DEVICE="{{ interface }}"
+ONBOOT="yes"
+TYPE="Ethernet"
+USERCTL="yes"
+STARTMODE="auto"
+LINK_REQUIRED="no"
+{%- if vrrp_ip %}
+{%- if vrrp_ipv6 %}
+IPV6INIT="yes"
+IPV6_DEFROUTE="yes"
+IPV6_AUTOCONF="no"
+IPV6ADDR="{{ vrrp_ip }}/{{ prefix }}"
+{%- if gateway %}
+IPV6_DEFAULTGW="{{ gateway }}"
+{%- endif %}
+{%- if mtu %}
+IPV6_MTU="{{ mtu }}"
+{%- endif %}
+{%- else %} {# not vrrp_ipv6 #}
+BOOTPROTO="static"
+IPADDR="{{ vrrp_ip }}"
+NETMASK="{{ netmask }}"
+{%- if gateway %}
+GATEWAY="{{ gateway }}"
+{%- endif %}
+MTU="{{ mtu }}"
+LABEL_0="0"
+IPADDR_0="{{ vip }}"
+NETMASK_0="{{ netmask }}"
+{%- endif %} {# end if vrrp_ipv6 #}
+{%- else %} {# not vrrp_ip #}
+{%- if vip_ipv6 %}
+IPV6INIT="yes"
+IPV6_DEFROUTE="yes"
+IPV6_AUTOCONF="yes"
+{%- else %}
+BOOTPROTO="dhcp"
+{%- endif %} {# end if vip_ipv6 #}
+{%- endif %} {# end if vrrp_ip #}
+
+{%- if vip_ipv6 %}
+IPV6ADDR_SECONDARIES="{{ vip }}/{{ prefix }}"
+{%- endif %}
diff --git a/octavia/amphorae/backends/agent/api_server/templates/suse_route_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/suse_route_ethX.conf.j2
new file mode 100644
index 00000000..b672ebcd
--- /dev/null
+++ b/octavia/amphorae/backends/agent/api_server/templates/suse_route_ethX.conf.j2
@@ -0,0 +1,23 @@
+{#
+# Copyright 2019 SUSE LLC
+#
+# 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.
+#}
+# Generated by Octavia agent
+{%- for hr in host_routes %}
+{{ hr.network }} {{ hr.gw }} {{ hr.netmask}} {{ interface }}
+{%- endfor %}
+# Add a source routing table to allow members to access the VIP
+{%- if gateway %}
+default  {{ gateway }} - -
+{%- endif %}
diff --git a/octavia/common/constants.py b/octavia/common/constants.py
index 627d01e4..fb7b4bf7 100644
--- a/octavia/common/constants.py
+++ b/octavia/common/constants.py
@@ -496,6 +496,7 @@ TESTING = 'testing'
 # Amphora distro-specific data
 UBUNTU_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/network/interfaces.d/'
 RH_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network-scripts/'
+SUSE_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network/'
 UBUNTU = 'ubuntu'
 CENTOS = 'centos'
 
diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py
index 0366ce0e..6198bc42 100644
--- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py
+++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py
@@ -44,6 +44,10 @@ class TestOSUtils(base.TestCase):
                         return_value='centos'):
             self.centos_os_util = osutils.BaseOS.get_os_util()
 
+        with mock.patch('distro.id',
+                        return_value='sles'):
+            self.suse_os_util = osutils.BaseOS.get_os_util()
+
     def test_get_os_util(self):
         with mock.patch('distro.id',
                         return_value='ubuntu'):
@@ -61,6 +65,14 @@ class TestOSUtils(base.TestCase):
                         return_value='centos'):
             returned_cls = osutils.BaseOS.get_os_util()
             self.assertIsInstance(returned_cls, osutils.CentOS)
+        with mock.patch('distro.id',
+                        return_value='opensuse'):
+            returned_cls = osutils.BaseOS.get_os_util()
+            self.assertIsInstance(returned_cls, osutils.SUSE)
+        with mock.patch('distro.id',
+                        return_value='sles'):
+            returned_cls = osutils.BaseOS.get_os_util()
+            self.assertIsInstance(returned_cls, osutils.SUSE)
         with mock.patch('distro.id',
                         return_value='FakeOS'):
             self.assertRaises(
@@ -98,6 +110,22 @@ class TestOSUtils(base.TestCase):
                 netns=consts.AMPHORA_NAMESPACE),
             ubuntu_interface_name)
 
+        suse_ifroute_interface = 'ifroute-foo'
+        suse_ifcfg_ifroute_interface = 'ifcfg-ifroute-foo'
+        suse_fake_nic_path = os.path.join(fake_agent_server_network_dir,
+                                          consts.NETNS_PRIMARY_INTERFACE)
+        suse_fake_ifroute_nic_path = os.path.join(
+            fake_agent_server_network_dir,
+            suse_ifcfg_ifroute_interface)
+        suse_nic_path = os.path.join(
+            consts.SUSE_AMP_NET_DIR_TEMPLATE.format(
+                netns=consts.AMPHORA_NAMESPACE),
+            consts.NETNS_PRIMARY_INTERFACE)
+        suse_ifroute_nic_path = os.path.join(
+            consts.SUSE_AMP_NET_DIR_TEMPLATE.format(
+                netns=consts.AMPHORA_NAMESPACE),
+            suse_ifcfg_ifroute_interface)
+
         # Check that agent_server_network_file is returned, when provided
         conf.config(group="amphora_agent",
                     agent_server_network_file=fake_agent_server_network_file)
@@ -117,6 +145,19 @@ class TestOSUtils(base.TestCase):
             get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
         self.assertEqual(fake_agent_server_network_file, ubuntu_interface_file)
 
+        # NOTE(gyee): for SUSE, CONF.amphora_agent.agent_server_network_file
+        # will be ignored as there's no single network file in SUSE. Instead,
+        # we should be using agent_server_network_dir.
+        suse_interface_file = (
+            self.suse_os_util.
+            get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
+        self.assertEqual(suse_nic_path, suse_interface_file)
+
+        suse_ifroute_interface_file = (
+            self.suse_os_util.
+            get_network_interface_file(suse_ifroute_interface))
+        self.assertEqual(suse_ifroute_nic_path, suse_ifroute_interface_file)
+
         # Check that agent_server_network_dir is used, when provided
         conf.config(group="amphora_agent", agent_server_network_file=None)
         conf.config(group="amphora_agent",
@@ -137,6 +178,19 @@ class TestOSUtils(base.TestCase):
             get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
         self.assertEqual(ubuntu_fake_nic_path, ubuntu_interface_file)
 
+        # FIXME(gyee): we may want to refactor these tests into
+        # perhaps one class per platform?
+        suse_interface_file = (
+            self.suse_os_util.
+            get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
+        self.assertEqual(suse_fake_nic_path, suse_interface_file)
+
+        suse_ifroute_interface_file = (
+            self.suse_os_util.
+            get_network_interface_file(suse_ifroute_interface))
+        self.assertEqual(suse_fake_ifroute_nic_path,
+                         suse_ifroute_interface_file)
+
         # Check When neither agent_server_network_dir or
         # agent_server_network_file where provided.
         conf.config(group="amphora_agent", agent_server_network_file=None)
@@ -157,12 +211,19 @@ class TestOSUtils(base.TestCase):
             get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
         self.assertEqual(ubuntu_real_nic_path, ubuntu_interface_file)
 
+        suse_interface_file = (
+            self.suse_os_util.
+            get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE))
+        self.assertEqual(suse_nic_path, suse_interface_file)
+
     def test_cmd_get_version_of_installed_package(self):
         package_name = 'foo'
         ubuntu_cmd = "dpkg-query -W -f=${{Version}} {name}".format(
             name=package_name)
         rh_cmd = "rpm -q --queryformat %{{VERSION}} {name}".format(
             name=package_name)
+        suse_cmd = "rpm -q --queryformat %{{VERSION}} {name}".format(
+            name=package_name)
 
         returned_ubuntu_cmd = (
             self.ubuntu_os_util.cmd_get_version_of_installed_package(
@@ -173,6 +234,11 @@ class TestOSUtils(base.TestCase):
                            cmd_get_version_of_installed_package(package_name))
         self.assertEqual(rh_cmd, returned_rh_cmd)
 
+        returned_suse_cmd = (
+            self.suse_os_util.
+            cmd_get_version_of_installed_package(package_name))
+        self.assertEqual(suse_cmd, returned_suse_cmd)
+
     def test_cmd_get_version_of_installed_package_mapped(self):
         package_name = 'haproxy'
         centos_cmd = "rpm -q --queryformat %{VERSION} haproxy18"
-- 
2.16.4

