from typing import Optional

from cloudinit import subp
from cloudinit.net.netops import NetOps


class Iproute2(NetOps):
    @staticmethod
    def link_up(
        interface: str, family: Optional[str] = None
    ) -> subp.SubpResult:
        family_args = []
        if family:
            family_args = ["-family", family]
        return subp.subp(
            ["ip", *family_args, "link", "set", "dev", interface, "up"]
        )

    @staticmethod
    def link_down(
        interface: str, family: Optional[str] = None
    ) -> subp.SubpResult:
        family_args = []
        if family:
            family_args = ["-family", family]
        return subp.subp(
            ["ip", *family_args, "link", "set", "dev", interface, "down"]
        )

    @staticmethod
    def link_rename(current_name: str, new_name: str):
        subp.subp(["ip", "link", "set", current_name, "name", new_name])

    @staticmethod
    def add_route(
        interface: str,
        route: str,
        *,
        gateway: Optional[str] = None,
        source_address: Optional[str] = None,
    ):
        gateway_args = []
        source_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        if source_address:
            source_args = ["src", source_address]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "add",
                route,
                *gateway_args,
                "dev",
                interface,
                *source_args,
            ]
        )

    @staticmethod
    def append_route(interface: str, address: str, gateway: str):
        gateway_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "append",
                address,
                *gateway_args,
                "dev",
                interface,
            ]
        )

    @staticmethod
    def del_route(
        interface: str,
        address: str,
        *,
        gateway: Optional[str] = None,
        source_address: Optional[str] = None,
    ):
        gateway_args = []
        source_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        if source_address:
            source_args = ["src", source_address]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "del",
                address,
                *gateway_args,
                "dev",
                interface,
                *source_args,
            ]
        )

    @staticmethod
    def get_default_route() -> str:
        return subp.subp(
            ["ip", "route", "show", "0.0.0.0/0"],
        ).stdout

    @staticmethod
    def add_addr(
        interface: str, address: str, broadcast: Optional[str] = None
    ):
        broadcast_args = []
        if broadcast:
            broadcast_args = ["broadcast", broadcast]
        subp.subp(
            [
                "ip",
                "-family",
                "inet",
                "addr",
                "add",
                address,
                *broadcast_args,
                "dev",
                interface,
            ],
            update_env={"LANG": "C"},
        )

    @staticmethod
    def del_addr(interface: str, address: str):
        subp.subp(
            ["ip", "-family", "inet", "addr", "del", address, "dev", interface]
        )

    @staticmethod
    def flush_addr(interface: str):
        subp.subp(["ip", "flush", "dev", interface])

cloudinit/net/netops/iproute2.py
from typing import Optional

from cloudinit import subp
from cloudinit.net.netops import NetOps


class Iproute2(NetOps):
    @staticmethod
    def link_up(
        interface: str, family: Optional[str] = None
    ) -> subp.SubpResult:
        family_args = []
        if family:
            family_args = ["-family", family]
        return subp.subp(
            ["ip", *family_args, "link", "set", "dev", interface, "up"]
        )

    @staticmethod
    def link_down(
        interface: str, family: Optional[str] = None
    ) -> subp.SubpResult:
        family_args = []
        if family:
            family_args = ["-family", family]
        return subp.subp(
            ["ip", *family_args, "link", "set", "dev", interface, "down"]
        )

    @staticmethod
    def link_rename(current_name: str, new_name: str):
        subp.subp(["ip", "link", "set", current_name, "name", new_name])

    @staticmethod
    def add_route(
        interface: str,
        route: str,
        *,
        gateway: Optional[str] = None,
        source_address: Optional[str] = None,
    ):
        gateway_args = []
        source_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        if source_address:
            source_args = ["src", source_address]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "replace",
                route,
                *gateway_args,
                "dev",
                interface,
                *source_args,
            ]
        )

    @staticmethod
    def append_route(interface: str, address: str, gateway: str):
        gateway_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "append",
                address,
                *gateway_args,
                "dev",
                interface,
            ]
        )

    @staticmethod
    def del_route(
        interface: str,
        address: str,
        *,
        gateway: Optional[str] = None,
        source_address: Optional[str] = None,
    ):
        gateway_args = []
        source_args = []
        if gateway and gateway != "0.0.0.0":
            gateway_args = ["via", gateway]
        if source_address:
            source_args = ["src", source_address]
        subp.subp(
            [
                "ip",
                "-4",
                "route",
                "del",
                address,
                *gateway_args,
                "dev",
                interface,
                *source_args,
            ]
        )

    @staticmethod
    def get_default_route() -> str:
        return subp.subp(
            ["ip", "route", "show", "0.0.0.0/0"],
        ).stdout

    @staticmethod
    def add_addr(
        interface: str, address: str, broadcast: Optional[str] = None
    ):
        broadcast_args = []
        if broadcast:
            broadcast_args = ["broadcast", broadcast]
        subp.subp(
            [
                "ip",
                "-family",
                "inet",
                "addr",
                "add",
                address,
                *broadcast_args,
                "dev",
                interface,
            ],
            update_env={"LANG": "C"},
        )

    @staticmethod
    def del_addr(interface: str, address: str):
        subp.subp(
            ["ip", "-family", "inet", "addr", "del", address, "dev", interface]
        )

    @staticmethod
    def flush_addr(interface: str):
        subp.subp(["ip", "flush", "dev", interface])

cloudinit/net/netops/iproute2.py
diff --git a/cloudinit/net/netops/iproute2.py b/cloudinit/net/netops/iproute2.py
index 70c311da918..46633b6d5c0 100644
--- a/cloudinit/net/netops/iproute2.py
+++ b/cloudinit/net/netops/iproute2.py
@@ -50,7 +50,7 @@ def add_route(
                 "ip",
                 "-4",
                 "route",
-                "add",
+                "replace",
                 route,
                 *gateway_args,
                 "dev",
diff --git a/tests/unittests/net/test_init.py b/tests/unittests/net/test_init.py
index d70162d0a56..6fa5dc828d6 100644
--- a/tests/unittests/net/test_init.py
+++ b/tests/unittests/net/test_init.py
@@ -1031,7 +1031,7 @@ def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
                     "ip",
                     "-4",
                     "route",
-                    "add",
+                    "replace",
                     "192.168.2.1",
                     "dev",
                     "eth0",
@@ -1044,7 +1044,7 @@ def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
                     "ip",
                     "-4",
                     "route",
-                    "add",
+                    "replace",
                     "default",
                     "via",
                     "192.168.2.1",

cloud-init generates a traceback if a default route already exists during ephemeral network setup
This bug was originally filed in Launchpad as [LP: #1860164](https://bugs.launchpad.net/cloud-init/+bug/1860164)
<details>
<summary>Launchpad details</summary>
<pre>
affected_projects = []
assignee = None
assignee_name = None
date_closed = None
date_created = 2020-01-17T18:37:30.886100+00:00
date_fix_committed = None
date_fix_released = None
id = 1860164
importance = medium
is_complete = False
lp_url = https://bugs.launchpad.net/cloud-init/+bug/1860164
milestone = None
owner = rjschwei
owner_name = Robert Schweikert
private = False
status = triaged
submitter = rjschwei
submitter_name = Robert Schweikert
tags = []
duplicates = []
</pre>
</details>

_Launchpad user **Robert Schweikert(rjschwei)** wrote on 2020-01-17T18:37:30.886100+00:00_

If a route already exists when the ephemeral network exists cloud-init will generate the following traceback:

2020-01-16 21:14:22,584 - util.py[DEBUG]: Getting data from <class 'cloudinit.sources.DataSourceOracle.DataSourceOracle'> failed
Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/cloudinit/sources/__init__.py", line 760, in find_source
    if s.update_metadata([EventType.BOOT_NEW_INSTANCE]):
  File "/usr/lib/python2.7/site-packages/cloudinit/sources/__init__.py", line 649, in update_metadata
    result = self.get_data()
  File "/usr/lib/python2.7/site-packages/cloudinit/sources/__init__.py", line 273, in get_data
    return_value = self._get_data()
  File "/usr/lib/python2.7/site-packages/cloudinit/sources/DataSourceOracle.py", line 195, in _get_data
    with dhcp.EphemeralDHCPv4(net.find_fallback_nic()):
  File "/usr/lib/python2.7/site-packages/cloudinit/net/dhcp.py", line 57, in __enter__
    return self.obtain_lease()
  File "/usr/lib/python2.7/site-packages/cloudinit/net/dhcp.py", line 109, in obtain_lease
    ephipv4.__enter__()
  File "/usr/lib/python2.7/site-packages/cloudinit/net/__init__.py", line 920, in __enter__
    self._bringup_static_routes()
  File "/usr/lib/python2.7/site-packages/cloudinit/net/__init__.py", line 974, in _bringup_static_routes
    ['dev', self.interface], capture=True)
  File "/usr/lib/python2.7/site-packages/cloudinit/util.py", line 2083, in subp
    cmd=args)
ProcessExecutionError: Unexpected error while running command.

This is a regression from 19.1 on SUSE where exiting routes were simply skipped.
