diff --git a/lower-constraints.txt b/lower-constraints.txt
index 1fa30674e1..83e19fe60f 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -12,11 +12,11 @@ contextlib2==0.4.0
 coverage==4.0
 cryptography==2.1
 debtcollector==1.2.0
-decorator==3.4.0
+decorator==4.4.1
 deprecation==1.0
 docker-pycreds==0.2.1
 docker==2.4.2
-dogpile.cache==0.6.2
+dogpile.cache==0.6.5
 eventlet==0.18.2
 extras==1.0.0
 fasteners==0.7.0
@@ -38,7 +38,7 @@ jmespath==0.9.0
 jsonpatch==1.16
 jsonpointer==1.13
 jsonschema==2.6.0
-keystoneauth1==3.16.0
+keystoneauth1==3.18.0
 kombu==4.0.0
 linecache2==1.0.0
 MarkupSafe==1.1.0
@@ -50,7 +50,7 @@ msgpack-python==0.4.0
 munch==2.1.0
 netaddr==0.7.18
 netifaces==0.10.4
-openstacksdk==0.36.0
+openstacksdk==0.38.0
 os-client-config==1.28.0
 os-service-types==1.7.0
 os-testr==1.0.0
diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py
index 464dbbec78..81b81f98b5 100644
--- a/openstackclient/network/v2/router.py
+++ b/openstackclient/network/v2/router.py
@@ -168,6 +168,93 @@ class AddSubnetToRouter(command.Command):
             subnet_id=subnet.id)
 
 
+class AddExtraRoutesToRouter(command.ShowOne):
+    _description = _("Add extra static routes to a router's routing table.")
+
+    def get_parser(self, prog_name):
+        parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name)
+        parser.add_argument(
+            'router',
+            metavar='<router>',
+            help=_("Router to which extra static routes "
+                   "will be added (name or ID).")
+        )
+        parser.add_argument(
+            '--route',
+            metavar='destination=<subnet>,gateway=<ip-address>',
+            action=parseractions.MultiKeyValueAction,
+            dest='routes',
+            default=[],
+            required_keys=['destination', 'gateway'],
+            help=_("Add extra static route to the router. "
+                   "destination: destination subnet (in CIDR notation), "
+                   "gateway: nexthop IP address. "
+                   "Repeat option to add multiple routes. "
+                   "Trying to add a route that's already present "
+                   "(exactly, including destination and nexthop) "
+                   "in the routing table is allowed and is considered "
+                   "a successful operation.")
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        if parsed_args.routes is not None:
+            for route in parsed_args.routes:
+                route['nexthop'] = route.pop('gateway')
+        client = self.app.client_manager.network
+        router_obj = client.add_extra_routes_to_router(
+            client.find_router(parsed_args.router, ignore_missing=False),
+            body={'router': {'routes': parsed_args.routes}})
+        display_columns, columns = _get_columns(router_obj)
+        data = utils.get_item_properties(
+            router_obj, columns, formatters=_formatters)
+        return (display_columns, data)
+
+
+class RemoveExtraRoutesFromRouter(command.ShowOne):
+    _description = _(
+        "Remove extra static routes from a router's routing table.")
+
+    def get_parser(self, prog_name):
+        parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name)
+        parser.add_argument(
+            'router',
+            metavar='<router>',
+            help=_("Router from which extra static routes "
+                   "will be removed (name or ID).")
+        )
+        parser.add_argument(
+            '--route',
+            metavar='destination=<subnet>,gateway=<ip-address>',
+            action=parseractions.MultiKeyValueAction,
+            dest='routes',
+            default=[],
+            required_keys=['destination', 'gateway'],
+            help=_("Remove extra static route from the router. "
+                   "destination: destination subnet (in CIDR notation), "
+                   "gateway: nexthop IP address. "
+                   "Repeat option to remove multiple routes. "
+                   "Trying to remove a route that's already missing "
+                   "(fully, including destination and nexthop) "
+                   "from the routing table is allowed and is considered "
+                   "a successful operation.")
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        if parsed_args.routes is not None:
+            for route in parsed_args.routes:
+                route['nexthop'] = route.pop('gateway')
+        client = self.app.client_manager.network
+        router_obj = client.remove_extra_routes_from_router(
+            client.find_router(parsed_args.router, ignore_missing=False),
+            body={'router': {'routes': parsed_args.routes}})
+        display_columns, columns = _get_columns(router_obj)
+        data = utils.get_item_properties(
+            router_obj, columns, formatters=_formatters)
+        return (display_columns, data)
+
+
 # TODO(yanxing'an): Use the SDK resource mapped attribute names once the
 # OSC minimum requirements include SDK 1.0.
 class CreateRouter(command.ShowOne):
@@ -540,17 +627,21 @@ class SetRouter(command.Command):
             dest='routes',
             default=None,
             required_keys=['destination', 'gateway'],
-            help=_("Routes associated with the router "
+            help=_("Add routes to the router "
                    "destination: destination subnet (in CIDR notation) "
                    "gateway: nexthop IP address "
-                   "(repeat option to set multiple routes)")
+                   "(repeat option to add multiple routes). "
+                   "This is deprecated in favor of 'router add/remove route' "
+                   "since it is prone to race conditions between concurrent "
+                   "clients when not used together with --no-route to "
+                   "overwrite the current value of 'routes'.")
         )
         parser.add_argument(
             '--no-route',
             action='store_true',
             help=_("Clear routes associated with the router. "
                    "Specify both --route and --no-route to overwrite "
-                   "current value of route.")
+                   "current value of routes.")
         )
         routes_ha = parser.add_mutually_exclusive_group()
         routes_ha.add_argument(
diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py
index 05aad7a013..0769dca6ff 100644
--- a/openstackclient/tests/functional/network/v2/test_router.py
+++ b/openstackclient/tests/functional/network/v2/test_router.py
@@ -261,3 +261,46 @@ class RouterTests(common.NetworkTagTests):
             new_name
         ))
         self.assertIsNone(cmd_output["external_gateway_info"])
+
+    def test_router_add_remove_route(self):
+        network_name = uuid.uuid4().hex
+        subnet_name = uuid.uuid4().hex
+        router_name = uuid.uuid4().hex
+
+        self.openstack('network create %s' % network_name)
+        self.addCleanup(self.openstack, 'network delete %s' % network_name)
+
+        self.openstack(
+            'subnet create %s '
+            '--network %s --subnet-range 10.0.0.0/24' % (
+                subnet_name, network_name))
+
+        self.openstack('router create %s' % router_name)
+        self.addCleanup(self.openstack, 'router delete %s' % router_name)
+
+        self.openstack('router add subnet %s %s' % (router_name, subnet_name))
+        self.addCleanup(self.openstack, 'router remove subnet %s %s' % (
+            router_name, subnet_name))
+
+        out1 = json.loads(self.openstack(
+            'router add route -f json %s '
+            '--route destination=10.0.10.0/24,gateway=10.0.0.10' %
+            router_name)),
+        self.assertEqual(1, len(out1[0]['routes']))
+
+        self.addCleanup(
+            self.openstack, 'router set %s --no-route' % router_name)
+
+        out2 = json.loads(self.openstack(
+            'router add route -f json %s '
+            '--route destination=10.0.10.0/24,gateway=10.0.0.10 '
+            '--route destination=10.0.11.0/24,gateway=10.0.0.11' %
+            router_name)),
+        self.assertEqual(2, len(out2[0]['routes']))
+
+        out3 = json.loads(self.openstack(
+            'router remove route -f json %s '
+            '--route destination=10.0.11.0/24,gateway=10.0.0.11 '
+            '--route destination=10.0.12.0/24,gateway=10.0.0.12' %
+            router_name)),
+        self.assertEqual(1, len(out3[0]['routes']))
diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py
index 38861b0ad5..09b4957cce 100644
--- a/openstackclient/tests/unit/network/v2/test_router.py
+++ b/openstackclient/tests/unit/network/v2/test_router.py
@@ -776,6 +776,146 @@ class TestRemoveSubnetFromRouter(TestRouter):
         self.assertIsNone(result)
 
 
+class TestAddExtraRoutesToRouter(TestRouter):
+
+    _router = network_fakes.FakeRouter.create_one_router()
+
+    def setUp(self):
+        super(TestAddExtraRoutesToRouter, self).setUp()
+        self.network.add_extra_routes_to_router = mock.Mock(
+            return_value=self._router)
+        self.cmd = router.AddExtraRoutesToRouter(self.app, self.namespace)
+        self.network.find_router = mock.Mock(return_value=self._router)
+
+    def test_add_no_extra_route(self):
+        arglist = [
+            self._router.id,
+        ]
+        verifylist = [
+            ('router', self._router.id),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.add_extra_routes_to_router.assert_called_with(
+            self._router, body={'router': {'routes': []}})
+        self.assertEqual(2, len(result))
+
+    def test_add_one_extra_route(self):
+        arglist = [
+            self._router.id,
+            '--route', 'destination=dst1,gateway=gw1',
+        ]
+        verifylist = [
+            ('router', self._router.id),
+            ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.add_extra_routes_to_router.assert_called_with(
+            self._router, body={'router': {'routes': [
+                {'destination': 'dst1', 'nexthop': 'gw1'},
+            ]}})
+        self.assertEqual(2, len(result))
+
+    def test_add_multiple_extra_routes(self):
+        arglist = [
+            self._router.id,
+            '--route', 'destination=dst1,gateway=gw1',
+            '--route', 'destination=dst2,gateway=gw2',
+        ]
+        verifylist = [
+            ('router', self._router.id),
+            ('routes', [
+                {'destination': 'dst1', 'gateway': 'gw1'},
+                {'destination': 'dst2', 'gateway': 'gw2'},
+            ]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.add_extra_routes_to_router.assert_called_with(
+            self._router, body={'router': {'routes': [
+                {'destination': 'dst1', 'nexthop': 'gw1'},
+                {'destination': 'dst2', 'nexthop': 'gw2'},
+            ]}})
+        self.assertEqual(2, len(result))
+
+
+class TestRemoveExtraRoutesFromRouter(TestRouter):
+
+    _router = network_fakes.FakeRouter.create_one_router()
+
+    def setUp(self):
+        super(TestRemoveExtraRoutesFromRouter, self).setUp()
+        self.network.remove_extra_routes_from_router = mock.Mock(
+            return_value=self._router)
+        self.cmd = router.RemoveExtraRoutesFromRouter(self.app, self.namespace)
+        self.network.find_router = mock.Mock(return_value=self._router)
+
+    def test_remove_no_extra_route(self):
+        arglist = [
+            self._router.id,
+        ]
+        verifylist = [
+            ('router', self._router.id),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.remove_extra_routes_from_router.assert_called_with(
+            self._router, body={'router': {'routes': []}})
+        self.assertEqual(2, len(result))
+
+    def test_remove_one_extra_route(self):
+        arglist = [
+            self._router.id,
+            '--route', 'destination=dst1,gateway=gw1',
+        ]
+        verifylist = [
+            ('router', self._router.id),
+            ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.remove_extra_routes_from_router.assert_called_with(
+            self._router, body={'router': {'routes': [
+                {'destination': 'dst1', 'nexthop': 'gw1'},
+            ]}})
+        self.assertEqual(2, len(result))
+
+    def test_remove_multiple_extra_routes(self):
+        arglist = [
+            self._router.id,
+            '--route', 'destination=dst1,gateway=gw1',
+            '--route', 'destination=dst2,gateway=gw2',
+        ]
+        verifylist = [
+            ('router', self._router.id),
+            ('routes', [
+                {'destination': 'dst1', 'gateway': 'gw1'},
+                {'destination': 'dst2', 'gateway': 'gw2'},
+            ]),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.network.remove_extra_routes_from_router.assert_called_with(
+            self._router, body={'router': {'routes': [
+                {'destination': 'dst1', 'nexthop': 'gw1'},
+                {'destination': 'dst2', 'nexthop': 'gw2'},
+            ]}})
+        self.assertEqual(2, len(result))
+
+
 class TestSetRouter(TestRouter):
 
     # The router to set.
diff --git a/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml
new file mode 100644
index 0000000000..33b5ba7a89
--- /dev/null
+++ b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    Add new commands ``router add route`` and ``router remove route`` to
+    support new Neutron extension: ``extraroute-atomic`` (see `Neutron RFE
+    <https://bugs.launchpad.net/neutron/+bug/1826396>`_).
+deprecations:
+  - |
+    The use of ``router set --route`` to add extra routes next to already
+    existing extra routes is deprecated in favor of ``router add route
+    --route``, because ``router set --route`` if used from multiple clients
+    concurrently may lead to lost updates.
diff --git a/requirements.txt b/requirements.txt
index b17b6a5593..f7a12dae85 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ six>=1.10.0 # MIT
 
 Babel!=2.4.0,>=2.3.4 # BSD
 cliff!=2.9.0,>=2.8.0 # Apache-2.0
-openstacksdk>=0.36.0 # Apache-2.0
+openstacksdk>=0.38.0 # Apache-2.0
 osc-lib>=2.0.0 # Apache-2.0
 oslo.i18n>=3.15.3 # Apache-2.0
 oslo.utils>=3.33.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 434da5b5be..4129be24b1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -481,11 +481,13 @@ openstack.network.v2 =
     port_unset = openstackclient.network.v2.port:UnsetPort
 
     router_add_port = openstackclient.network.v2.router:AddPortToRouter
+    router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter
     router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter
     router_create = openstackclient.network.v2.router:CreateRouter
     router_delete = openstackclient.network.v2.router:DeleteRouter
     router_list = openstackclient.network.v2.router:ListRouter
     router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter
+    router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter
     router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter
     router_set = openstackclient.network.v2.router:SetRouter
     router_show = openstackclient.network.v2.router:ShowRouter