move network controller to new world

This commit is contained in:
Michael Hudson-Doyle 2020-10-12 12:24:55 +13:00
parent e9071a00ca
commit 7f41803418
9 changed files with 376 additions and 12 deletions

View File

@ -3,6 +3,7 @@ subiquity/client/client.py
subiquity/client/controller.py subiquity/client/controller.py
subiquity/client/controllers/__init__.py subiquity/client/controllers/__init__.py
subiquity/client/controllers/keyboard.py subiquity/client/controllers/keyboard.py
subiquity/client/controllers/network.py
subiquity/client/controllers/progress.py subiquity/client/controllers/progress.py
subiquity/client/controllers/refresh.py subiquity/client/controllers/refresh.py
subiquity/client/controllers/welcome.py subiquity/client/controllers/welcome.py
@ -35,7 +36,6 @@ subiquity/controllers/filesystem.py
subiquity/controllers/identity.py subiquity/controllers/identity.py
subiquity/controllers/__init__.py subiquity/controllers/__init__.py
subiquity/controllers/mirror.py subiquity/controllers/mirror.py
subiquity/controllers/network.py
subiquity/controllers/proxy.py subiquity/controllers/proxy.py
subiquity/controllers/reboot.py subiquity/controllers/reboot.py
subiquity/controllers/snaplist.py subiquity/controllers/snaplist.py
@ -122,6 +122,7 @@ subiquity/server/controllers/__init__.py
subiquity/server/controllers/install.py subiquity/server/controllers/install.py
subiquity/server/controllers/keyboard.py subiquity/server/controllers/keyboard.py
subiquity/server/controllers/locale.py subiquity/server/controllers/locale.py
subiquity/server/controllers/network.py
subiquity/server/controllers/package.py subiquity/server/controllers/package.py
subiquity/server/controllers/refresh.py subiquity/server/controllers/refresh.py
subiquity/server/controllers/reporting.py subiquity/server/controllers/reporting.py

View File

@ -94,6 +94,7 @@ class SubiquityClient(TuiApplication):
"Refresh", "Refresh",
"Keyboard", "Keyboard",
"Zdev", "Zdev",
"Network",
"Progress", "Progress",
] ]

View File

@ -14,6 +14,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from .keyboard import KeyboardController from .keyboard import KeyboardController
from .network import NetworkController
from .progress import ProgressController from .progress import ProgressController
from .refresh import RefreshController from .refresh import RefreshController
from .welcome import WelcomeController from .welcome import WelcomeController
@ -21,6 +22,7 @@ from .zdev import ZdevController
__all__ = [ __all__ = [
'KeyboardController', 'KeyboardController',
'NetworkController',
'ProgressController', 'ProgressController',
'RefreshController', 'RefreshController',
'WelcomeController', 'WelcomeController',

View File

@ -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 <http://www.gnu.org/licenses/>.
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)

View File

@ -13,9 +13,16 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import enum
from typing import List, Optional 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 ( from subiquity.common.types import (
ApplicationState, ApplicationState,
ApplicationStatus, ApplicationStatus,
@ -80,6 +87,101 @@ class API:
class chzdev: class chzdev:
def POST(action: str, zdev: ZdevInfo) -> List[ZdevInfo]: ... 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 install:
class status: class status:
def GET(cur: Optional[InstallState] = None) -> InstallStatus: ... 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: ...

View File

@ -17,7 +17,6 @@ from ..controller import RepeatedController
from .filesystem import FilesystemController from .filesystem import FilesystemController
from .identity import IdentityController from .identity import IdentityController
from .mirror import MirrorController from .mirror import MirrorController
from .network import NetworkController
from .proxy import ProxyController from .proxy import ProxyController
from .reboot import RebootController from .reboot import RebootController
from .snaplist import SnapListController from .snaplist import SnapListController
@ -27,7 +26,6 @@ __all__ = [
'FilesystemController', 'FilesystemController',
'IdentityController', 'IdentityController',
'MirrorController', 'MirrorController',
'NetworkController',
'ProxyController', 'ProxyController',
'RebootController', 'RebootController',
'RepeatedController', 'RepeatedController',

View File

@ -18,6 +18,7 @@ from .debconf import DebconfController
from .install import InstallController from .install import InstallController
from .keyboard import KeyboardController from .keyboard import KeyboardController
from .locale import LocaleController from .locale import LocaleController
from .network import NetworkController
from .package import PackageController from .package import PackageController
from .refresh import RefreshController from .refresh import RefreshController
from .reporting import ReportingController from .reporting import ReportingController
@ -32,6 +33,7 @@ __all__ = [
'KeyboardController', 'KeyboardController',
'LateController', 'LateController',
'LocaleController', 'LocaleController',
'NetworkController',
'PackageController', 'PackageController',
'RefreshController', 'RefreshController',
'ReportingController', 'ReportingController',

View File

@ -15,16 +15,30 @@
import asyncio import asyncio
import logging import logging
from typing import List, Optional
import aiohttp
from subiquitycore.async_helpers import schedule_task from subiquitycore.async_helpers import schedule_task
from subiquitycore.context import with_context 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.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 = { MATCH = {
'type': 'object', 'type': 'object',
@ -65,7 +79,9 @@ NETPLAN_SCHEMA = {
} }
class NetworkController(NetworkController, SubiquityTuiController): class NetworkController(BaseNetworkController, SubiquityController):
endpoint = API.network
ai_data = None ai_data = None
autoinstall_key = "network" autoinstall_key = "network"
@ -85,6 +101,8 @@ class NetworkController(NetworkController, SubiquityTuiController):
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
app.note_file_for_apport("NetplanConfig", self.netplan_path) app.note_file_for_apport("NetplanConfig", self.netplan_path)
self.view_shown = False
self.clients = {}
def load_autoinstall_data(self, data): def load_autoinstall_data(self, data):
if data is not None: if data is not None:
@ -171,9 +189,119 @@ class NetworkController(NetworkController, SubiquityTuiController):
if not self.interactive(): if not self.interactive():
raise raise
def done(self):
self.configured()
super().done()
def make_autoinstall(self): def make_autoinstall(self):
return self.model.render_config()['network'] 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)

View File

@ -122,6 +122,7 @@ class SubiquityServer(Application):
"Refresh", "Refresh",
"Keyboard", "Keyboard",
"Zdev", "Zdev",
"Network",
"Install", "Install",
"Late", "Late",
] ]