示例#1
0
文件: qemu.py 项目: thrix/fingertip
 def key_file(self):
     key_file = path.fingertip('ssh_key', 'fingertip')
     mode = os.stat(key_file)[stat.ST_MODE]
     if mode & 0o77:
         self.m.log.debug(f'fixing up permissions on {key_file}')
         os.chmod(key_file, mode & 0o7700)
     return key_file
示例#2
0
    def run(self, load=SNAPSHOT_BASE_NAME, guest_forwards=[], extra_args=[]):
        if load:
            self.vm.time_desync.report(self.vm.time_desync.LARGE)
        run_args = ['-loadvm', load] if load else []

        self.monitor = Monitor(self.vm)
        run_args += ['-qmp', (f'tcp:127.0.0.1:{self.monitor.port},'
                              'server,nowait,nodelay')]

        # TODO: extract SSH into a separate plugin?
        self.vm.ssh = SSH(self.vm,
                          key=path.fingertip('ssh_key', 'fingertip.paramiko'))
        self.vm.shared_directory = SharedDirectory(self.vm)
        self.vm.exec = self.vm.ssh.exec
        ssh_host_forward = f'hostfwd=tcp:127.0.0.1:{self.vm.ssh.port}-:22'
        cache_guest_forward = (CACHE_INTERNAL_IP, CACHE_INTERNAL_PORT,
                               f'nc 127.0.0.1 {self.vm.http_cache.port}')
        guest_forwards = guest_forwards + [cache_guest_forward]
        run_args += ['-device', 'virtio-net,netdev=net0', '-netdev',
                     ','.join(['user', 'id=net0', ssh_host_forward] +
                              (['restrict=yes'] if self.vm.sealed else []) +
                              [f'guestfwd=tcp:{ip}:{port}-cmd:{cmd}'
                               for ip, port, cmd in guest_forwards])]

        image = os.path.join(self.vm.path, 'image.qcow2')
        if self._image_to_clone:
            required_space = os.path.getsize(self._image_to_clone) + 2**30
            lock = fasteners.process_lock.InterProcessLock('/tmp/.fingertip')
            lock.acquire()
            if self.vm._transient and temp.has_space(required_space):
                image = temp.disappearing_file('/tmp', hint='fingertip-qemu')
                reflink.auto(self._image_to_clone, image)
                lock.release()
            else:
                lock.release()
                reflink.auto(self._image_to_clone, image)
            self._image_to_clone = None
        run_args += ['-drive',
                     f'file={image},cache=unsafe,if=virtio,discard=unmap']

        run_args += ['-m', self.ram_size]

        os.makedirs(path.SHARED, exist_ok=True)

        args = QEMU_COMMON_ARGS + self.custom_args + run_args + extra_args
        self.vm.log.debug(' '.join(args))
        if self.vm._backend_mode == 'pexpect':
            pexp = self.vm.log.pseudofile_powered(pexpect.spawn,
                                                  logfile=logging.INFO)
            self.vm.console = pexp(self._qemu, args, echo=False,
                                   timeout=None,
                                   encoding='utf-8', codec_errors='ignore')
            self.live = True
        elif self.vm._backend_mode == 'direct':
            subprocess.run([self._qemu, '-serial', 'mon:stdio'] + args,
                           check=True)
            self.live = False
            self._go_down()
示例#3
0
 def key_file(self):
     key_file = path.fingertip('ssh_key', 'fingertip')
     s = os.stat(key_file)
     mode = s[stat.ST_MODE]
     owner = s[stat.ST_UID]
     # OpenSSH cares about permissions on key file only if the owner
     # matches current user
     if mode & 0o77 and owner == os.getuid():
         self.m.log.debug(f'fixing up permissions on {key_file}')
         os.chmod(key_file, mode & 0o7700)
     return key_file
示例#4
0
文件: ssh.py 项目: t184256/fingertip
def existing():
    # HACK HACK HACK
    key_file = path.fingertip('ssh_key', 'fingertip')
    process = subprocess.Popen(['ps', '-uf'], stdout=subprocess.PIPE)
    stdout, _ = process.communicate()
    ports = [
        int(p) for p in re.findall(r'hostfwd=tcp:127.0.0.1:(\d+)-:22',
                                   stdout.decode())
    ]
    if len(ports) == 1:
        return _connect(ports[0], key_file)
    elif len(ports) > 1:
        print('several fingertip VMs found, which port?')
        for i, p in enumerate(ports):
            print(f'[{i}] {p}')
        c = int(input('> '))
        return _connect(ports[c] if c < len(ports) else c, key_file)
    print('no running fingertip VMs found')
示例#5
0
文件: alpine.py 项目: thrix/fingertip
def first_boot(m):
    ssh_key_fname = path.fingertip('ssh_key', 'fingertip.pub')
    with open(ssh_key_fname) as f:
        ssh_pubkey = f.read().strip()

    with m:
        m.qemu.run(load=None)
        m.console.expect_exact(f'{m.hostname} login: '******'root')
        m.console.expect_exact(m.prompt)

        m.console.sendline(f'install -m 700 -d .ssh')
        m.console.expect_exact(m.prompt)
        m.console.sendline(f'echo "{ssh_pubkey}" >> .ssh/authorized_keys')
        m.console.expect_exact(m.prompt)
        m.expiration.depend_on_a_file(ssh_key_fname)

        m.hooks.unseal.append(lambda: m('/etc/init.d/networking restart'))
    return m
示例#6
0
def install_in_qemu(m=None):

    m = m or fingertip.machine.build('backend.qemu')

    with m:
        ssh_key_path = path.fingertip('ssh_key', 'fingertip.pub')
        with open(ssh_key_path) as f:
            ssh_pubkey = f.read().strip()
        m.expiration.depend_on_a_file(ssh_key_path)

        ks = path.fingertip('fingertip', 'plugins', 'os', 'centos_stream.ks')
        HOSTNAME = 'centos'

        fqdn = HOSTNAME + '.fingertip.local'

        with open(ks) as f:
            ks_text = f.read().format(HOSTNAME=fqdn,
                                      SSH_PUBKEY=ssh_pubkey,
                                      PROXY=m.http_cache.internal_url)
        m.expiration.depend_on_a_file(ks)
        m.http_cache.mock('http://mock/ks', text=ks_text)
        m.log.info(f'fetching kernel: {URL}/isolinux/vmlinuz')
        kernel = os.path.join(m.path, 'kernel')
        m.http_cache.fetch(f'{URL}/isolinux/vmlinuz', kernel)
        m.log.info(f'fetching initrd: {URL}/isolinux/initrd.img')
        initrd = os.path.join(m.path, 'initrd')
        m.http_cache.fetch(f'{URL}/isolinux/initrd.img', initrd)
        append = ('ks=http://mock/ks console=ttyS0 ' +
                  'inst.text inst.notmux inst.cmdline ' +
                  f'proxy={m.http_cache.internal_url} ' + f'repo={URL} ')
        extra_args = ['-kernel', kernel, '-initrd', initrd, '-append', append]

        m.ram.safeguard = '768M'
        with m.ram('>=4G'):
            m.qemu.run(load=None, extra_args=extra_args)
            m.console.expect('Installation complete.')
            m.console.expect('Power down.')
            m.qemu.wait()
        m.qemu.run(load=None)  # cold boot

        def login(username='******', password='******'):
            if username == 'root':
                m.prompt = f'[root@{HOSTNAME} ~]# '
            else:
                m.prompt = f'[{username}@{HOSTNAME} ~]$ '
            m.console.expect(f'{HOSTNAME} login: '******'Password: '******'CentOS Stream installation finished')

        def disable_proxy():
            return m.apply('ansible',
                           'ini_file',
                           path='/etc/yum.conf',
                           section='main',
                           option='proxy',
                           state='absent')

        m.hooks.disable_proxy.append(disable_proxy)

        m.hooks.unseal += [
            lambda: m('systemctl restart NetworkManager'),
            lambda: m('nm-online')
        ]

        m.hooks.timesync.append(lambda: m('hwclock -s'))

        m.centos = 8
        m.dist_git_branch = 'c8s'

        return m
示例#7
0
class SSH:
    _key_file = path.fingertip('ssh_key', 'fingertip')
    key_file_paramiko = path.fingertip('ssh_key', 'fingertip.paramiko')
    pubkey_file = path.fingertip('ssh_key', 'fingertip.pub')

    def __init__(self, m, host='127.0.0.1', port=None):
        self.host, self.port = host, port
        self.m = m
        self.m.log.debug(f'ssh port {self.port}')
        self._transport = None

    def connect(self, force_reconnect=False, retries=12, timeout=1 / 32):
        atexit.register(self.invalidate)
        import paramiko  # ... in parallel with VM spin-up
        if not force_reconnect and self._transport is not None:
            self._transport.send_ignore()
            if self._transport.is_authenticated():
                return  # the transport is already OK
        self._transport = None
        self.m.log.debug('waiting for the VM to spin up and offer SSH...')
        pubkey = paramiko.ECDSAKey.from_private_key_file(SSH.key_file_paramiko)

        def connect():
            self.m.log.debug('Trying to connect ...')
            t = paramiko.Transport((self.host, self.port))
            t.start_client()
            return t

        transport = repeatedly.keep_trying(connect,
                                           paramiko.ssh_exception.SSHException,
                                           retries=retries,
                                           timeout=timeout)
        transport.auth_publickey('root', pubkey)
        self._transport = transport

    def invalidate(self):
        # gracefully terminate transport channel
        if self._transport:
            self.m.log.debug('Closing SSH session')
            self._transport.close()
        self._transport = None
        atexit.unregister(self.invalidate)

    def _stream_out_and_err(self, channel, quiet=False):
        m_log = self.m.log.info if not quiet else self.m.log.debug
        sel = selectors.DefaultSelector()
        sel.register(channel, selectors.EVENT_READ)
        out, err, out_buf, err_buf = b'', b'', b'', b''
        last_out_time = time.time()
        silence_min = 0
        linebreak = ord(b'\n')
        while True:
            sel.select(timeout=10)
            activity = False
            while channel.recv_ready():
                r = channel.recv(16384)
                out += r
                out_buf += r
                if linebreak in r:
                    out_lines = out_buf.split(b'\n')
                    for out_line in out_lines[:-1]:
                        m_log(log.strip_control_sequences(out_line))
                    out_buf = out_lines[-1]
                activity = True
            while channel.recv_stderr_ready():
                r = channel.recv_stderr(16384)
                err += r
                err_buf += r
                if linebreak in r:
                    err_lines = err_buf.split(b'\n')
                    for err_line in err_lines[:-1]:
                        m_log(log.strip_control_sequences(err_line))
                    err_buf = err_lines[-1]
                activity = True
            if activity:
                last_out_time = time.time()
            else:
                if channel.exit_status_ready():
                    return out, err
                new_silence_min = int(time.time() - last_out_time) // 60
                if new_silence_min > silence_min:
                    silence_min = new_silence_min
                    self.m.log.debug(f'- no output for {silence_min} min -')

    def exec(self, *cmd, shell=False, quiet=False):
        cmd = (' '.join(["'" + a.replace("'", r"\'") + "'"
                         for a in cmd]) if not shell else cmd[0])
        self.connect()
        try:
            channel = self._transport.open_session()
        except EOFError:
            self.m.log.warning('EOFError on SSH exec, retrying in 2 sec...')
            time.sleep(2)
            self.connect(force_reconnect=True)
            channel = self._transport.open_session()
        log_func = self.m.log.debug if quiet else self.m.log.info
        for l in cmd.split('\n'):
            log_func(f'ssh: {l}')
        channel.exec_command(cmd)
        out, err = self._stream_out_and_err(channel)
        retval = channel.recv_exit_status()
        return fingertip.exec.ExecResult(retval, out, err)

    def upload(self, src, dst=None):  # may be unused
        import paramiko  # ... in parallel with VM spin-up
        assert os.path.isfile(src)
        dst = dst or os.path.basename(src)
        self.connect()
        sftp_client = paramiko.SFTPClient.from_transport(self._transport)
        sftp_client.put(src, dst)
        sftp_client.chmod(dst, os.stat(src).st_mode)

    def download(self, src, dst=None):
        import paramiko  # ... in parallel with VM spin-up
        dst = dst or os.path.basename(src)
        self.connect()
        sftp_client = paramiko.SFTPClient.from_transport(self._transport)
        sftp_client.stat(src)  # don't truncate dst if src does not exist
        sftp_client.get(src, dst)

    @property
    def key_file(self):
        s = os.stat(SSH._key_file)
        mode = s[stat.ST_MODE]
        owner = s[stat.ST_UID]
        # OpenSSH cares about permissions on key file only if the owner
        # matches current user
        if mode & 0o77 and owner == os.getuid():
            self.m.log.debug(f'fixing up permissions on {SSH._key_file}')
            os.chmod(SSH._key_file, mode & 0o7700)
        return SSH._key_file

    @property
    def pubkey(self):
        self.m.expiration.depend_on_a_file(SSH.pubkey_file)
        with open(SSH.pubkey_file) as f:
            return f.read().strip()
示例#8
0
def install_in_qemu(m, version, updates=True,
                    mirror=None, resolve_redirect=False):
    ml_norm = ('http://mirrors.fedoraproject.org/metalink' +
               f'?repo=fedora-{version}&arch=x86_64&protocol=http')
    ml_upd = ('http://mirrors.fedoraproject.org/metalink' +
              f'?repo=updates-released-f{version}&arch=x86_64&protocol=http')
    releases_development = 'development' if version == '32' else 'releases'
    if mirror:
        if resolve_redirect:
            m.log.info(f'autoselecting mirror by redirect from {mirror}...')
            mirror = determine_mirror(mirror)
        url = f'{mirror}/{releases_development}/{version}/Everything/x86_64/os'
        upd = f'{mirror}/updates/{version}/Everything/x86_64'
        repos = (f'url --url {url}\n' +
                 f'repo --name fedora --baseurl {url}\n' +
                 (f'repo --name updates --baseurl {upd}'
                  if updates else ''))
    else:
        m.log.info('autoselecting mirror...')
        mirror = determine_mirror(FEDORA_GEOREDIRECTOR)
        url = f'{mirror}/{releases_development}/{version}/Everything/x86_64/os'
        repos = (f'url --url {url}\n' +
                 f'repo --name fedora --metalink {ml_norm}\n' +
                 (f'repo --name updates --metalink {ml_upd}'
                  if updates else ''))
    m.log.info(f'selected mirror: {mirror}')

    m.expiration.cap('2d')  # non-immutable repositories

    original_ram_size = m.qemu.ram_size

    with m:
        m.qemu.ram_size = '2G'

        ssh_key_fname = path.fingertip('ssh_key', 'fingertip.pub')
        with open(ssh_key_fname) as f:
            ssh_pubkey = f.read().strip()
        m.expiration.depend_on_a_file(ssh_key_fname)

        ks_fname = path.fingertip('kickstart_templates', f'fedora{version}')
        with open(ks_fname) as f:
            ks_text = f.read().format(HOSTNAME=f'fedora{version}',
                                      SSH_PUBKEY=ssh_pubkey,
                                      PROXY=m.http_cache.internal_url,
                                      REPOS=repos)
        m.expiration.depend_on_a_file(ks_fname)

        m.http_cache.mock('http://ks', text=ks_text)
        m.log.info(f'fetching kernel: {url}/isolinux/vmlinuz')
        kernel = os.path.join(m.path, 'kernel')
        m.http_cache.fetch(f'{url}/isolinux/vmlinuz', kernel)
        m.log.info(f'fetching initrd: {url}/isolinux/initrd.img')
        initrd = os.path.join(m.path, 'initrd')
        m.http_cache.fetch(f'{url}/isolinux/initrd.img', initrd)
        append = ('ks=http://ks inst.ksstrict console=ttyS0 inst.notmux '
                  f'proxy={m.http_cache.internal_url} ' +
                  f'inst.proxy={m.http_cache.internal_url} ' +
                  f'inst.repo={url}')
        extra_args = ['-kernel', kernel, '-initrd', initrd, '-append', append]

        m.qemu.run(load=None, extra_args=extra_args)
        i = m.console.expect(['Storing configuration files and kickstarts',
                              'installation failed',
                              'installation was stopped',
                              'installer will now terminate'])
        assert i == 0, 'Installation failed'
        m.qemu.wait()
        m.qemu.compress_image()
        m.qemu.ram_size = original_ram_size
        m.qemu.run(load=None)  # cold boot
        HOSTNAME = f'fedora{version}'
        ROOT_PASSWORD = '******'
        m.prompt = f'[root@{HOSTNAME} ~]# '
        m.console.expect(f'{HOSTNAME} login: '******'root')
        m.console.expect('Password: '******'Fedora installation finished')

        def disable_proxy():
            return m.apply('ansible', 'ini_file', path='/etc/dnf/dnf.conf',
                           section='main', option='proxy', state='absent')
        m.hooks.disable_proxy.append(disable_proxy)

        m.hooks.unseal += [lambda: m('systemctl restart NetworkManager'),
                           lambda: m('nm-online')]

        m.fedora = version

        return m
示例#9
0
    def run(self, load=SNAPSHOT_BASE_NAME, guest_forwards=[], extra_args=[]):
        if load:
            self.vm.time_desync.report(self.vm.time_desync.LARGE)
        run_args = ['-loadvm', load] if load else []

        self.monitor = Monitor(self.vm)
        run_args += [
            '-qmp',
            (f'tcp:127.0.0.1:{self.monitor.port},'
             'server,nowait,nodelay')
        ]

        # TODO: extract SSH into a separate plugin?
        self.vm.ssh = SSH(self.vm,
                          key=path.fingertip('ssh_key', 'fingertip.paramiko'))
        self.vm.shared_directory = SharedDirectory(self.vm)
        self.vm.exec = self.vm.ssh.exec
        host_forwards = [(self.vm.ssh.port, 22)] + self.vm._host_forwards
        host_forwards = [
            f'hostfwd=tcp:127.0.0.1:{h}-:{g}' for h, g in host_forwards
        ]
        cache_guest_forward = (CACHE_INTERNAL_IP, CACHE_INTERNAL_PORT,
                               f'nc 127.0.0.1 {self.vm.http_cache.port}')
        guest_forwards = guest_forwards + [cache_guest_forward]
        run_args += [
            '-device', 'virtio-net,netdev=net0', '-netdev',
            ','.join(['user', 'id=net0'] + host_forwards +
                     (['restrict=yes'] if self.vm.sealed else []) + [
                         f'guestfwd=tcp:{ip}:{port}-cmd:{cmd}'
                         for ip, port, cmd in guest_forwards
                     ])
        ]

        self.image = os.path.join(self.vm.path, 'image.qcow2')
        if self._image_to_clone:
            # let's try to use /tmp (which is, hopefully, tmpfs) for transients
            # if it looks empty enough
            cloned_to_tmp = False
            required_space = os.path.getsize(self._image_to_clone) + 2 * 2**30
            if self.vm._transient:
                # Would be ideal to have it global (and multiuser-ok)
                tmp_free_lock = path.cache('.tmp-free-space-check-lock')
                with fasteners.process_lock.InterProcessLock(tmp_free_lock):
                    if temp.has_space(required_space, where='/tmp'):
                        self.image = temp.disappearing_file(
                            '/tmp', hint='fingertip-qemu')
                        reflink.auto(self._image_to_clone, self.image)
                        cloned_to_tmp = True
            if not cloned_to_tmp:
                reflink.auto(self._image_to_clone, self.image)
            self._image_to_clone = None
        run_args += [
            '-drive', f'file={self.image},cache=unsafe,if=virtio,discard=unmap'
        ]

        run_args += ['-m', self.ram_size]

        os.makedirs(path.SHARED, exist_ok=True)

        args = QEMU_COMMON_ARGS + self.custom_args + run_args + extra_args
        self.vm.log.debug(' '.join(args))
        if self.vm._backend_mode == 'pexpect':
            pexp = self.vm.log.pseudofile_powered(pexpect.spawn,
                                                  logfile=logging.INFO)
            self.vm.console = pexp(self._qemu,
                                   args,
                                   echo=False,
                                   timeout=None,
                                   encoding='utf-8',
                                   codec_errors='ignore')
            self.live = True
        elif self.vm._backend_mode == 'direct':
            subprocess.run([self._qemu, '-serial', 'mon:stdio'] + args,
                           check=True)
            self.live = False
            self._go_down()
示例#10
0
def install_in_qemu(m, version, mirror=None, specific_mirror=True, fips=False):
    version = int(version) if version != 'rawhide' else 'rawhide'
    releases_development = ('development' if version
                            in (RELEASED + 1, 'rawhide') else 'releases')
    if mirror is None:
        if not specific_mirror:
            mirror = FEDORA_GEOREDIRECTOR  # not consistent, not recommended!
        else:
            mirror = determine_mirror(FEDORA_GEOREDIRECTOR, version,
                                      releases_development)
            m.log.info(f'autoselected mirror {mirror}')
    url = f'{mirror}/{releases_development}/{version}/Everything/x86_64/os'
    upd = f'{mirror}/updates/{version}/Everything/x86_64'
    repos = (f'url --url {url}\n' + f'repo --name fedora --baseurl {url}\n' +
             f'repo --name updates --baseurl {upd}')

    with m:
        m.ram.safeguard = '768M'
        m.expiration.cap('2d')  # non-immutable repositories

        hostname = f'fedora{version}' + ('-fips' if fips else '')
        fqdn = hostname + '.fingertip.local'
        ssh_key_fname = path.fingertip('ssh_key', 'fingertip.pub')
        with open(ssh_key_fname) as f:
            ssh_pubkey = f.read().strip()
        m.expiration.depend_on_a_file(ssh_key_fname)

        ks_fname = path.fingertip(
            'kickstart_templates', f'fedora{version}'
            if version != 'rawhide' else f'fedora{RELEASED+1}')
        with open(ks_fname) as f:
            ks_text = f.read().format(HOSTNAME=fqdn,
                                      SSH_PUBKEY=ssh_pubkey,
                                      PROXY=m.http_cache.internal_url,
                                      MIRROR=mirror,
                                      REPOS=repos)
        m.expiration.depend_on_a_file(ks_fname)

        m.http_cache.mock('http://mock/ks', text=ks_text)
        m.log.info(f'fetching kernel: {url}/isolinux/vmlinuz')
        kernel = os.path.join(m.path, 'kernel')
        m.http_cache.fetch(f'{url}/isolinux/vmlinuz', kernel)
        m.log.info(f'fetching initrd: {url}/isolinux/initrd.img')
        initrd = os.path.join(m.path, 'initrd')
        m.http_cache.fetch(f'{url}/isolinux/initrd.img', initrd)
        if version == 35:
            # work around bz2019579
            f35_fix = os.path.join(m.path, 'resolvconf-f35-fix.img')
            m.http_cache.fetch(F35_FIX_URL, f35_fix)
        append = ('ks=http://mock/ks inst.ks=http://mock/ks '
                  'inst.ksstrict '
                  'console=ttyS0 inst.notmux '
                  'inst.zram=off '
                  f'proxy={m.http_cache.internal_url} '
                  f'inst.proxy={m.http_cache.internal_url} '
                  f'inst.repo={url} ' +
                  (f'inst.updates={F35_FIX_URL} ' if version == 35 else '') +
                  ('fips=1' if fips else ''))
        extra_args = ['-kernel', kernel, '-initrd', initrd, '-append', append]

        with m.ram('3G'):
            m.qemu.run(load=None, extra_args=extra_args)
            i = m.console.expect([
                'Storing configuration files and kickstarts',
                'installation failed', 'installation was stopped',
                'installer will now terminate'
            ])
            assert i == 0, 'Installation failed'
            m.qemu.wait()

        m.qemu.run(load=None)  # cold boot

        def login(username='******', password='******'):
            if username == 'root':
                m.prompt = f'[root@{hostname} ~]# '
            else:
                m.prompt = f'[{username}@{hostname} ~]$ '
            m.console.expect(f'{hostname} login: '******'Password: '******'Fedora installation finished')
        os.unlink(kernel)
        os.unlink(initrd)

        m.hooks.unseal += [
            lambda: m('systemctl restart NetworkManager'),
            lambda: m('nm-online')
        ]

        m.hooks.timesync.append(lambda: m('hwclock -s'))

        m.fedora = version
        m.dist_git_branch = f'f{version}' if version != 'rawhide' else 'master'

        red_hat_based.proxy_dnf(m)

        return m
示例#11
0
def install_in_qemu(m, version, mirror=None, specific_mirror=True, fips=False):
    version = int(version) if version != 'rawhide' else 'rawhide'
    releases_development = ('development'
                            if version in (LATEST, 'rawhide') else 'releases')
    if mirror is None:
        if not specific_mirror:
            mirror = FEDORA_GEOREDIRECTOR  # not consistent, not recommended!
        else:
            mirror = determine_mirror(FEDORA_GEOREDIRECTOR, version,
                                      releases_development)
            m.log.info(f'autoselected mirror {mirror}')
    url = f'{mirror}/{releases_development}/{version}/Everything/x86_64/os'
    upd = f'{mirror}/updates/{version}/Everything/x86_64'
    repos = (f'url --url {url}\n' + f'repo --name fedora --baseurl {url}\n' +
             f'repo --name updates --baseurl {upd}')

    m.expiration.cap('2d')  # non-immutable repositories

    original_ram_size = m.qemu.ram_size

    with m:
        m.qemu.ram_size = '2G'
        hostname = f'fedora{version}' + ('-fips' if fips else '')
        fqdn = hostname + '.fingertip.local'
        ssh_key_fname = path.fingertip('ssh_key', 'fingertip.pub')
        with open(ssh_key_fname) as f:
            ssh_pubkey = f.read().strip()
        m.expiration.depend_on_a_file(ssh_key_fname)

        ks_fname = path.fingertip(
            'kickstart_templates',
            f'fedora{version}' if version != 'rawhide' else f'fedora{LATEST}')
        with open(ks_fname) as f:
            ks_text = f.read().format(HOSTNAME=fqdn,
                                      SSH_PUBKEY=ssh_pubkey,
                                      PROXY=m.http_cache.internal_url,
                                      MIRROR=mirror,
                                      REPOS=repos)
        m.expiration.depend_on_a_file(ks_fname)

        m.http_cache.mock('http://mock/ks', text=ks_text)
        m.log.info(f'fetching kernel: {url}/isolinux/vmlinuz')
        kernel = os.path.join(m.path, 'kernel')
        m.http_cache.fetch(f'{url}/isolinux/vmlinuz', kernel)
        m.log.info(f'fetching initrd: {url}/isolinux/initrd.img')
        initrd = os.path.join(m.path, 'initrd')
        m.http_cache.fetch(f'{url}/isolinux/initrd.img', initrd)
        append = ('ks=http://mock/ks inst.ksstrict console=ttyS0 inst.notmux '
                  f'proxy={m.http_cache.internal_url} ' +
                  f'inst.proxy={m.http_cache.internal_url} ' +
                  f'inst.repo={url} ' + ('fips=1' if fips else ''))
        extra_args = ['-kernel', kernel, '-initrd', initrd, '-append', append]

        m.qemu.run(load=None, extra_args=extra_args)
        i = m.console.expect([
            'Storing configuration files and kickstarts',
            'installation failed', 'installation was stopped',
            'installer will now terminate'
        ])
        assert i == 0, 'Installation failed'
        m.qemu.wait()
        m.qemu.compress_image()
        m.qemu.ram_size = original_ram_size
        m.qemu.run(load=None)  # cold boot
        ROOT_PASSWORD = '******'
        m.prompt = f'[root@{hostname} ~]# '
        m.console.expect(f'{hostname} login: '******'root')
        m.console.expect('Password: '******'Fedora installation finished')

        def disable_proxy():
            return m.apply('ansible',
                           'ini_file',
                           path='/etc/dnf/dnf.conf',
                           section='main',
                           option='proxy',
                           state='absent')

        m.hooks.disable_proxy.append(disable_proxy)

        m.hooks.unseal += [
            lambda: m('systemctl restart NetworkManager'),
            lambda: m('nm-online')
        ]

        m.hooks.timesync.append(lambda: m('hwclock -s'))

        m.fedora = version

        return m