def _ansible(m, *args, check=True, cmd=('ansible', 'fingertip')): env = {**os.environ, 'ANSIBLE_HOST_KEY_CHECKING': 'False'} if hasattr(m, 'ssh'): m.ssh.exec('true') # to ensure correct spin-up connection = 'ssh' # TODO: compare with paramiko prefix = () host = ['fingertip', 'ansible_connection=ssh', 'ansible_user=root', 'ansible_host=localhost', f'ansible_port={m.ssh.port}', f'ansible_ssh_private_key_file={m.ssh.key_file}'] elif m.backend == 'podman-criu': connection = 'podman' prefix = ('sudo', '-H') host = ['fingertip', f'ansible_host={m.container.container_id}'] else: raise NotImplementedError() inventory = temp.disappearing_file(hint='ansible-inventory') with open(inventory, 'w') as f: f.write(' '.join(host)) more_opts = ('-T', '120', '-i', inventory, '-c', connection) cmd = prefix + cmd + more_opts + args m.log.debug(' '.join(cmd)) run = m.log.pipe_powered(subprocess.run, stdout=logging.INFO, stderr=logging.INFO) return run(cmd, env=env, check=check)
def cached_clone(m, url, path_in_m, rev=None, rev_is_enough=True): assert hasattr(m, 'ssh') with m: kwa = {} if not rev_is_enough else {'enough_to_have': rev} with Repo(url, url.replace('/', '::'), **kwa) as repo: tar = temp.disappearing_file() tar_in_m = f'/tmp/{os.path.basename(tar)}' extracted_in_m = f'/tmp/{os.path.basename(tar)}-extracted' log.info(f'packing {url} checkout...') with tarfile.open(tar, 'w') as tf: tf.add(repo.path, arcname=extracted_in_m) log.info(f'uploading {url} checkout...') m.ssh.upload(tar, tar_in_m) log.info(f'performing {url} checkout...') m(f''' set -uex tar xmvf {tar_in_m} -C / mkdir -p {path_in_m} git clone -n {extracted_in_m} {path_in_m} cd {path_in_m} git remote set-url origin {url} git checkout {f'{rev}' if rev else 'origin/HEAD'} rm -rf {extracted_in_m} rm -f {tar_in_m} ''') return m
def build(m, from_=None, preinstall=False): tarbomb = from_ if from_ and from_.endswith('.tar') else None if tarbomb is None: if from_ is None: fingertip_sources = path.FINGERTIP assert os.path.exists(os.path.join(fingertip_sources, '.copr')) tarbomb = temp.disappearing_file() with tarfile.open(tarbomb, 'w') as tf: tf.add(fingertip_sources, arcname='/', filter=lambda ti: ti if '/redhat/' not in ti.name else None) if not hasattr(m, 'fingertip_prepared'): m = m.apply(prepare, preinstall) with m: m.ssh.upload(tarbomb, '/tmp/fingertip.tar') m.expiration.depend_on_a_file(tarbomb) m(r''' set -uex mkdir -p /tmp/fingertip/builddir cd /tmp/fingertip/builddir tar xf /tmp/fingertip.tar ./.copr/build-local.sh dnf -y builddep --setopt=install_weak_deps=False \ /tmp/fingertip/srpms/*.rpm mkdir /tmp/fingertip/rpms rpmbuild -rb /tmp/fingertip/srpms/*.src.rpm \ --define "_rpmdir /tmp/fingertip/rpms" ''') m.fingertip_built = True 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 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 is_supported(dirpath): tmp = temp.disappearing_file(dstdir=dirpath) r = subprocess.Popen(['cp', '--reflink=always', tmp, tmp + '-reflink'], stderr=subprocess.PIPE) _, err = r.communicate() r.wait() temp.remove(tmp, tmp + '-reflink') sure_not = b'failed to clone' in err and b'Operation not supported' in err if r.returncode and not sure_not: log.error('reflink support detection inconclusive, cache dir problems') return r.returncode == 0
def upload_contents(m, url, path_in_m, rev=None, rev_is_enough=True): assert hasattr(m, 'ssh') with m: kwa = {} if not rev_is_enough else {'enough_to_have': rev} with Repo(url, url.replace('/', '::'), **kwa) as repo: tar = temp.disappearing_file() log.info(f'packing {url} contents at rev {rev}...') tar_in_m = f'/.tmp-{os.path.basename(tar)}' with open(tar, 'wb') as tf: repo.archive(tf, treeish=rev, prefix=path_in_m + '/') log.info(f'uploading {url} contents at rev {rev}...') m.ssh.upload(tar, tar_in_m) log.info(f'unpacking {url} contents at rev {rev}...') m(f''' set -uex tar xmf {tar_in_m} -C / rm -f {tar_in_m} ''') return m
def storage_setup_wizard(): assert SETUP in ('auto', 'suggest', 'never') if SETUP == 'never': return size = SIZE os.makedirs(path.MACHINES, exist_ok=True) if not is_supported(path.MACHINES): log.warning(f'images directory {path.MACHINES} lacks reflink support') log.warning('without it, fingertip will thrash and fill up your SSD ' 'in no time') backing_file = os.path.join(path.CACHE, 'for-machines.xfs') if not os.path.exists(backing_file): if SETUP == 'suggest': log.info(f'would you like to allow fingertip ' f'to allocate {size} at {backing_file} ' 'for a reflink-enabled XFS loop mount?') log.info('(set FINGERTIP_SETUP="auto" environment variable' ' to do it automatically)') i = input(f'[{size}]/different size/cancel/ignore> ').strip() if i == 'cancel': log.error('cancelled') sys.exit(1) elif i == 'ignore': return size = i or size tmp = temp.disappearing_file(path.CACHE) create_supported_fs(tmp, size) os.rename(tmp, backing_file) log.info(f'fingertip will now mount the XFS image at {backing_file}') if SETUP == 'suggest': i = input(f'[ok]/skip/cancel> ').strip() if i == 'skip': log.warning('skipping; ' 'fingertip will have no reflink superpowers') log.warning('tell your SSD I\'m sorry') return elif i and i != 'ok': log.error('cancelled') sys.exit(1) mount_supported_fs(backing_file, path.MACHINES)
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') ] self.vm.ssh.port = free_port.find() 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') self.vm.log.info('preloading image to /tmp...') reflink.auto(self._image_to_clone, self.image) self.vm.log.info('preloading image to /tmp completed') cloned_to_tmp = True if not cloned_to_tmp: reflink.auto(self._image_to_clone, self.image) self._image_to_clone = None if self.virtio_scsi: run_args += [ '-device', 'virtio-scsi-pci', '-device', 'scsi-hd,drive=hd', '-drive', f'file={self.image},cache=unsafe,' 'if=none,id=hd,discard=unmap' ] else: run_args += [ '-drive', f'file={self.image},cache=unsafe,' 'if=virtio,discard=unmap' ] run_args += ['-m', str(self.vm.ram.max // 2**20)] 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': # start connecting/negotiating QMP, later starts auto-ballooning threading.Thread(target=self.monitor.connect, daemon=True).start() 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) # FIXME: autoballooning won't start w/o the monitor connection! self.live = False self._go_down()