Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
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()
Exemplo n.º 5
0
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
Exemplo n.º 6
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
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')
        ]

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