smoosh InstallState into ApplicationState
Having these be separate was slightly confusing, particular as I need to add an "ERROR" state that isn't necessarily due to the install step itself failing. Now we have one enum for the overall status of the installer and a separate field that indicates whether the client should be in interactive or non-interactive mode (or "not-yet-known" which is handled more or less like non-interactive mode).
This commit is contained in:
parent
4798b7f9a5
commit
0ad21b7c96
35
DESIGN.md
35
DESIGN.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(): ...
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue