class RefreshController(SubiquityController): endpoint = API.refresh autoinstall_key = "refresh-installer" autoinstall_schema = { 'type': 'object', 'properties': { 'update': { 'type': 'boolean' }, 'channel': { 'type': 'string' }, }, 'additionalProperties': False, } signals = [ ('snapd-network-change', 'snapd_network_changed'), ] def __init__(self, app): super().__init__(app) self.ai_data = {} self.snap_name = os.environ.get("SNAP_NAME", "subiquity") self.configure_task = None self.check_task = None self.status = RefreshStatus(availability=RefreshCheckState.UNKNOWN) def load_autoinstall_data(self, data): if data is not None: self.ai_data = data @property def active(self): if 'update' in self.ai_data: return self.ai_data['update'] else: return self.interactive() def start(self): if not self.active: return self.configure_task = schedule_task(self.configure_snapd()) self.check_task = SingleInstanceTask(self.check_for_update, propagate_errors=False) self.check_task.start_sync() @with_context() async def apply_autoinstall_config(self, context, index=1): if not self.active: return try: await asyncio.wait_for(self.check_task.wait(), 60) except asyncio.TimeoutError: return if self.status.availability != RefreshCheckState.AVAILABLE: return change_id = await self.start_update(context=context) while True: change = await self.get_progress(change_id) if change['status'] not in ['Do', 'Doing', 'Done']: raise Exception("update failed: %s", change['status']) await asyncio.sleep(0.1) @with_context() async def configure_snapd(self, context): with context.child("get_details") as subcontext: try: r = await self.app.snapd.get( 'v2/snaps/{snap_name}'.format(snap_name=self.snap_name)) except requests.exceptions.RequestException: log.exception("getting snap details") return self.status.current_snap_version = r['result']['version'] for k in 'channel', 'revision', 'version': self.app.note_data_for_apport("Snap" + k.title(), r['result'][k]) subcontext.description = "current version of snap is: %r" % ( self.status.current_snap_version) channel = self.get_refresh_channel() desc = "switching {} to {}".format(self.snap_name, channel) with context.child("switching", desc) as subcontext: try: await self.app.snapd.post_and_wait( 'v2/snaps/{}'.format(self.snap_name), { 'action': 'switch', 'channel': channel }) except requests.exceptions.RequestException: log.exception("switching channels") return subcontext.description = "switched to " + channel def get_refresh_channel(self): """Return the channel we should refresh subiquity to.""" prefix = "subiquity-channel=" for arg in self.app.kernel_cmdline: if arg.startswith(prefix): log.debug("get_refresh_channel: found %s on kernel cmdline", arg) return arg[len(prefix):] if 'channel' in self.ai_data: return self.ai_data['channel'] info_file = '/cdrom/.disk/info' try: fp = open(info_file) except FileNotFoundError: if self.opts.dry_run: info = ('Ubuntu-Server 18.04.2 LTS "Bionic Beaver" - ' 'Release amd64 (20190214.3)') else: log.debug( "get_refresh_channel: failed to find .disk/info file") return else: with fp: info = fp.read() release = info.split()[1] return 'stable/ubuntu-' + release def snapd_network_changed(self): if self.active and \ self.status.availability == RefreshCheckState.UNKNOWN: self.check_task.start_sync() @with_context() async def check_for_update(self, context): await asyncio.shield(self.configure_task) if self.app.updated: context.description = "not offered update when already updated" self.status.availability = RefreshCheckState.UNAVAILABLE return try: result = await self.app.snapd.get('v2/find', select='refresh') except requests.exceptions.RequestException: log.exception("checking for snap update failed") context.description = "checking for snap update failed" self.status.availability = RefreshCheckState.UNKNOWN return log.debug("check_for_update received %s", result) for snap in result["result"]: if snap["name"] == self.snap_name: self.status.new_snap_version = snap["version"] context.description = ("new version of snap available: %r" % self.status.new_snap_version) self.status.availability = RefreshCheckState.AVAILABLE return else: context.description = "no new version of snap available" self.status.availability = RefreshCheckState.UNAVAILABLE @with_context() async def start_update(self, context): open(self.app.state_path('updating'), 'w').close() change = await self.app.snapd.post( 'v2/snaps/{}'.format(self.snap_name), {'action': 'refresh'}) context.description = "change id: {}".format(change) return change async def get_progress(self, change): result = await self.app.snapd.get('v2/changes/{}'.format(change)) change = result['result'] if change['status'] == 'Done': # Clearly if we got here we didn't get restarted by # snapd/systemctl (dry-run mode) self.app.restart() return change async def GET(self, wait: bool = False) -> RefreshStatus: if wait: await self.check_task.wait() return self.status async def POST(self, context) -> str: return await self.start_update(context=context) async def progress_GET(self, change_id: str) -> dict: return await self.get_progress(change_id)
class MirrorController(SubiquityController): autoinstall_key = "apt" model_name = "mirror" signals = [ ('snapd-network-change', 'snapd_network_changed'), ] def __init__(self, app): self.ai_data = {} super().__init__(app) self.check_state = CheckState.NOT_STARTED if 'country-code' in self.answers: self.check_state = CheckState.DONE self.model.set_country(self.answers['country-code']) self.lookup_task = SingleInstanceTask(self.lookup) self.geoip_enabled = True def load_autoinstall_data(self, data): if data is None: return geoip = data.pop('geoip', True) merge_config(self.model.config, data) self.geoip_enabled = geoip and self.model.is_default() async def apply_autoinstall_config(self): if not self.geoip_enabled: return try: await asyncio.wait_for(self.lookup_task.wait(), 10) except asyncio.TimeoutError: pass def snapd_network_changed(self): if not self.geoip_enabled: return if self.check_state != CheckState.DONE: self.check_state = CheckState.CHECKING self.lookup_task.start_sync() async def lookup(self): with self.context.child("lookup"): try: response = await run_in_thread( requests.get, "https://geoip.ubuntu.com/lookup") response.raise_for_status() except requests.exceptions.RequestException: log.exception("geoip lookup failed") self.check_state = CheckState.FAILED return try: e = ElementTree.fromstring(response.text) except ElementTree.ParseError: log.exception("parsing %r failed", response.text) self.check_state = CheckState.FAILED return cc = e.find("CountryCode") if cc is None: log.debug("no CountryCode found in %r", response.text) self.check_state = CheckState.FAILED return cc = cc.text.lower() if len(cc) != 2: log.debug("bogus CountryCode found in %r", response.text) self.check_state = CheckState.FAILED return self.check_state = CheckState.DONE self.model.set_country(cc) def start_ui(self): self.check_state = CheckState.DONE self.ui.set_body(MirrorView(self.model, self)) if 'mirror' in self.answers: self.done(self.answers['mirror']) elif 'country-code' in self.answers \ or 'accept-default' in self.answers: self.done(self.model.get_mirror()) def cancel(self): self.app.prev_screen() def serialize(self): return self.model.get_mirror() def deserialize(self, data): super().deserialize(data) self.model.set_mirror(data) def done(self, mirror): log.debug("MirrorController.done next_screen mirror=%s", mirror) if mirror != self.model.get_mirror(): self.model.set_mirror(mirror) self.configured() self.app.next_screen()
class RefreshController(SubiquityController): autoinstall_key = "refresh-installer" signals = [ ('snapd-network-change', 'snapd_network_changed'), ] def __init__(self, app): super().__init__(app) self.snap_name = os.environ.get("SNAP_NAME", "subiquity") self.configure_task = None self.check_task = None self.current_snap_version = "unknown" self.new_snap_version = "" self.offered_first_time = False self.active = self.interactive() def load_autoinstall_data(self, data): if data is not None and data.get('refresh'): self.active = True def start(self): if not self.active: return self.configure_task = schedule_task(self.configure_snapd()) self.check_task = SingleInstanceTask(self.check_for_update, propagate_errors=False) self.check_task.start_sync() async def apply_autoinstall_config(self, index=1): if not self.active: return try: await asyncio.wait_for(self.check_task.wait(), 60) except asyncio.TimeoutError: return if self.check_state != CheckState.AVAILABLE: return change_id = await self.start_update() while True: try: change = await self.controller.get_progress(change_id) except requests.exceptions.RequestException as e: raise e if change['status'] == 'Done': # Will only get here dry run mode as part of the refresh is us # getting restarted by snapd... return if change['status'] not in ['Do', 'Doing']: raise Exception("update failed") await asyncio.sleep(0.1) @property def check_state(self): if not self.active: return CheckState.UNAVAILABLE task = self.check_task.task if not task.done() or task.cancelled(): return CheckState.UNKNOWN if task.exception(): return CheckState.UNAVAILABLE return task.result() async def configure_snapd(self): with self.context.child("configure_snapd") as context: with context.child("get_details") as subcontext: try: r = await self.app.snapd.get( 'v2/snaps/{snap_name}'.format(snap_name=self.snap_name) ) except requests.exceptions.RequestException: log.exception("getting snap details") return self.current_snap_version = r['result']['version'] for k in 'channel', 'revision', 'version': self.app.note_data_for_apport("Snap" + k.title(), r['result'][k]) subcontext.description = "current version of snap is: %r" % ( self.current_snap_version) channel = self.get_refresh_channel() desc = "switching {} to {}".format(self.snap_name, channel) with context.child("switching", desc) as subcontext: try: await self.app.snapd.post_and_wait( 'v2/snaps/{}'.format(self.snap_name), { 'action': 'switch', 'channel': channel }) except requests.exceptions.RequestException: log.exception("switching channels") return subcontext.description = "switched to " + channel def get_refresh_channel(self): """Return the channel we should refresh subiquity to.""" if 'channel' in self.answers: return self.answers['channel'] with open('/proc/cmdline') as fp: cmdline = fp.read() prefix = "subiquity-channel=" for arg in cmdline.split(): if arg.startswith(prefix): log.debug("get_refresh_channel: found %s on kernel cmdline", arg) return arg[len(prefix):] info_file = '/cdrom/.disk/info' try: fp = open(info_file) except FileNotFoundError: if self.opts.dry_run: info = ('Ubuntu-Server 18.04.2 LTS "Bionic Beaver" - ' 'Release amd64 (20190214.3)') else: log.debug( "get_refresh_channel: failed to find .disk/info file") return else: with fp: info = fp.read() release = info.split()[1] return 'stable/ubuntu-' + release def snapd_network_changed(self): if self.check_state == CheckState.UNKNOWN: self.check_task.start_sync() async def check_for_update(self): await asyncio.shield(self.configure_task) with self.context.child("check_for_update") as context: if self.app.updated: context.description = ( "not offered update when already updated") return CheckState.UNAVAILABLE result = await self.app.snapd.get('v2/find', select='refresh') log.debug("check_for_update received %s", result) for snap in result["result"]: if snap["name"] == self.snap_name: self.new_snap_version = snap["version"] context.description = ( "new version of snap available: %r" % self.new_snap_version) return CheckState.AVAILABLE else: context.description = ("no new version of snap available") return CheckState.UNAVAILABLE async def start_update(self): update_marker = os.path.join(self.app.state_dir, 'updating') open(update_marker, 'w').close() with self.context.child("starting_update") as context: change = await self.app.snapd.post( 'v2/snaps/{}'.format(self.snap_name), {'action': 'refresh'}) context.description = "change id: {}".format(change) return change async def get_progress(self, change): result = await self.app.snapd.get('v2/changes/{}'.format(change)) return result['result'] def start_ui(self, index=1): from subiquity.ui.views.refresh import RefreshView if self.app.updated: raise Skip() show = False if index == 1: if self.check_state == CheckState.AVAILABLE: show = True self.offered_first_time = True elif index == 2: if not self.offered_first_time: if self.check_state in [ CheckState.UNKNOWN, CheckState.AVAILABLE ]: show = True else: raise AssertionError("unexpected index {}".format(index)) if show: self.ui.set_body(RefreshView(self)) else: raise Skip() def done(self, sender=None): log.debug("RefreshController.done next_screen") self.app.next_screen() def cancel(self, sender=None): self.app.prev_screen()
class MirrorController(SubiquityController): endpoint = API.mirror autoinstall_key = "apt" autoinstall_schema = { # This is obviously incomplete. 'type': 'object', 'properties': { 'preserve_sources_list': {'type': 'boolean'}, 'primary': {'type': 'array'}, 'geoip': {'type': 'boolean'}, 'sources': {'type': 'object'}, }, } model_name = "mirror" signals = [ ('snapd-network-change', 'snapd_network_changed'), ] def __init__(self, app): super().__init__(app) self.geoip_enabled = True self.check_state = CheckState.NOT_STARTED self.lookup_task = SingleInstanceTask(self.lookup) def load_autoinstall_data(self, data): if data is None: return geoip = data.pop('geoip', True) merge_config(self.model.config, data) self.geoip_enabled = geoip and self.model.is_default() @with_context() async def apply_autoinstall_config(self, context): if not self.geoip_enabled: return if self.lookup_task.task is None: return try: with context.child('waiting'): await asyncio.wait_for(self.lookup_task.wait(), 10) except asyncio.TimeoutError: pass def snapd_network_changed(self): if not self.geoip_enabled: return if self.check_state != CheckState.DONE: self.check_state = CheckState.CHECKING self.lookup_task.start_sync() @with_context() async def lookup(self, context): try: response = await run_in_thread(requests.get, "https://geoip.ubuntu.com/lookup") response.raise_for_status() except requests.exceptions.RequestException: log.exception("geoip lookup failed") self.check_state = CheckState.FAILED return try: e = ElementTree.fromstring(response.text) except ElementTree.ParseError: log.exception("parsing %r failed", response.text) self.check_state = CheckState.FAILED return cc = e.find("CountryCode") if cc is None: log.debug("no CountryCode found in %r", response.text) self.check_state = CheckState.FAILED return cc = cc.text.lower() if len(cc) != 2: log.debug("bogus CountryCode found in %r", response.text) self.check_state = CheckState.FAILED return self.check_state = CheckState.DONE self.model.set_country(cc) def serialize(self): return self.model.get_mirror() def deserialize(self, data): self.model.set_mirror(data) def make_autoinstall(self): r = self.model.render()['apt'] r['geoip'] = self.geoip_enabled return r async def GET(self) -> str: return self.model.get_mirror() async def POST(self, data: str): self.model.set_mirror(data) self.configured()