Merge pull request #873 from mwhudson/combine-install-and-application-state

smoosh InstallState into ApplicationState
This commit is contained in:
Michael Hudson-Doyle 2020-12-18 15:52:59 +13:00 committed by GitHub
commit 07a85949b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 178 deletions

View File

@ -168,8 +168,8 @@ The API takes a "long poll" approach to status updates. For example,
`wait=False` and if the result indicates that the check for updates is still in `wait=False` and if the result indicates that the check for updates is still in
progress it shows a screen indicating this and calls it again with `wait=True`, progress it shows a screen indicating this and calls it again with `wait=True`,
which will not return until the check has completed (or failed). In a similar which will not return until the check has completed (or failed). In a similar
vein, `install.status.GET()` takes an argument indicating what the client vein, `meta.status.GET()` takes an argument indicating what the client
thinks the install state currently is and will block until that changes. thinks the application state currently is and will block until that changes.
### Examples and common patterns ### Examples and common patterns
@ -332,24 +332,31 @@ controllers have methods that are called to load and apply the autoinstall data
for each controller. The only real difference to the client is that it behaves for each controller. The only real difference to the client is that it behaves
totally differently if the install is to be totally automated: in this case it totally differently if the install is to be totally automated: in this case it
does not start the urwid-based UI at all and mostly just "listens" to install does not start the urwid-based UI at all and mostly just "listens" to install
progress via journald and the `install.status.GET()` API call. progress via journald and the `meta.status.GET()` API call.
### Starting and confirming the install ### The server state machine
The installation code proceeds in stages: The server code proceeds in stages:
1. First it waits for all the model objects that feed into the curtin config 1. It starts up, checks for an autoinstall config and runs any early
to be configured. commands.
2. It waits for confirmation. 2. Then it waits for all the model objects that feed into the curtin
3. It runs "curtin install" and waits for that to finish. config to be configured.
4. It waits for the model objects that feed into the cloud-init config to be 3. It waits for confirmation.
4. It runs "curtin install" and waits for that to finish.
5. It waits for the model objects that feed into the cloud-init config to be
configured. configured.
5. If there appears to be a working network connection, it downloads and 6. It creates the cloud-init config for the first boot of the
installed system.
7. If there appears to be a working network connection, it downloads and
installs security updates. installs security updates.
6. It waits for the user to click "reboot". 8. It runs any late commands.
9. It waits for the user to click "reboot".
Each of these states gets a different value of the `InstallState` enum, so the Each of these states gets a different value of the `ApplicationState`
client gets notified via long-polling `install.status.GET()` of progress. enum, so the client gets notified via long-polling `meta.status.GET()`
of progress. In addition, `ApplicationState.ERROR` indicates something
has gone wrong.
### Refreshing the snap ### Refreshing the snap

View File

@ -46,7 +46,6 @@ from subiquity.common.types import (
ApplicationState, ApplicationState,
ErrorReportKind, ErrorReportKind,
ErrorReportRef, ErrorReportRef,
InstallState,
) )
from subiquity.journald import journald_listen from subiquity.journald import journald_listen
from subiquity.ui.frame import SubiquityUI from subiquity.ui.frame import SubiquityUI
@ -206,24 +205,24 @@ class SubiquityClient(TuiApplication):
answer = await run_in_thread(input) answer = await run_in_thread(input)
await self.confirm_install() await self.confirm_install()
async def noninteractive_watch_install_state(self): async def noninteractive_watch_app_state(self, initial_status):
install_state = None app_status = initial_status
confirm_task = None confirm_task = None
while True: while True:
try: app_state = app_status.state
install_status = await self.client.install.status.GET( if app_state == ApplicationState.NEEDS_CONFIRMATION:
cur=install_state) if confirm_task is None:
install_state = install_status.state
except aiohttp.ClientError:
await asyncio.sleep(1)
continue
if install_state == InstallState.NEEDS_CONFIRMATION:
if confirm_task is not None:
confirm_task = self.aio_loop.create_task( confirm_task = self.aio_loop.create_task(
self.noninteractive_confirmation()) self.noninteractive_confirmation())
elif confirm_task is not None: elif confirm_task is not None:
confirm_task.cancel() confirm_task.cancel()
confirm_task = None confirm_task = None
try:
app_status = await self.client.meta.status.GET(
cur=app_state)
except aiohttp.ClientError:
await asyncio.sleep(1)
continue
def subiquity_event_noninteractive(self, event): def subiquity_event_noninteractive(self, event):
if event['SUBIQUITY_EVENT_TYPE'] == 'start': if event['SUBIQUITY_EVENT_TYPE'] == 'start':
@ -244,28 +243,20 @@ class SubiquityClient(TuiApplication):
print(".", end='', flush=True) print(".", end='', flush=True)
else: else:
break break
print() print("\nconnected")
journald_listen( journald_listen(
self.aio_loop, self.aio_loop,
[status.echo_syslog_id], [status.echo_syslog_id],
lambda e: print(e['MESSAGE'])) lambda e: print(e['MESSAGE']))
if status.state == ApplicationState.STARTING: if status.state == ApplicationState.STARTING_UP:
print("server is starting...", end='', flush=True)
while status.state == ApplicationState.STARTING:
await asyncio.sleep(1)
print(".", end='', flush=True)
status = await self.client.meta.status.GET()
print()
if status.state == ApplicationState.EARLY_COMMANDS:
print("running early commands...")
status = await self.client.meta.status.GET(cur=status.state) status = await self.client.meta.status.GET(cur=status.state)
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
return status return status
async def start(self): async def start(self):
status = await self.connect() status = await self.connect()
if status.state == ApplicationState.INTERACTIVE: self.interactive = status.interactive
self.interactive = True if self.interactive:
await super().start() await super().start()
journald_listen( journald_listen(
self.aio_loop, self.aio_loop,
@ -283,7 +274,6 @@ class SubiquityClient(TuiApplication):
self.show_error_report(report.ref()) self.show_error_report(report.ref())
break break
else: else:
self.interactive = False
if self.opts.run_on_serial: if self.opts.run_on_serial:
# Thanks to the fact that we are launched with agetty's # Thanks to the fact that we are launched with agetty's
# --skip-login option, on serial lines we can end up starting # --skip-login option, on serial lines we can end up starting
@ -299,7 +289,7 @@ class SubiquityClient(TuiApplication):
self.subiquity_event_noninteractive, self.subiquity_event_noninteractive,
seek=True) seek=True)
self.aio_loop.create_task( self.aio_loop.create_task(
self.noninteractive_watch_install_state()) self.noninteractive_watch_app_state(status))
def _exception_handler(self, loop, context): def _exception_handler(self, loop, context):
exc = context.get('exception') exc = context.get('exception')

View File

@ -21,7 +21,7 @@ import aiohttp
from subiquitycore.context import with_context from subiquitycore.context import with_context
from subiquity.client.controller import SubiquityTuiController from subiquity.client.controller import SubiquityTuiController
from subiquity.common.types import InstallState from subiquity.common.types import ApplicationState
from subiquity.ui.views.installprogress import ( from subiquity.ui.views.installprogress import (
InstallRunning, InstallRunning,
ProgressView, ProgressView,
@ -33,12 +33,10 @@ log = logging.getLogger("subiquity.client.controllers.progress")
class ProgressController(SubiquityTuiController): class ProgressController(SubiquityTuiController):
endpoint_name = 'install'
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
self.progress_view = ProgressView(self) self.progress_view = ProgressView(self)
self.install_state = None self.app_state = None
self.crash_report_ref = None self.crash_report_ref = None
self.answers = app.answers.get("InstallProgress", {}) self.answers = app.answers.get("InstallProgress", {})
@ -77,43 +75,43 @@ class ProgressController(SubiquityTuiController):
install_running = None install_running = None
while True: while True:
try: try:
install_status = await self.endpoint.status.GET( app_status = await self.app.client.meta.status.GET(
cur=self.install_state) cur=self.app_state)
except aiohttp.ClientError: except aiohttp.ClientError:
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
self.install_state = install_status.state self.app_state = app_status.state
self.progress_view.update_for_state(self.install_state) self.progress_view.update_for_state(self.app_state)
if self.ui.body is self.progress_view: if self.ui.body is self.progress_view:
self.ui.set_header(self.progress_view.title) self.ui.set_header(self.progress_view.title)
if install_status.error is not None: if app_status.error is not None:
if self.crash_report_ref is None: if self.crash_report_ref is None:
self.crash_report_ref = install_status.error self.crash_report_ref = app_status.error
self.ui.set_body(self.progress_view) self.ui.set_body(self.progress_view)
self.app.show_error_report(self.crash_report_ref) self.app.show_error_report(self.crash_report_ref)
if self.install_state == InstallState.NEEDS_CONFIRMATION: if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
if self.showing: if self.showing:
self.app.show_confirm_install() self.app.show_confirm_install()
if self.install_state == InstallState.RUNNING: if self.app_state == ApplicationState.RUNNING:
if install_status.confirming_tty != self.app.our_tty: if app_status.confirming_tty != self.app.our_tty:
install_running = InstallRunning( install_running = InstallRunning(
self.app, install_status.confirming_tty) self.app, app_status.confirming_tty)
self.app.add_global_overlay(install_running) self.app.add_global_overlay(install_running)
else: else:
if install_running is not None: if install_running is not None:
self.app.remove_global_overlay(install_running) self.app.remove_global_overlay(install_running)
install_running = None install_running = None
if self.install_state == InstallState.DONE: if self.app_state == ApplicationState.DONE:
if self.answers.get('reboot', False): if self.answers.get('reboot', False):
self.click_reboot() self.click_reboot()
def make_ui(self): def make_ui(self):
if self.install_state == InstallState.NEEDS_CONFIRMATION: if self.app_state == ApplicationState.NEEDS_CONFIRMATION:
self.app.show_confirm_install() self.app.show_confirm_install()
return self.progress_view return self.progress_view

View File

@ -29,8 +29,6 @@ from subiquity.common.types import (
ErrorReportRef, ErrorReportRef,
KeyboardSetting, KeyboardSetting,
IdentityData, IdentityData,
InstallState,
InstallStatus,
RefreshStatus, RefreshStatus,
SnapInfo, SnapInfo,
SnapListResponse, SnapListResponse,
@ -79,6 +77,7 @@ class API:
class crash: class crash:
def GET() -> None: def GET() -> None:
"""Requests to this method will fail with a HTTP 500.""" """Requests to this method will fail with a HTTP 500."""
class refresh: class refresh:
def GET(wait: bool = False) -> RefreshStatus: def GET(wait: bool = False) -> RefreshStatus:
"""Get information about the snap refresh status. """Get information about the snap refresh status.
@ -182,10 +181,6 @@ class API:
class snap_info: class snap_info:
def GET(snap_name: str) -> SnapInfo: ... def GET(snap_name: str) -> SnapInfo: ...
class install:
class status:
def GET(cur: Optional[InstallState] = None) -> InstallStatus: ...
class reboot: class reboot:
def POST(): ... def POST(): ...

View File

@ -25,22 +25,6 @@ from typing import List, Optional
import attr import attr
class ApplicationState(enum.Enum):
STARTING = enum.auto()
EARLY_COMMANDS = enum.auto()
INTERACTIVE = enum.auto()
NON_INTERACTIVE = enum.auto()
@attr.s(auto_attribs=True)
class ApplicationStatus:
state: ApplicationState
cloud_init_ok: bool
echo_syslog_id: str
log_syslog_id: str
event_syslog_id: str
class ErrorReportState(enum.Enum): class ErrorReportState(enum.Enum):
INCOMPLETE = enum.auto() INCOMPLETE = enum.auto()
LOADING = enum.auto() LOADING = enum.auto()
@ -68,6 +52,31 @@ class ErrorReportRef:
oops_id: Optional[str] oops_id: Optional[str]
class ApplicationState(enum.Enum):
STARTING_UP = enum.auto()
WAITING = enum.auto()
NEEDS_CONFIRMATION = enum.auto()
RUNNING = enum.auto()
POST_WAIT = enum.auto()
POST_RUNNING = enum.auto()
UU_RUNNING = enum.auto()
UU_CANCELLING = enum.auto()
DONE = enum.auto()
ERROR = enum.auto()
@attr.s(auto_attribs=True)
class ApplicationStatus:
state: ApplicationState
confirming_tty: str
error: Optional[ErrorReportRef]
cloud_init_ok: bool
interactive: Optional[bool]
echo_syslog_id: str
log_syslog_id: str
event_syslog_id: str
class RefreshCheckState(enum.Enum): class RefreshCheckState(enum.Enum):
UNKNOWN = enum.auto() UNKNOWN = enum.auto()
AVAILABLE = enum.auto() AVAILABLE = enum.auto()
@ -195,22 +204,3 @@ class SnapListResponse:
status: SnapCheckState status: SnapCheckState
snaps: List[SnapInfo] = attr.Factory(list) snaps: List[SnapInfo] = attr.Factory(list)
selections: List[SnapSelection] = attr.Factory(list) selections: List[SnapSelection] = attr.Factory(list)
class InstallState(enum.Enum):
NOT_STARTED = enum.auto()
NEEDS_CONFIRMATION = enum.auto()
RUNNING = enum.auto()
POST_WAIT = enum.auto()
POST_RUNNING = enum.auto()
UU_RUNNING = enum.auto()
UU_CANCELLING = enum.auto()
DONE = enum.auto()
ERROR = enum.auto()
@attr.s(auto_attribs=True)
class InstallStatus:
state: InstallState
confirming_tty: str = ''
error: Optional[ErrorReportRef] = None

View File

@ -21,7 +21,7 @@ from systemd import journal
from subiquitycore.context import with_context from subiquitycore.context import with_context
from subiquitycore.utils import arun_command from subiquitycore.utils import arun_command
from subiquity.common.types import InstallState from subiquity.common.types import ApplicationState
from subiquity.server.controller import NonInteractiveController from subiquity.server.controller import NonInteractiveController
@ -102,7 +102,7 @@ class LateController(CmdListController):
async def _run(self): async def _run(self):
Install = self.app.controllers.Install Install = self.app.controllers.Install
await Install.install_task await Install.install_task
if Install.install_state == InstallState.DONE: if self.app.state == ApplicationState.DONE:
await self.run() await self.run()
@ -113,7 +113,7 @@ class ErrorController(CmdListController):
@with_context() @with_context()
async def run(self, context): async def run(self, context):
if self.app.interactive(): if self.app.interactive:
self.syslog_id = self.app.log_syslog_id self.syslog_id = self.app.log_syslog_id
else: else:
self.syslog_id = self.app.echo_syslog_id self.syslog_id = self.app.echo_syslog_id

View File

@ -21,7 +21,6 @@ import re
import shutil import shutil
import sys import sys
import tempfile import tempfile
from typing import Optional
from curtin.commands.install import ( from curtin.commands.install import (
ERROR_TARFILE, ERROR_TARFILE,
@ -40,14 +39,12 @@ from subiquitycore.utils import (
astart_command, astart_command,
) )
from subiquity.common.apidef import API
from subiquity.common.errorreport import ErrorReportKind from subiquity.common.errorreport import ErrorReportKind
from subiquity.server.controller import ( from subiquity.server.controller import (
SubiquityController, SubiquityController,
) )
from subiquity.common.types import ( from subiquity.common.types import (
InstallState, ApplicationState,
InstallStatus,
) )
from subiquity.journald import journald_listen from subiquity.journald import journald_listen
@ -75,14 +72,9 @@ class TracebackExtractor:
class InstallController(SubiquityController): class InstallController(SubiquityController):
endpoint = API.install
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
self.model = app.base_model self.model = app.base_model
self._install_state = InstallState.NOT_STARTED
self._install_state_event = asyncio.Event()
self.error_ref = None
self.unattended_upgrades_proc = None self.unattended_upgrades_proc = None
self.unattended_upgrades_ctx = None self.unattended_upgrades_ctx = None
@ -90,48 +82,27 @@ class InstallController(SubiquityController):
self.tb_extractor = TracebackExtractor() self.tb_extractor = TracebackExtractor()
self.curtin_event_contexts = {} self.curtin_event_contexts = {}
def interactive(self):
return True
async def status_GET(
self, cur: Optional[InstallState] = None) -> InstallStatus:
if cur == self.install_state:
await self._install_state_event.wait()
return InstallStatus(
self.install_state,
self.app.confirming_tty,
self.error_ref)
def stop_uu(self): def stop_uu(self):
if self.install_state == InstallState.UU_RUNNING: if self.app.state == ApplicationState.UU_RUNNING:
self.update_state(InstallState.UU_CANCELLING) self.app.update_state(ApplicationState.UU_CANCELLING)
self.app.aio_loop.create_task(self.stop_unattended_upgrades()) self.app.aio_loop.create_task(self.stop_unattended_upgrades())
def start(self): def start(self):
self.install_task = self.app.aio_loop.create_task(self.install()) self.install_task = self.app.aio_loop.create_task(self.install())
@property
def install_state(self):
return self._install_state
def update_state(self, state):
self._install_state_event.set()
self._install_state_event.clear()
self._install_state = state
def tpath(self, *path): def tpath(self, *path):
return os.path.join(self.model.target, *path) return os.path.join(self.model.target, *path)
def curtin_error(self): def curtin_error(self):
self.update_state(InstallState.ERROR)
kw = {} kw = {}
if sys.exc_info()[0] is not None: if sys.exc_info()[0] is not None:
log.exception("curtin_error") log.exception("curtin_error")
# send traceback.format_exc() to journal? # send traceback.format_exc() to journal?
if self.tb_extractor.traceback: if self.tb_extractor.traceback:
kw["Traceback"] = "\n".join(self.tb_extractor.traceback) kw["Traceback"] = "\n".join(self.tb_extractor.traceback)
self.error_ref = self.app.make_apport_report( self.app.fatal_error = self.app.make_apport_report(
ErrorReportKind.INSTALL_FAIL, "install failed", **kw).ref() ErrorReportKind.INSTALL_FAIL, "install failed", **kw)
self.app.update_state(ApplicationState.ERROR)
def logged_command(self, cmd): def logged_command(self, cmd):
return ['systemd-cat', '--level-prefix=false', return ['systemd-cat', '--level-prefix=false',
@ -254,15 +225,15 @@ class InstallController(SubiquityController):
try: try:
await asyncio.wait({e.wait() for e in self.model.install_events}) await asyncio.wait({e.wait() for e in self.model.install_events})
if not self.app.interactive(): if not self.app.interactive:
if 'autoinstall' in self.app.kernel_cmdline: if 'autoinstall' in self.app.kernel_cmdline:
self.model.confirm() self.model.confirm()
self.update_state(InstallState.NEEDS_CONFIRMATION) self.app.update_state(ApplicationState.NEEDS_CONFIRMATION)
await self.model.confirmation.wait() await self.model.confirmation.wait()
self.update_state(InstallState.RUNNING) self.app.update_state(ApplicationState.RUNNING)
if os.path.exists(self.model.target): if os.path.exists(self.model.target):
await self.unmount_target( await self.unmount_target(
@ -270,22 +241,22 @@ class InstallController(SubiquityController):
await self.curtin_install(context=context) await self.curtin_install(context=context)
self.update_state(InstallState.POST_WAIT) self.app.update_state(ApplicationState.POST_WAIT)
await asyncio.wait( await asyncio.wait(
{e.wait() for e in self.model.postinstall_events}) {e.wait() for e in self.model.postinstall_events})
await self.drain_curtin_events(context=context) await self.drain_curtin_events(context=context)
self.update_state(InstallState.POST_RUNNING) self.app.update_state(ApplicationState.POST_RUNNING)
await self.postinstall(context=context) await self.postinstall(context=context)
if self.model.network.has_network: if self.model.network.has_network:
self.update_state(InstallState.UU_RUNNING) self.app.update_state(ApplicationState.UU_RUNNING)
await self.run_unattended_upgrades(context=context) await self.run_unattended_upgrades(context=context)
self.update_state(InstallState.DONE) self.app.update_state(ApplicationState.DONE)
except Exception: except Exception:
self.curtin_error() self.curtin_error()

View File

@ -32,7 +32,7 @@ class LocaleController(SubiquityController):
autoinstall_default = 'en_US.UTF-8' autoinstall_default = 'en_US.UTF-8'
def interactive(self): def interactive(self):
return self.app.interactive() return self.app.interactive
def load_autoinstall_data(self, data): def load_autoinstall_data(self, data):
os.environ["LANG"] = data os.environ["LANG"] = data

View File

@ -24,7 +24,7 @@ from subiquitycore.utils import arun_command, run_command
from subiquity.common.apidef import API from subiquity.common.apidef import API
from subiquity.server.controller import SubiquityController from subiquity.server.controller import SubiquityController
from subiquity.server.controllers.install import InstallState from subiquity.server.controllers.install import ApplicationState
log = logging.getLogger("subiquity.controllers.restart") log = logging.getLogger("subiquity.controllers.restart")
@ -51,10 +51,10 @@ class RebootController(SubiquityController):
await Install.install_task await Install.install_task
await self.app.controllers.Late.run_event.wait() await self.app.controllers.Late.run_event.wait()
await self.copy_logs_to_target() await self.copy_logs_to_target()
if self.app.interactive(): if self.app.interactive:
await self.user_reboot_event.wait() await self.user_reboot_event.wait()
self.reboot() self.reboot()
elif Install.install_state == InstallState.DONE: elif self.app.state == ApplicationState.DONE:
self.reboot() self.reboot()
@with_context() @with_context()

View File

@ -69,7 +69,7 @@ class ReportingController(NonInteractiveController):
app.add_event_listener(self) app.add_event_listener(self)
def load_autoinstall_data(self, data): def load_autoinstall_data(self, data):
if self.app.interactive(): if self.app.interactive:
return return
self.config.update(copy.deepcopy(NON_INTERACTIVE_CONFIG)) self.config.update(copy.deepcopy(NON_INTERACTIVE_CONFIG))
if data is not None: if data is not None:

View File

@ -47,7 +47,6 @@ from subiquity.common.types import (
ApplicationState, ApplicationState,
ApplicationStatus, ApplicationStatus,
ErrorReportRef, ErrorReportRef,
InstallState,
) )
from subiquity.server.controller import SubiquityController from subiquity.server.controller import SubiquityController
from subiquity.models.subiquity import SubiquityModel from subiquity.models.subiquity import SubiquityModel
@ -73,8 +72,11 @@ class MetaController:
if cur == self.app.state: if cur == self.app.state:
await self.app.state_event.wait() await self.app.state_event.wait()
return ApplicationStatus( return ApplicationStatus(
self.app.state, state=self.app.state,
confirming_tty=self.app.confirming_tty,
error=self.app.fatal_error,
cloud_init_ok=self.app.cloud_init_ok, cloud_init_ok=self.app.cloud_init_ok,
interactive=self.app.interactive,
echo_syslog_id=self.app.echo_syslog_id, echo_syslog_id=self.app.echo_syslog_id,
event_syslog_id=self.app.event_syslog_id, event_syslog_id=self.app.event_syslog_id,
log_syslog_id=self.app.log_syslog_id) log_syslog_id=self.app.log_syslog_id)
@ -145,9 +147,11 @@ class SubiquityServer(Application):
super().__init__(opts) super().__init__(opts)
self.block_log_dir = block_log_dir self.block_log_dir = block_log_dir
self.cloud_init_ok = cloud_init_ok self.cloud_init_ok = cloud_init_ok
self._state = ApplicationState.STARTING self._state = ApplicationState.STARTING_UP
self.state_event = asyncio.Event() self.state_event = asyncio.Event()
self.interactive = None
self.confirming_tty = '' self.confirming_tty = ''
self.fatal_error = None
self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid()) self.echo_syslog_id = 'subiquity_echo.{}'.format(os.getpid())
self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid()) self.event_syslog_id = 'subiquity_event.{}'.format(os.getpid())
@ -184,14 +188,14 @@ class SubiquityServer(Application):
self.event_listeners.append(listener) self.event_listeners.append(listener)
def _maybe_push_to_journal(self, event_type, context, description): def _maybe_push_to_journal(self, event_type, context, description):
if not context.get('is-install-context') and self.interactive(): if not context.get('is-install-context') and self.interactive:
controller = context.get('controller') controller = context.get('controller')
if controller is None or controller.interactive(): if controller is None or controller.interactive():
return return
if context.get('request'): if context.get('request'):
return return
indent = context.full_name().count('/') - 2 indent = context.full_name().count('/') - 2
if context.get('is-install-context') and self.interactive(): if context.get('is-install-context') and self.interactive:
indent -= 1 indent -= 1
msg = context.description msg = context.description
else: else:
@ -241,20 +245,14 @@ class SubiquityServer(Application):
return self.error_reporter.make_apport_report( return self.error_reporter.make_apport_report(
kind, thing, wait=wait, **kw) kind, thing, wait=wait, **kw)
def interactive(self):
if not self.autoinstall_config:
return True
return bool(self.autoinstall_config.get('interactive-sections'))
@web.middleware @web.middleware
async def middleware(self, request, handler): async def middleware(self, request, handler):
override_status = None override_status = None
controller = await controller_for_request(request) controller = await controller_for_request(request)
if isinstance(controller, SubiquityController): if isinstance(controller, SubiquityController):
install_state = self.controllers.Install.install_state
if not controller.interactive(): if not controller.interactive():
override_status = 'skip' override_status = 'skip'
elif install_state == InstallState.NEEDS_CONFIRMATION: elif self.state == ApplicationState.NEEDS_CONFIRMATION:
if self.base_model.needs_configuration(controller.model_name): if self.base_model.needs_configuration(controller.model_name):
override_status = 'confirm' override_status = 'confirm'
if override_status is not None: if override_status is not None:
@ -326,18 +324,19 @@ class SubiquityServer(Application):
if self.autoinstall_config and self.controllers.Early.cmds: if self.autoinstall_config and self.controllers.Early.cmds:
stamp_file = self.state_path("early-commands") stamp_file = self.state_path("early-commands")
if not os.path.exists(stamp_file): if not os.path.exists(stamp_file):
self.update_state(ApplicationState.EARLY_COMMANDS)
await self.controllers.Early.run() await self.controllers.Early.run()
open(stamp_file, 'w').close() open(stamp_file, 'w').close()
await asyncio.sleep(1)
self.load_autoinstall_config(only_early=False) self.load_autoinstall_config(only_early=False)
if not self.interactive() and not self.opts.dry_run: if self.autoinstall_config:
self.interactive = bool(
self.autoinstall_config.get('interactive-sections'))
else:
self.interactive = True
if not self.interactive and not self.opts.dry_run:
open('/run/casper-no-prompt', 'w').close() open('/run/casper-no-prompt', 'w').close()
self.load_serialized_state() self.load_serialized_state()
if self.interactive(): self.update_state(ApplicationState.WAITING)
self.update_state(ApplicationState.INTERACTIVE)
else:
self.update_state(ApplicationState.NON_INTERACTIVE)
await asyncio.sleep(1)
await super().start() await super().start()
await self.apply_autoinstall_config() await self.apply_autoinstall_config()

View File

@ -33,7 +33,7 @@ from subiquitycore.ui.utils import button_pile, Padding, rewrap
from subiquitycore.ui.stretchy import Stretchy from subiquitycore.ui.stretchy import Stretchy
from subiquitycore.ui.width import widget_width from subiquitycore.ui.width import widget_width
from subiquity.common.types import InstallState from subiquity.common.types import ApplicationState
log = logging.getLogger("subiquity.views.installprogress") log = logging.getLogger("subiquity.views.installprogress")
@ -138,22 +138,22 @@ class ProgressView(BaseView):
self._set_button_width() self._set_button_width()
def update_for_state(self, state): def update_for_state(self, state):
if state == InstallState.NOT_STARTED: if state == ApplicationState.WAITING:
self.title = _("Installing system") self.title = _("Installing system")
btns = [] btns = []
elif state == InstallState.NEEDS_CONFIRMATION: elif state == ApplicationState.NEEDS_CONFIRMATION:
self.title = _("Installing system") self.title = _("Installing system")
btns = [] btns = []
elif state == InstallState.RUNNING: elif state == ApplicationState.RUNNING:
self.title = _("Installing system") self.title = _("Installing system")
btns = [self.view_log_btn] btns = [self.view_log_btn]
elif state == InstallState.POST_WAIT: elif state == ApplicationState.POST_WAIT:
self.title = _("Installing system") self.title = _("Installing system")
btns = [self.view_log_btn] btns = [self.view_log_btn]
elif state == InstallState.POST_RUNNING: elif state == ApplicationState.POST_RUNNING:
self.title = _("Installing system") self.title = _("Installing system")
btns = [self.view_log_btn] btns = [self.view_log_btn]
elif state == InstallState.UU_RUNNING: elif state == ApplicationState.UU_RUNNING:
self.title = _("Install complete!") self.title = _("Install complete!")
self.reboot_btn.base_widget.set_label( self.reboot_btn.base_widget.set_label(
_("Cancel update and reboot")) _("Cancel update and reboot"))
@ -161,7 +161,7 @@ class ProgressView(BaseView):
self.view_log_btn, self.view_log_btn,
self.reboot_btn, self.reboot_btn,
] ]
elif state == InstallState.UU_CANCELLING: elif state == ApplicationState.UU_CANCELLING:
self.title = _("Install complete!") self.title = _("Install complete!")
self.reboot_btn.base_widget.set_label(_("Rebooting...")) self.reboot_btn.base_widget.set_label(_("Rebooting..."))
self.reboot_btn.enabled = False self.reboot_btn.enabled = False
@ -169,14 +169,14 @@ class ProgressView(BaseView):
self.view_log_btn, self.view_log_btn,
self.reboot_btn, self.reboot_btn,
] ]
elif state == InstallState.DONE: elif state == ApplicationState.DONE:
self.title = _("Install complete!") self.title = _("Install complete!")
self.reboot_btn.base_widget.set_label(_("Reboot Now")) self.reboot_btn.base_widget.set_label(_("Reboot Now"))
btns = [ btns = [
self.view_log_btn, self.view_log_btn,
self.reboot_btn, self.reboot_btn,
] ]
elif state == InstallState.ERROR: elif state == ApplicationState.ERROR:
self.title = _('An error occurred during installation') self.title = _('An error occurred during installation')
self.reboot_btn.base_widget.set_label(_("Reboot Now")) self.reboot_btn.base_widget.set_label(_("Reboot Now"))
self.reboot_btn.enabled = True self.reboot_btn.enabled = True

View File

@ -4,7 +4,7 @@ from unittest import mock
from subiquitycore.testing import view_helpers from subiquitycore.testing import view_helpers
from subiquity.client.controllers.progress import ProgressController from subiquity.client.controllers.progress import ProgressController
from subiquity.common.types import InstallState from subiquity.common.types import ApplicationState
from subiquity.ui.views.installprogress import ProgressView from subiquity.ui.views.installprogress import ProgressView
@ -28,7 +28,7 @@ class IdentityViewTests(unittest.TestCase):
view = self.make_view() view = self.make_view()
btn = view_helpers.find_button_matching(view, "^Reboot Now$") btn = view_helpers.find_button_matching(view, "^Reboot Now$")
self.assertIs(btn, None) self.assertIs(btn, None)
view.update_for_state(InstallState.DONE) view.update_for_state(ApplicationState.DONE)
btn = view_helpers.find_button_matching(view, "^Reboot Now$") btn = view_helpers.find_button_matching(view, "^Reboot Now$")
self.assertIsNot(btn, None) self.assertIsNot(btn, None)
view_helpers.click(btn) view_helpers.click(btn)