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
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()
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
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')
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
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
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()
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
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()
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
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