diff --git a/bin/subiquity-tui b/bin/subiquity-tui index 9fe62ff5..f119493c 100755 --- a/bin/subiquity-tui +++ b/bin/subiquity-tui @@ -83,8 +83,10 @@ def parse_options(argv): help=("Load snap details from examples/snaps instead of store. " "See examples/snaps/README.md for more.")) parser.add_argument( - '--snap-section', action='store', default='developers', # will change default to server when that exists - help=("Show snaps from this section of the store in the snap list screen.")) + # will change default to server when that exists + '--snap-section', action='store', default='developers', + help=("Show snaps from this section of the store in the snap " + "list screen.")) return parser.parse_args(argv) diff --git a/subiquity/controllers/installprogress.py b/subiquity/controllers/installprogress.py index a26c8793..d30ad7a2 100644 --- a/subiquity/controllers/installprogress.py +++ b/subiquity/controllers/installprogress.py @@ -134,7 +134,8 @@ class DownloadSnapTask(BackgroundTask): self.channel = channel def start(self): - self.controller._install_event_start(_("downloading {}").format(self.snap_name)) + self.controller._install_event_start( + _("downloading {}").format(self.snap_name)) os.mkdir(self.this_snap_download_dir) self.proc = utils.start_command( ['snap', 'download', '--channel='+self.channel, self.snap_name], @@ -144,13 +145,14 @@ class DownloadSnapTask(BackgroundTask): stdout, stderr = self.proc.communicate() if self.proc.returncode != 0: raise subprocess.CalledProcessError( - self.proc.returncode, self.proc.args, output=stdout, stderr=stderr) + self.proc.returncode, self.proc.args, output=stdout, + stderr=stderr) def end(self, observer, fut): self.controller._install_event_finish() try: fut.result() - except: + except BaseException: shutil.rmtree(self.this_snap_download_dir) raise else: @@ -161,10 +163,14 @@ class UpdateSnapSeed(BackgroundTask): def __init__(self, controller, root): self.controller = controller - self.seed_yaml = os.path.join(root, "var/lib/snapd/seed/seed.yaml") - self.tmp_dir = os.path.join(root, "var/lib/snapd/seed/tmp") - self.snap_dir = os.path.join(root, "var/lib/snapd/seed/snaps") - self.assertions_dir = os.path.join(root, "var/lib/snapd/seed/assertions") + self.seed_yaml = os.path.join( + root, "var/lib/snapd/seed/seed.yaml") + self.tmp_dir = os.path.join( + root, "var/lib/snapd/seed/tmp") + self.snap_dir = os.path.join( + root, "var/lib/snapd/seed/snaps") + self.assertions_dir = os.path.join( + root, "var/lib/snapd/seed/assertions") def start(self): self.controller._install_event_start(_("updating snap seed")) @@ -175,21 +181,28 @@ class UpdateSnapSeed(BackgroundTask): with open(self.seed_yaml) as fp: seed = yaml.safe_load(fp) + to_install = self.controller.base_model.snaplist.to_install for snap_name in os.listdir(self.tmp_dir): this_snap_download_dir = os.path.join(self.tmp_dir, snap_name) - [snap_path] = glob.glob(os.path.join(this_snap_download_dir, "*.snap")) - [assertion_path] = glob.glob(os.path.join(this_snap_download_dir, "*.assert")) + [snap_path] = glob.glob( + os.path.join(this_snap_download_dir, "*.snap")) + [assertion_path] = glob.glob( + os.path.join(this_snap_download_dir, "*.assert")) snap_file = os.path.basename(snap_path) assertion_file = os.path.basename(assertion_path) - os.rename(snap_path, os.path.join(self.snap_dir, snap_file)) - os.rename(assertion_path, os.path.join(self.assertions_dir, assertion_file)) + os.rename( + snap_path, + os.path.join(self.snap_dir, snap_file)) + os.rename( + assertion_path, + os.path.join(self.assertions_dir, assertion_file)) # If this directory is not empty, something very # unexpected has happened and we should fail. os.rmdir(this_snap_download_dir) - selection = self.controller.base_model.snaplist.to_install[snap_name] + selection = to_install[snap_name] seedinfo = { 'name': snap_name, 'file': snap_file, @@ -212,6 +225,39 @@ class UpdateSnapSeed(BackgroundTask): observer.task_succeeded() +class SnapSeedTaskWatcher(TaskWatcher): + def __init__(self, controller): + self.controller = controller + self.tasklist = [] + + def task_complete(self, stage): + self.tasklist.pop(0) + + def task_error(self, stage, info): + if stage.startswith("download"): + curtask = self.tasklist[0][1] + explanation = None + if isinstance(info, tuple): + log.debug("xxx %s", info) + if isinstance(info[1], subprocess.CalledProcessError): + explanation = info[1].stderr.strip() + else: + explanation = "".join(traceback.format_exception(*info)) + self.controller.progress_view.ask_for_retry_snap( + self, curtask.snap_name, explanation) + return + if isinstance(info, tuple): + tb = traceback.format_exception(*info) + self.controller.curtin_error("".join(tb)) + else: + self.controller.curtin_error() + + def tasks_finished(self): + self.controller._install_event_finish() + self.controller.loop.set_alarm_in( + 0.0, lambda loop, ud: self.controller.postinstall_complete()) + + class InstallProgressController(BaseController): signals = [ ('installprogress:filesystem-config-done', 'filesystem_config_done'), @@ -237,13 +283,15 @@ class InstallProgressController(BaseController): self.curtin_start_install() def identity_config_done(self): - if self.install_state == InstallState.DONE and self._snap_config_done: + if self.install_state == InstallState.DONE and \ + self._snap_config_done: self.postinstall_configuration() else: self._identity_config_done = True def snap_config_done(self): - if self.install_state == InstallState.DONE and self._identity_config_done: + if self.install_state == InstallState.DONE and \ + self._identity_config_done: self.postinstall_configuration() else: self._snap_config_done = True @@ -295,23 +343,7 @@ class InstallProgressController(BaseController): if event_type not in ['start', 'finish']: return if event_type == 'start': -<<<<<<< HEAD self._install_event_start(event.get("CURTIN_MESSAGE", "??")) -||||||| merged common ancestors - message = event.get("CURTIN_MESSAGE", "??") - if not self.progress_view_showing is None: - self.footer_description.set_text(message) - self.progress_view.add_event(self._event_indent + message) - self._event_indent += " " - self.footer_spinner.start() -======= - message = event.get("CURTIN_MESSAGE", "??") - if self.progress_view_showing is not None: - self.footer_description.set_text(message) - self.progress_view.add_event(self._event_indent + message) - self._event_indent += " " - self.footer_spinner.start() ->>>>>>> master if event_type == 'finish': self._install_event_finish() @@ -417,54 +449,41 @@ class InstallProgressController(BaseController): self.copy_logs_to_target() if self.base_model.snaplist.to_install: - class watcher(TaskWatcher): - def __init__(self, controller): - self.controller = controller - self.tasklist = [] - def task_complete(self, stage): - self.tasklist.pop(0) - def task_error(self, stage, info): - if stage.startswith("download"): - curtask = self.tasklist[0][1] - explanation = None - if isinstance(info, tuple): - log.debug("xxx %s", info) - if isinstance(info[1], subprocess.CalledProcessError): - explanation = info[1].stderr.strip() - else: - explanation = "".join(traceback.format_exception(*info)) - self.controller.progress_view.ask_for_retry_snap(self, curtask.snap_name, explanation) - return - if isinstance(info, tuple): - tb = traceback.format_exception(*info) - self.controller.curtin_error("".join(tb)) - else: - self.controller.curtin_error() - def tasks_finished(self): - self.controller._install_event_finish() - self.controller.loop.set_alarm_in(0.0, lambda loop, ud:self.controller.postinstall_complete()) - w = watcher(self) + w = SnapSeedTaskWatcher(self) if self.opts.dry_run: root = '.subiquity' - shutil.rmtree(os.path.join(root, 'var/lib/snapd/seed'), ignore_errors=True) - os.makedirs(os.path.join(root, 'var/lib/snapd/seed/snaps')) - os.makedirs(os.path.join(root, 'var/lib/snapd/seed/assertions')) - with open(os.path.join(root, 'var/lib/snapd/seed/seed.yaml'), 'w') as fp: - fp.write("snaps:\n- name: core\n channel: stable\n file: core_XXXX.snap") + shutil.rmtree( + os.path.join(root, 'var/lib/snapd/seed'), + ignore_errors=True) + os.makedirs( + os.path.join(root, 'var/lib/snapd/seed/snaps')) + os.makedirs( + os.path.join(root, 'var/lib/snapd/seed/assertions')) + fake_seed = ("snaps:\n" + "- name: core\n" + " channel: stable\n" + " file: core_XXXX.snap") + seed_path = os.path.join(root, 'var/lib/snapd/seed/seed.yaml') + with open(seed_path, 'w') as fp: + fp.write(fake_seed) else: root = TARGET tmp_dir = os.path.join(root, 'var/lib/snapd/seed/tmp') os.mkdir(tmp_dir) w.tasklist.append(('drain', WaitForCurtinEventsTask(self))) - for snap_name, selection in sorted(self.base_model.snaplist.to_install.items()): - w.tasklist.append(("download " + snap_name, DownloadSnapTask(self, tmp_dir, snap_name, selection.channel))) + for snap_name, selection in sorted( + self.base_model.snaplist.to_install.items()): + w.tasklist.append(( + "download " + snap_name, + DownloadSnapTask( + self, tmp_dir, snap_name, selection.channel) + )) w.tasklist.append(("snapseed", UpdateSnapSeed(self, root))) ts = TaskSequence(self.run_in_bg, w.tasklist, w) ts.run() else: self.postinstall_complete() - def postinstall_complete(self): self.ui.set_header(_("Installation complete!")) self.progress_view.set_status(_("Finished install!")) diff --git a/subiquity/controllers/snaplist.py b/subiquity/controllers/snaplist.py index 9e9bb3f8..0d2f01ad 100644 --- a/subiquity/controllers/snaplist.py +++ b/subiquity/controllers/snaplist.py @@ -68,11 +68,12 @@ class SnapdSnapInfoLoader: self.session = requests_unixsocket.Session() self.pending_info_snaps = [] - self.ongoing = {} # {snap:[callbacks]} + self.ongoing = {} # {snap:[callbacks]} def start(self): self.running = True log.debug("loading list of snaps") + def cb(snap_list): if not self.running: return @@ -87,7 +88,8 @@ class SnapdSnapInfoLoader: self.running = False def _bg_fetch_list(self): - return self.session.get(self.url_base + 'section=' + self.store_section, timeout=60) + return self.session.get( + self.url_base + 'section=' + self.store_section, timeout=60) def _fetched_list(self, fut): if not self.running: @@ -139,7 +141,8 @@ class SnapdSnapInfoLoader: self._fetch_info_for_snap(snap, self._fetch_next_info) def _bg_fetch_next_info(self, snap): - return self.session.get(self.url_base + 'name=' + snap.name, timeout=60) + return self.session.get( + self.url_base + 'name=' + snap.name, timeout=60) def _fetched_info(self, snap, fut): if not self.running: @@ -184,9 +187,15 @@ class SnapListController(BaseController): if self.opts.snaps_from_examples: self.loader = SampleDataSnapInfoLoader( self.model, - os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "examples", "snaps")) + os.path.join( + os.path.dirname( + os.path.dirname( + os.path.dirname(__file__))), + "examples", "snaps")) else: - self.loader = SnapdSnapInfoLoader(self.model, self.run_in_bg, self.snapd_socket_path, self.opts.snap_section) + self.loader = SnapdSnapInfoLoader( + self.model, self.run_in_bg, self.snapd_socket_path, + self.opts.snap_section) self.loader.start() def network_config_done(self, netplan_path): @@ -197,8 +206,9 @@ class SnapListController(BaseController): if self.opts.dry_run: cmds = [['sleep', '0.5']] else: - os.makedirs('/etc/systemd/system/snapd.service.d', exist_ok=True) - with open('/etc/systemd/system/snapd.service.d/snap_proxy.conf', 'w') as fp: + dropin_dir = '/etc/systemd/system/snapd.service.d' + os.makedirs(dropin_dir, exist_ok=True) + with open(os.path.join(dropin_dir, 'snap_proxy.conf'), 'w') as fp: fp.write(self.base_model.proxy.proxy_systemd_dropin()) cmds = [ ['systemctl', 'daemon-reload'], diff --git a/subiquity/models/snaplist.py b/subiquity/models/snaplist.py index 17744fb9..0f2be50f 100644 --- a/subiquity/models/snaplist.py +++ b/subiquity/models/snaplist.py @@ -48,13 +48,14 @@ class SnapSelection: risks = ["stable", "candidate", "beta", "edge"] + class SnapListModel: """The overall model for subiquity.""" def __init__(self): self._snap_info = [] self._snaps_by_name = {} - self.to_install = {} # snap_name -> SnapSelection + self.to_install = {} # snap_name -> SnapSelection def load_find_data(self, data): for s in data['result']: diff --git a/subiquity/ui/views/installprogress.py b/subiquity/ui/views/installprogress.py index 4dc85977..b01234c8 100644 --- a/subiquity/ui/views/installprogress.py +++ b/subiquity/ui/views/installprogress.py @@ -37,24 +37,31 @@ class MyLineBox(LineBox): else: return "" + class AskForRetryStretchy(Stretchy): def __init__(self, parent, watcher, snap_name, explanation): self.parent = parent self.watcher = watcher if explanation is None: widgets = [ - Text(_('Downloading the snap "{}" failed for an unknown reason.').format(snap_name)), + Text(_('Downloading the snap "{}" failed for an unknown ' + 'reason.').format(snap_name)), ] stretchy_index = 0 else: widgets = [ - Text(_('Downloading the snap "{}" failed with the following output:').format(snap_name)), + Text(_('Downloading the snap "{}" failed with the following ' + 'output:').format(snap_name)), Text(""), Text(explanation), ] stretchy_index = 2 - retry = other_btn(label=_("Try again"), on_press=self.cont, user_arg=True) - give_up = other_btn(label=_("Give up on this snap"), on_press=self.cont, user_arg=False) + retry = other_btn( + label=_("Try again"), + on_press=self.cont, user_arg=True) + give_up = other_btn( + label=_("Give up on this snap"), + on_press=self.cont, user_arg=False) widgets.extend([ Text(""), Text(_("Would you like to try to download this snap again?")), @@ -68,6 +75,7 @@ class AskForRetryStretchy(Stretchy): self.parent.remove_overlay() self.parent.controller.resume_snap_downloads(self.watcher, retry_cur) + class ProgressView(BaseView): def __init__(self, controller): self.controller = controller @@ -141,7 +149,8 @@ class ProgressView(BaseView): p.focus_position = 1 def ask_for_retry_snap(self, watcher, snap_name, explanation): - self.show_stretchy_overlay(AskForRetryStretchy(self, watcher, snap_name, explanation)) + self.show_stretchy_overlay( + AskForRetryStretchy(self, watcher, snap_name, explanation)) def reboot(self, btn): self.controller.reboot() diff --git a/subiquity/ui/views/snaplist.py b/subiquity/ui/views/snaplist.py index e6650a37..060188b7 100644 --- a/subiquity/ui/views/snaplist.py +++ b/subiquity/ui/views/snaplist.py @@ -79,7 +79,7 @@ class SnapInfoView(Widget): self.needs_focus = True channel_width = (max(len(csi.channel_name) for csi in snap.channels) - + StarRadioButton.reserve_columns + 1) + + StarRadioButton.reserve_columns + 1) max_version = max(len(csi.version) for csi in snap.channels) max_revision = max(len(str(csi.revision)) for csi in snap.channels) + 2 max_size = max(len(humanize_size(csi.size)) for csi in snap.channels) @@ -108,13 +108,15 @@ class SnapInfoView(Widget): ('pack', Text(notes)), ], dividechars=1))) - self.lb_channels = Padding.center_79(NoTabCyclingListBox(self.channels)) + self.lb_channels = Padding.center_79( + NoTabCyclingListBox(self.channels)) contents = [ ('pack', Text("")), ('pack', Padding.center_79(Columns([ Text(snap.name), - ('pack', Text("Publisher: {}".format(snap.publisher), align='right')), + ('pack', Text( + "Publisher: {}".format(snap.publisher), align='right')), ], dividechars=1))), ('pack', Text("")), ('pack', Padding.center_79(Text(snap.summary))), @@ -123,7 +125,9 @@ class SnapInfoView(Widget): ('pack', Text("")), ('weight', 1, self.lb_channels), ('pack', Text("")), - ('pack', button_pile([other_btn(label=_("Close"), on_press=self.close)])), + ('pack', button_pile([ + other_btn(label=_("Close"), on_press=self.close), + ])), ('pack', Text("")), ] self.description_index = contents.index(self.lb_description) @@ -149,7 +153,8 @@ class SnapInfoView(Widget): if o == pack_option: rows_available -= w.rows((maxcol,), focus) - rows_wanted_description = Padding.center_79(self.description).rows((maxcol,), False) + padded_description = Padding.center_79(self.description) + rows_wanted_description = padded_description.rows((maxcol,), False) rows_wanted_channels = len(self.channels) if rows_wanted_channels + rows_wanted_description <= rows_available: @@ -161,7 +166,8 @@ class SnapInfoView(Widget): channel_rows = min(rows_wanted_channels, int(rows_available/3)) description_rows = rows_available - channel_rows - self.pile.contents[self.description_index] = (self.lb_description, self.pile.options('given', description_rows)) + self.pile.contents[self.description_index] = ( + self.lb_description, self.pile.options('given', description_rows)) if description_rows >= rows_wanted_description: self.lb_description.original_widget._selectable = False else: @@ -188,7 +194,9 @@ class FetchingInfo(WidgetWrap): Pile([ ('pack', Text(' ' + text)), ('pack', self.spinner), - ('pack', button_pile([cancel_btn(label=_("Cancel"), on_press=self.close)])), + ('pack', button_pile([ + cancel_btn(label=_("Cancel"), on_press=self.close), + ])), ]))) def close(self, sender=None): @@ -198,6 +206,7 @@ class FetchingInfo(WidgetWrap): self.spinner.stop() self.parent.remove_overlay() + class FetchingFailed(WidgetWrap): def __init__(self, row, snap): @@ -233,7 +242,8 @@ class SnapListRow(WidgetWrap): self.parent = parent self.snap = snap self.box = StarCheckBox(snap.name, on_state_change=self.state_change) - self.name_and_publisher_width = max_name_len + self.box.reserve_columns + max_publisher_len + 2 + self.name_and_publisher_width = ( + max_name_len + self.box.reserve_columns + max_publisher_len + 2) self.two_column = Color.menu_button(Columns([ (max_name_len+self.box.reserve_columns, self.box), Text(snap.summary, wrap='clip'), @@ -248,23 +258,26 @@ class SnapListRow(WidgetWrap): def load_info(self): called = False fi = None + def callback(): nonlocal called called = True if fi is not None: fi.close() - if len(self.snap.channels) == 0: # or other indication of failure + if len(self.snap.channels) == 0: # or other indication of failure ff = FetchingFailed(self, self.snap) self.parent.show_overlay(ff, width=ff.width) else: - cur_channel = None + cur_chan = None if self.snap.name in self.parent.to_install: - cur_channel = self.parent.to_install[self.snap.name].channel - self.parent._w = SnapInfoView(self.parent, self.snap, cur_channel) + cur_chan = self.parent.to_install[self.snap.name].channel + self.parent._w = SnapInfoView(self.parent, self.snap, cur_chan) self.parent.controller.get_snap_info(self.snap, callback) - # If we didn't get callback synchronously, display a dialog while the info loads. + # If we didn't get callback synchronously, display a dialog + # while the info loads. if not called: - fi = FetchingInfo(self.parent, self.snap, self.parent.controller.loop) + fi = FetchingInfo( + self.parent, self.snap, self.parent.controller.loop) self.parent.show_overlay(fi, width=fi.width) def keypress(self, size, key): @@ -288,6 +301,7 @@ class SnapListRow(WidgetWrap): else: return self.two_column.render(size, focus) + class SnapListView(BaseView): title = _("Featured Server Snaps") @@ -295,12 +309,13 @@ class SnapListView(BaseView): def __init__(self, model, controller): self.model = model self.controller = controller - self.to_install = {} # {snap_name: (channel, is_classic)} + self.to_install = {} # {snap_name: (channel, is_classic)} self.load() def load(self, sender=None): spinner = None called = False + def callback(snap_list): nonlocal called called = True