Пример #1
0
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)
Пример #2
0
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()
Пример #3
0
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()
Пример #4
0
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()