diff --git a/po/POTFILES.in b/po/POTFILES.in
index f3bb5283..8928e048 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -3,6 +3,7 @@ subiquity/client/client.py
subiquity/client/controller.py
subiquity/client/controllers/__init__.py
subiquity/client/controllers/keyboard.py
+subiquity/client/controllers/network.py
subiquity/client/controllers/progress.py
subiquity/client/controllers/refresh.py
subiquity/client/controllers/welcome.py
@@ -35,7 +36,6 @@ subiquity/controllers/filesystem.py
subiquity/controllers/identity.py
subiquity/controllers/__init__.py
subiquity/controllers/mirror.py
-subiquity/controllers/network.py
subiquity/controllers/proxy.py
subiquity/controllers/reboot.py
subiquity/controllers/snaplist.py
@@ -122,6 +122,7 @@ subiquity/server/controllers/__init__.py
subiquity/server/controllers/install.py
subiquity/server/controllers/keyboard.py
subiquity/server/controllers/locale.py
+subiquity/server/controllers/network.py
subiquity/server/controllers/package.py
subiquity/server/controllers/refresh.py
subiquity/server/controllers/reporting.py
diff --git a/subiquity/client/client.py b/subiquity/client/client.py
index 92057241..f0077e2a 100644
--- a/subiquity/client/client.py
+++ b/subiquity/client/client.py
@@ -94,6 +94,7 @@ class SubiquityClient(TuiApplication):
"Refresh",
"Keyboard",
"Zdev",
+ "Network",
"Progress",
]
diff --git a/subiquity/client/controllers/__init__.py b/subiquity/client/controllers/__init__.py
index 49a224cf..b735f127 100644
--- a/subiquity/client/controllers/__init__.py
+++ b/subiquity/client/controllers/__init__.py
@@ -14,6 +14,7 @@
# along with this program. If not, see .
from .keyboard import KeyboardController
+from .network import NetworkController
from .progress import ProgressController
from .refresh import RefreshController
from .welcome import WelcomeController
@@ -21,6 +22,7 @@ from .zdev import ZdevController
__all__ = [
'KeyboardController',
+ 'NetworkController',
'ProgressController',
'RefreshController',
'WelcomeController',
diff --git a/subiquity/client/controllers/network.py b/subiquity/client/controllers/network.py
new file mode 100644
index 00000000..b1ea396c
--- /dev/null
+++ b/subiquity/client/controllers/network.py
@@ -0,0 +1,129 @@
+# Copyright 2020 Canonical, Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import logging
+import os
+import shutil
+import tempfile
+from typing import List, Optional
+
+from subiquitycore.controllers.network import NetworkAnswersMixin
+from subiquitycore.models.network import (
+ BondConfig,
+ NetDevInfo,
+ StaticConfig,
+ )
+from subiquitycore.ui.views.network import NetworkView
+
+from subiquity.client.controller import SubiquityTuiController
+from subiquity.common.api.server import make_server_at_path
+from subiquity.common.apidef import LinkAction, NetEventAPI
+
+log = logging.getLogger('subiquity.client.controllers.network')
+
+
+class NetworkController(SubiquityTuiController, NetworkAnswersMixin):
+
+ endpoint_name = 'network'
+
+ def __init__(self, app):
+ super().__init__(app)
+ self.view = None
+
+ async def update_link_POST(self, act: LinkAction,
+ info: NetDevInfo) -> None:
+ if self.view is None:
+ return
+ if act == LinkAction.NEW:
+ self.view.new_link(info)
+ if act == LinkAction.CHANGE:
+ self.view.update_link(info)
+ if act == LinkAction.DEL:
+ self.view.del_link(info)
+
+ async def route_watch_POST(self, routes: List[int]) -> None:
+ if self.view is not None:
+ self.view.update_default_routes(routes)
+
+ async def apply_starting_POST(self) -> None:
+ if self.view is not None:
+ self.view.show_apply_spinner()
+
+ async def apply_stopping_POST(self) -> None:
+ if self.view is not None:
+ self.view.hide_apply_spinner()
+
+ async def apply_error_POST(self, stage: str) -> None:
+ if self.view is not None:
+ self.view.show_network_error(stage)
+
+ async def subscribe(self):
+ self.tdir = tempfile.mkdtemp()
+ self.sock_path = os.path.join(self.tdir, 'socket')
+ self.site = await make_server_at_path(
+ self.sock_path, NetEventAPI, self)
+ await self.endpoint.subscription.PUT(self.sock_path)
+
+ async def unsubscribe(self):
+ await self.endpoint.subscription.DELETE(self.sock_path)
+ await self.site.stop()
+ shutil.rmtree(self.tdir)
+
+ async def make_ui(self):
+ netdev_infos = await self.endpoint.GET()
+ self.view = NetworkView(self, netdev_infos)
+ await self.subscribe()
+ return self.view
+
+ def end_ui(self):
+ if self.view is not None:
+ self.view = None
+ self.app.aio_loop.create_task(self.unsubscribe())
+
+ def cancel(self):
+ self.app.prev_screen()
+
+ def done(self):
+ self.app.next_screen(self.endpoint.POST())
+
+ def set_static_config(self, dev_name: str, ip_version: int,
+ static_config: StaticConfig) -> None:
+ self.app.aio_loop.create_task(
+ self.endpoint.set_static_config.POST(
+ dev_name, ip_version, static_config))
+
+ def enable_dhcp(self, dev_name, ip_version: int) -> None:
+ self.app.aio_loop.create_task(
+ self.endpoint.enable_dhcp.POST(dev_name, ip_version))
+
+ def disable_network(self, dev_name: str, ip_version: int) -> None:
+ self.app.aio_loop.create_task(
+ self.endpoint.disable.POST(dev_name, ip_version))
+
+ def add_vlan(self, dev_name: str, vlan_id: int):
+ self.app.aio_loop.create_task(
+ self.endpoint.vlan.PUT(dev_name, vlan_id))
+
+ def delete_link(self, dev_name: str):
+ self.app.aio_loop.create_task(self.endpoint.delete.POST(dev_name))
+
+ def add_or_update_bond(self, existing_name: Optional[str],
+ new_name: str, new_info: BondConfig) -> None:
+ self.app.aio_loop.create_task(
+ self.endpoint.add_or_edit_bond.POST(
+ existing_name, new_name, new_info))
+
+ async def get_info_for_netdev(self, dev_name: str) -> str:
+ return await self.endpoint.info.GET(dev_name)
diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py
index 6a4f6056..119a31c7 100644
--- a/subiquity/common/apidef.py
+++ b/subiquity/common/apidef.py
@@ -13,9 +13,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import enum
from typing import List, Optional
-from subiquity.common.api.defs import api, simple_endpoint
+from subiquitycore.models.network import (
+ BondConfig,
+ NetDevInfo,
+ StaticConfig,
+ )
+
+from subiquity.common.api.defs import api, Payload, simple_endpoint
from subiquity.common.types import (
ApplicationState,
ApplicationStatus,
@@ -80,6 +87,101 @@ class API:
class chzdev:
def POST(action: str, zdev: ZdevInfo) -> List[ZdevInfo]: ...
+ class network:
+ def GET() -> List[NetDevInfo]: ...
+ def POST() -> None: ...
+
+ class global_addresses:
+ def GET() -> List[str]:
+ """Return the global IP addresses the system currently has."""
+
+ class subscription:
+ """Subscribe to networking updates.
+
+ The socket must serve the NetEventAPI below.
+ """
+ def PUT(socket_path: str) -> None: ...
+ def DELETE(socket_path: str) -> None: ...
+
+ # These methods could definitely be more RESTish, like maybe a
+ # GET request to /network/interfaces/$name should return netplan
+ # config which could then be POSTed back the same path. But
+ # well, that's not implemented yet.
+ #
+ # (My idea is that the API definition would look something like
+ #
+ # class network:
+ # class interfaces:
+ # class dev_name:
+ # __subscript__ = True
+ # def GET() -> dict: ...
+ # def POST(config: Payload[dict]) -> None: ...
+ #
+ # The client would use subscripting to get a client for
+ # the nic, so something like
+ #
+ # dev_client = client.network[dev_name]
+ # config = await dev_client.GET()
+ # ...
+ # await dev_client.POST(config)
+ #
+ # The implementation would look like:
+ #
+ # class NetworkController:
+ #
+ # async def interfaces_devname_GET(dev_name: str) -> dict: ...
+ # async def interfaces_devname_POST(dev_name: str, config: dict) \
+ # -> None: ...
+ #
+ # So methods on nics get an extra dev_name: str parameter)
+
+ class set_static_config:
+ def POST(dev_name: str, ip_version: int,
+ static_config: Payload[StaticConfig]) -> None: ...
+
+ class enable_dhcp:
+ def POST(dev_name: str, ip_version: int) -> None: ...
+
+ class disable:
+ def POST(dev_name: str, ip_version: int) -> None: ...
+
+ class vlan:
+ def PUT(dev_name: str, vlan_id: int) -> None: ...
+
+ class add_or_edit_bond:
+ def POST(existing_name: Optional[str], new_name: str,
+ bond_config: Payload[BondConfig]) -> None: ...
+
+ class delete:
+ def POST(dev_name: str) -> None: ...
+
+ class info:
+ def GET(dev_name: str) -> str: ...
+
class install:
class status:
def GET(cur: Optional[InstallState] = None) -> InstallStatus: ...
+
+
+class LinkAction(enum.Enum):
+ NEW = enum.auto()
+ CHANGE = enum.auto()
+ DEL = enum.auto()
+
+
+@api
+class NetEventAPI:
+ class update_link:
+ def POST(act: LinkAction, info: Payload[NetDevInfo]) -> None: ...
+
+ class route_watch:
+ def POST(routes: List[int]) -> None: ...
+
+ class apply_starting:
+ def POST() -> None: ...
+
+ class apply_stopping:
+ def POST() -> None: ...
+
+ class apply_error:
+ def POST(stage: str) -> None: ...
diff --git a/subiquity/controllers/__init__.py b/subiquity/controllers/__init__.py
index 06805c66..98ce9e2c 100644
--- a/subiquity/controllers/__init__.py
+++ b/subiquity/controllers/__init__.py
@@ -17,7 +17,6 @@ from ..controller import RepeatedController
from .filesystem import FilesystemController
from .identity import IdentityController
from .mirror import MirrorController
-from .network import NetworkController
from .proxy import ProxyController
from .reboot import RebootController
from .snaplist import SnapListController
@@ -27,7 +26,6 @@ __all__ = [
'FilesystemController',
'IdentityController',
'MirrorController',
- 'NetworkController',
'ProxyController',
'RebootController',
'RepeatedController',
diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py
index a960c3c6..637e3ce0 100644
--- a/subiquity/server/controllers/__init__.py
+++ b/subiquity/server/controllers/__init__.py
@@ -18,6 +18,7 @@ from .debconf import DebconfController
from .install import InstallController
from .keyboard import KeyboardController
from .locale import LocaleController
+from .network import NetworkController
from .package import PackageController
from .refresh import RefreshController
from .reporting import ReportingController
@@ -32,6 +33,7 @@ __all__ = [
'KeyboardController',
'LateController',
'LocaleController',
+ 'NetworkController',
'PackageController',
'RefreshController',
'ReportingController',
diff --git a/subiquity/controllers/network.py b/subiquity/server/controllers/network.py
similarity index 54%
rename from subiquity/controllers/network.py
rename to subiquity/server/controllers/network.py
index cdc17eab..45c6a84e 100644
--- a/subiquity/controllers/network.py
+++ b/subiquity/server/controllers/network.py
@@ -15,16 +15,30 @@
import asyncio
import logging
+from typing import List, Optional
+
+import aiohttp
from subiquitycore.async_helpers import schedule_task
from subiquitycore.context import with_context
-from subiquitycore.controllers.network import NetworkController
+from subiquitycore.controllers.network import BaseNetworkController
+from subiquitycore.models.network import (
+ BondConfig,
+ NetDevInfo,
+ StaticConfig,
+ )
+from subiquity.common.api.client import make_client_for_conn
+from subiquity.common.apidef import (
+ API,
+ LinkAction,
+ NetEventAPI,
+ )
from subiquity.common.errorreport import ErrorReportKind
-from subiquity.controller import SubiquityTuiController
+from subiquity.server.controller import SubiquityController
-log = logging.getLogger("subiquity.controllers.network")
+log = logging.getLogger("subiquity.server.controllers.network")
MATCH = {
'type': 'object',
@@ -65,7 +79,9 @@ NETPLAN_SCHEMA = {
}
-class NetworkController(NetworkController, SubiquityTuiController):
+class NetworkController(BaseNetworkController, SubiquityController):
+
+ endpoint = API.network
ai_data = None
autoinstall_key = "network"
@@ -85,6 +101,8 @@ class NetworkController(NetworkController, SubiquityTuiController):
def __init__(self, app):
super().__init__(app)
app.note_file_for_apport("NetplanConfig", self.netplan_path)
+ self.view_shown = False
+ self.clients = {}
def load_autoinstall_data(self, data):
if data is not None:
@@ -171,9 +189,119 @@ class NetworkController(NetworkController, SubiquityTuiController):
if not self.interactive():
raise
- def done(self):
- self.configured()
- super().done()
-
def make_autoinstall(self):
return self.model.render_config()['network']
+
+ async def GET(self) -> List[NetDevInfo]:
+ if not self.view_shown:
+ self.apply_config(silent=True)
+ self.view_shown = True
+ return [
+ netdev.netdev_info() for netdev in self.model.get_all_netdevs()
+ ]
+
+ def configured(self):
+ self.model.has_network = bool(
+ self.network_event_receiver.default_routes)
+ super().configured()
+
+ async def POST(self) -> None:
+ self.configured()
+
+ async def global_addresses_GET(self) -> List[str]:
+ ips = []
+ for dev in self.model.get_all_netdevs():
+ ips.extend(map(str, dev.actual_global_ip_addresses))
+ return ips
+
+ async def subscription_PUT(self, socket_path: str) -> None:
+ log.debug('added subscription %s', socket_path)
+ conn = aiohttp.UnixConnector(socket_path)
+ client = make_client_for_conn(NetEventAPI, conn)
+ lock = asyncio.Lock()
+ self.clients[socket_path] = (client, conn, lock)
+ self.app.aio_loop.create_task(
+ self._call_client(
+ client, conn, lock, "route_watch",
+ self.network_event_receiver.default_routes))
+
+ async def subscription_DELETE(self, socket_path: str) -> None:
+ if socket_path not in self.clients:
+ return
+ log.debug('removed subscription %s', socket_path)
+ client, conn, lock = self.clients.pop(socket_path)
+ async with lock:
+ await conn.close()
+
+ async def _call_client(self, client, conn, lock, meth_name, *args):
+ async with lock:
+ log.debug("_call_client %s %s", meth_name, conn.path)
+ if conn.closed:
+ log.debug('closed')
+ return
+ await getattr(client, meth_name).POST(*args)
+
+ def _call_clients(self, meth_name, *args):
+ for client, conn, lock in self.clients.values():
+ log.debug('creating _call_client task %s %s', conn.path, meth_name)
+ self.app.aio_loop.create_task(
+ self._call_client(client, conn, lock, meth_name, *args))
+
+ def apply_starting(self):
+ super().apply_starting()
+ self._call_clients("apply_starting")
+
+ def apply_stopping(self):
+ super().apply_stopping()
+ self._call_clients("apply_stopping")
+
+ def apply_error(self, stage):
+ super().apply_error()
+ self._call_clients("apply_error", stage)
+
+ def update_default_routes(self, routes):
+ super().update_default_routes(routes)
+ self._call_clients("route_watch", routes)
+
+ def _send_update(self, act, dev):
+ with self.context.child(
+ "_send_update", "{} {}".format(act.name, dev.name)):
+ log.debug("dev_info {} {}".format(dev.name, dev.config))
+ dev_info = dev.netdev_info()
+ self._call_clients("update_link", act, dev_info)
+
+ def new_link(self, dev):
+ super().new_link(dev)
+ self._send_update(LinkAction.NEW, dev)
+
+ def update_link(self, dev):
+ super().update_link(dev)
+ self._send_update(LinkAction.CHANGE, dev)
+
+ def del_link(self, dev):
+ super().del_link(dev)
+ self._send_update(LinkAction.DEL, dev)
+
+ async def set_static_config_POST(self, dev_name: str, ip_version: int,
+ static_config: StaticConfig) -> None:
+ self.set_static_config(dev_name, ip_version, static_config)
+
+ async def enable_dhcp_POST(self, dev_name: str, ip_version: int) -> None:
+ self.enable_dhcp(dev_name, ip_version)
+
+ async def disable_POST(self, dev_name: str, ip_version: int) -> None:
+ self.disable_network(dev_name, ip_version)
+
+ async def vlan_PUT(self, dev_name: str, vlan_id: int) -> None:
+ self.add_vlan(dev_name, vlan_id)
+
+ async def add_or_edit_bond_POST(self, existing_name: Optional[str],
+ new_name: str,
+ bond_config: BondConfig) -> None:
+ self.add_or_update_bond(existing_name, new_name, bond_config)
+
+ async def delete_POST(self, dev_name: str) -> None:
+ self.delete_link(dev_name)
+
+ async def info_GET(self, dev_name: str) -> str:
+ return await self.get_info_for_netdev(dev_name)
diff --git a/subiquity/server/server.py b/subiquity/server/server.py
index c277a424..179ccc32 100644
--- a/subiquity/server/server.py
+++ b/subiquity/server/server.py
@@ -122,6 +122,7 @@ class SubiquityServer(Application):
"Refresh",
"Keyboard",
"Zdev",
+ "Network",
"Install",
"Late",
]