Beispiel #1
0
def reset_cleansers(confirm=True):
    """destroys all cleanser slaves and their rollback snapshots, as well as the initial master
    snapshot - this allows re-running the jailhost deployment to recreate fresh cleansers."""

    if value_asbool(confirm) and not yesno("""\nObacht!
            This will destroy any existing and or currently running cleanser jails.
            Are you sure that you want to continue?"""):
        exit("Glad I asked...")

    get_vars()

    cleanser_count = AV['ploy_cleanser_count']
    # make sure no workers interfere:
    fab.run('ezjail-admin stop worker')
    # stop and nuke the cleanser slaves
    for cleanser_index in range(cleanser_count):
        cindex = '{:02d}'.format(cleanser_index + 1)
        fab.run('ezjail-admin stop cleanser_{cindex}'.format(cindex=cindex))
        with fab.warn_only():
            fab.run('zfs destroy tank/jails/cleanser_{cindex}@jdispatch_rollback'.format(cindex=cindex))
            fab.run('ezjail-admin delete -fw cleanser_{cindex}'.format(cindex=cindex))
            fab.run('umount -f /usr/jails/cleanser_{cindex}'.format(cindex=cindex))
            fab.run('rm -rf /usr/jails/cleanser_{cindex}'.format(cindex=cindex))

    with fab.warn_only():
        # remove master snapshot
        fab.run('zfs destroy -R tank/jails/cleanser@clonesource')

        # restart worker and cleanser to prepare for subsequent ansible configuration runs
        fab.run('ezjail-admin start worker')
        fab.run('ezjail-admin stop cleanser')
        fab.run('ezjail-admin start cleanser')
Beispiel #2
0
 def cmd_import(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         return
     cmd = ['gpg', '--quiet', '--no-tty', '--decrypt']
     cmd.extend(self.gpg_opts)
     cmd.extend([args.file])
     key = subprocess.check_output(cmd)
     src.set(key)
Beispiel #3
0
 def cmd_import(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         return
     cmd = ['gpg', '--quiet', '--no-tty', '--decrypt']
     cmd.extend(self.gpg_opts)
     cmd.extend([args.file])
     key = subprocess.check_output(cmd)
     src.set(key)
Beispiel #4
0
 def cmd_generate(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         sys.exit(1)
     key = b2a_base64(os.urandom(32))
     key = key.strip()
     key = key.replace('+', '-')
     key = key.replace('/', '_')
     src.set(key)
Beispiel #5
0
 def cmd_generate(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         sys.exit(1)
     key = b2a_base64(os.urandom(32))
     key = key.strip()
     key = key.replace('+', '-')
     key = key.replace('/', '_')
     src.set(key)
Beispiel #6
0
 def __call__(self, argv, help):
     """Manage vault keys."""
     parser = argparse.ArgumentParser(
         prog="%s vault-key" % self.ctrl.progname,
         description=help)
     group = parser.add_mutually_exclusive_group(required=True)
     group.add_argument(
         '-g', '--generate',
         action="store_true",
         help="generate a new random 32 byte vault key and store it")
     group.add_argument(
         '-s', '--set',
         action="store_true",
         help="set the vault key")
     group.add_argument(
         '-d', '--delete',
         action="store_true",
         help="delete the vault key")
     parser.add_argument(
         '-o', '--old',
         action="store_true",
         help="use 'vault-password-old-source'")
     args = parser.parse_args(argv)
     if args.old:
         src = get_vault_password_source(self.ctrl.config, option='vault-password-old-source')
     else:
         src = get_vault_password_source(self.ctrl.config)
     if args.generate:
         if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
             sys.exit(1)
         from binascii import b2a_base64
         key = b2a_base64(os.urandom(32))
         key = key.strip()
         key = key.replace('+', '-')
         key = key.replace('/', '_')
         src.set(key)
     elif args.set:
         if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
             sys.exit(1)
         import getpass
         src.set(getpass.getpass("Password for '%s': " % src.id))
     elif args.delete:
         if yesno("Do you really want to delete the key for '%s'?" % src.id):
             src.delete()
Beispiel #7
0
def test_yesno(default, all, question, answer, expected):
    from ploy.common import yesno
    raw_input_values = answer

    def get_input_result(q):
        assert q == question
        a = raw_input_values.pop()
        print(q, repr(a))
        return a

    with patch('ploy.common.get_input') as RawInput:
        RawInput.side_effect = get_input_result
        try:
            assert yesno('Foo', default, all) == expected
        except Exception as e:
            assert type(e) == expected
Beispiel #8
0
def test_yesno(default, all, question, answer, expected):
    from ploy.common import yesno
    raw_input_values = answer

    def get_input_result(q):
        assert q == question
        a = raw_input_values.pop()
        print(q, repr(a))
        return a

    with patch('ploy.common.get_input') as RawInput:
        RawInput.side_effect = get_input_result
        try:
            assert yesno('Foo', default, all) == expected
        except Exception as e:
            assert type(e) == expected
Beispiel #9
0
 def ensure(self, instance):
     dhcpservers = instance.vb.list('dhcpservers')
     name = "HostInterfaceNetworking-%s" % self.name
     kw = {}
     for key in ('ip', 'netmask', 'lowerip', 'upperip'):
         if key not in self.config:
             log.error("The '%s' option is required for dhcpserver '%s'." % (key, self.name))
             sys.exit(1)
         kw[key] = self.config[key]
     if name not in dhcpservers:
         try:
             instance.vb.dhcpserver('add', '--enable', netname=name, **kw)
         except subprocess.CalledProcessError as e:
             log.error("Failed to add dhcpserver '%s':\n%s" % (self.name, e))
             sys.exit(1)
         log.info("Added dhcpserver '%s'." % self.name)
     dhcpserver = instance.vb.list('dhcpservers')[name]
     matches = True
     if 'ip' in self.config:
         if dhcpserver['IP'] != self.config['ip']:
             log.error("The host only interface '%s' has an IP '%s' that doesn't match the config '%s'." % (
                 self.name, dhcpserver['IP'], self.config['ip']))
             matches = False
     if 'netmask' in self.config:
         if dhcpserver['NetworkMask'] != self.config['netmask']:
             log.error("The host only interface '%s' has an netmask '%s' that doesn't match the config '%s'." % (
                 self.name, dhcpserver['NetworkMask'], self.config['netmask']))
             matches = False
     if 'lower-ip' in self.config:
         if dhcpserver['lowerIPAddress'] != self.config['lower-ip']:
             log.error("The host only interface '%s' has a lower IP '%s' that doesn't match the config '%s'." % (
                 self.name, dhcpserver['lowerIPAddress'], self.config['lower-ip']))
             matches = False
     if 'upper-ip' in self.config:
         if dhcpserver['upperIPAddress'] != self.config['upper-ip']:
             log.error("The host only interface '%s' has a upper IP '%s' that doesn't match the config '%s'." % (
                 self.name, dhcpserver['upperIPAddress'], self.config['upper-ip']))
             matches = False
     if not matches:
         if not yesno("Should the dhcpserver '%s' be modified to match the config?" % self.name):
             sys.exit(1)
         try:
             instance.vb.dhcpserver('modify', '--enable', netname=name, **kw)
         except subprocess.CalledProcessError as e:
             log.error("Failed to modify dhcpserver '%s':\n%s" % (self.name, e))
             sys.exit(1)
Beispiel #10
0
    def download_remote(self, url, sha_checksum=None):
        def check(path, sha):
            d = hashlib.sha1()
            with open(path, 'rb') as f:
                while 1:
                    buf = f.read(1024 * 1024)
                    if not len(buf):
                        break
                    d.update(buf)
            return d.hexdigest() == sha

        download_dir = os.path.expanduser(
            self.master.main_config.get('global',
                                        dict()).get('download_dir',
                                                    '~/.ploy/downloads'))

        if not os.path.exists(download_dir):
            os.makedirs(download_dir, mode=0o750)

        path, filename = os.path.split(url.path)
        local_path = os.path.join(download_dir, filename)

        if sha_checksum is None:
            if not yesno(
                    'No checksum provided! Are you sure you want to boot from an unverified image?'
            ):
                sys.exit(1)

        if os.path.exists(local_path):
            if sha_checksum is None or check(local_path, sha_checksum):
                return local_path
            else:
                log.error('Checksum mismatch for %s!' % local_path)
                sys.exit(1)

        log.info("Downloading remote disk image from %s to %s" %
                 (url.geturl(), local_path))
        urllib.urlretrieve(url.geturl(), local_path)
        log.info('Downloaded successfully to %s' % local_path)
        if sha_checksum is not None and not check(local_path, sha_checksum):
            log.error('Checksum mismatch!')
            sys.exit(1)

        return local_path
Beispiel #11
0
 def cmd_terminate(self, argv, help):
     """Terminates the instance"""
     from ploy.common import yesno
     parser = argparse.ArgumentParser(
         prog="%s terminate" % self.progname,
         description=help,
     )
     instances = self.get_instances(command='terminate')
     parser.add_argument("instance", nargs=1,
                         metavar="instance",
                         help="Name of the instance from the config.",
                         choices=sorted(instances))
     args = parser.parse_args(argv)
     instance = instances[args.instance[0]]
     if not yesno("Are you sure you want to terminate '%s'?" % instance.config_id):
         return
     instance.hooks.before_terminate(instance)
     instance.terminate()
     instance.hooks.after_terminate(instance)
Beispiel #12
0
 def cmd_terminate(self, argv, help):
     """Terminates the instance"""
     from ploy.common import yesno
     parser = argparse.ArgumentParser(
         prog="%s terminate" % self.progname,
         description=help,
     )
     instances = self.get_instances(command='terminate')
     parser.add_argument("instance", nargs=1,
                         metavar="instance",
                         help="Name of the instance from the config.",
                         choices=sorted(instances))
     args = parser.parse_args(argv)
     instance = instances[args.instance[0]]
     if not yesno("Are you sure you want to terminate '%s'?" % instance.config_id):
         return
     instance.hooks.before_terminate(instance)
     instance.terminate()
     instance.hooks.after_terminate(instance)
Beispiel #13
0
def reset_jails(confirm=True, keep_cleanser_master=True):
    """ stops, deletes and re-creates all jails.
    since the cleanser master is rather large, that one is omitted by default.
    """
    if value_asbool(confirm) and not yesno("""\nObacht!
            This will destroy all existing and or currently running jails on the host.
            Are you sure that you want to continue?"""):
        exit("Glad I asked...")

    reset_cleansers(confirm=False)

    jails = ['appserver', 'webserver', 'worker']
    if not value_asbool(keep_cleanser_master):
        jails.append('cleanser')

    with fab.warn_only():
        for jail in jails:
            fab.run('ezjail-admin delete -fw {jail}'.format(jail=jail))
        # remove authorized keys for no longer existing key (they are regenerated for each new worker)
        fab.run('rm /usr/jails/cleanser/usr/home/cleanser/.ssh/authorized_keys')
Beispiel #14
0
def reset_jails(confirm=True, keep_cleanser_master=True):
    """ stops, deletes and re-creates all jails.
    since the cleanser master is rather large, that one is omitted by default.
    """
    if value_asbool(confirm) and not yesno("""\nObacht!
            This will destroy all existing and or currently running jails on the host.
            Are you sure that you want to continue?"""):
        exit("Glad I asked...")

    reset_cleansers(confirm=False)

    jails = ['appserver', 'webserver', 'worker']
    if not value_asbool(keep_cleanser_master):
        jails.append('cleanser')

    with fab.warn_only():
        for jail in jails:
            fab.run('ezjail-admin delete -fw {jail}'.format(jail=jail))
        # remove authorized keys for no longer existing key (they are regenerated for each new worker)
        fab.run(
            'rm /usr/jails/cleanser/usr/home/cleanser/.ssh/authorized_keys')
Beispiel #15
0
    def download_remote(self, url, sha_checksum=None):

        def check(path, sha):
            d = hashlib.sha1()
            with open(path, 'rb') as f:
                while 1:
                    buf = f.read(1024 * 1024)
                    if not len(buf):
                        break
                    d.update(buf)
            return d.hexdigest() == sha

        download_dir = os.path.expanduser(self.master.main_config.get('global', dict()).get(
            'download_dir', '~/.ploy/downloads'))

        if not os.path.exists(download_dir):
            os.makedirs(download_dir, mode=0o750)

        path, filename = os.path.split(url.path)
        local_path = os.path.join(download_dir, filename)

        if sha_checksum is None:
            if not yesno('No checksum provided! Are you sure you want to boot from an unverified image?'):
                sys.exit(1)

        if os.path.exists(local_path):
            if sha_checksum is None or check(local_path, sha_checksum):
                return local_path
            else:
                log.error('Checksum mismatch for %s!' % local_path)
                sys.exit(1)

        log.info("Downloading remote disk image from %s to %s" % (url.geturl(), local_path))
        urllib.urlretrieve(url.geturl(), local_path)
        log.info('Downloaded successfully to %s' % local_path)
        if sha_checksum is not None and not check(local_path, sha_checksum):
            log.error('Checksum mismatch!')
            sys.exit(1)

        return local_path
Beispiel #16
0
def reset_cleansers(confirm=True):
    """destroys all cleanser slaves and their rollback snapshots, as well as the initial master
    snapshot - this allows re-running the jailhost deployment to recreate fresh cleansers."""

    if value_asbool(confirm) and not yesno("""\nObacht!
            This will destroy any existing and or currently running cleanser jails.
            Are you sure that you want to continue?"""):
        exit("Glad I asked...")

    get_vars()

    cleanser_count = AV['ploy_cleanser_count']
    # make sure no workers interfere:
    fab.run('ezjail-admin stop worker')
    # stop and nuke the cleanser slaves
    for cleanser_index in range(cleanser_count):
        cindex = '{:02d}'.format(cleanser_index + 1)
        fab.run('ezjail-admin stop cleanser_{cindex}'.format(cindex=cindex))
        with fab.warn_only():
            fab.run(
                'zfs destroy tank/jails/cleanser_{cindex}@jdispatch_rollback'.
                format(cindex=cindex))
            fab.run('ezjail-admin delete -fw cleanser_{cindex}'.format(
                cindex=cindex))
            fab.run(
                'umount -f /usr/jails/cleanser_{cindex}'.format(cindex=cindex))
            fab.run(
                'rm -rf /usr/jails/cleanser_{cindex}'.format(cindex=cindex))

    with fab.warn_only():
        # remove master snapshot
        fab.run('zfs destroy -R tank/jails/cleanser@clonesource')

        # restart worker and cleanser to prepare for subsequent ansible configuration runs
        fab.run('ezjail-admin start worker')
        fab.run('ezjail-admin stop cleanser')
        fab.run('ezjail-admin start cleanser')
Beispiel #17
0
 def cmd_set(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         return
     src.set(getpass.getpass("Password for '%s': " % src.id))
Beispiel #18
0
    def bootstrap_files(self):
        """ we need some files to bootstrap the FreeBSD installation.
        Some...
            - need to be provided by the user (i.e. authorized_keys)
            - others have some (sensible) defaults (i.e. rc.conf)
            - some can be downloaded via URL (i.e.) http://pkg.freebsd.org/freebsd:10:x86:64/latest/Latest/pkg.txz

        For those which can be downloaded we check the downloads directory. if the file exists there
        (and if the checksum matches TODO!) we will upload it to the host. If not, we will fetch the file
        from the given URL from the host.

        For files that cannot be downloaded (authorized_keys, rc.conf etc.) we allow the user to provide their
        own version in a ``bootstrap-files`` folder. The location of this folder can either be explicitly provided
        via the ``bootstrap-files`` key in the host definition of the config file or it defaults to ``deployment/bootstrap-files``.

        User provided files can be rendered as Jinja2 templates, by providing ``use_jinja: True`` in the YAML file.
        They will be rendered with the instance configuration dictionary as context.

        If the file is not found there, we revert to the default
        files that are part of bsdploy. If the file cannot be found there either we either error out or for authorized_keys
        we look in ``~/.ssh/identity.pub``.
        """
        bootstrap_file_yamls = [
            abspath(join(self.default_template_path, self.bootstrap_files_yaml)),
            abspath(join(self.custom_template_path, self.bootstrap_files_yaml)),
        ]
        bootstrap_files = dict()
        if self.upload_authorized_keys:
            bootstrap_files["authorized_keys"] = BootstrapFile(
                self,
                "authorized_keys",
                **{
                    "directory": "/mnt/root/.ssh",
                    "directory_mode": "0600",
                    "remote": "/mnt/root/.ssh/authorized_keys",
                    "fallback": [
                        "~/.ssh/identity.pub",
                        "~/.ssh/id_rsa.pub",
                        "~/.ssh/id_dsa.pub",
                        "~/.ssh/id_ecdsa.pub",
                    ],
                }
            )
        for bootstrap_file_yaml in bootstrap_file_yamls:
            if not exists(bootstrap_file_yaml):
                continue
            with open(bootstrap_file_yaml) as f:
                info = yaml.load(f, Loader=SafeLoader)
            if info is None:
                continue
            for k, v in info.items():
                bootstrap_files[k] = BootstrapFile(self, k, **v)

        for bf in bootstrap_files.values():
            if not exists(bf.local) and bf.raw_fallback:
                if not bf.existing_fallback:
                    print(
                        "Found no public key in %s, you have to create '%s' manually" % (expanduser("~/.ssh"), bf.local)
                    )
                    sys.exit(1)
                print("The '%s' file is missing." % bf.local)
                for path in bf.existing_fallback:
                    yes = env.instance.config.get("bootstrap-yes", False)
                    if yes or yesno("Should we generate it using the key in '%s'?" % path):
                        if not exists(bf.expected_path):
                            os.mkdir(bf.expected_path)
                        with open(bf.local, "wb") as out:
                            with open(path, "rb") as f:
                                out.write(f.read())
                        break
                else:
                    # answered no to all options
                    sys.exit(1)

            if not bf.check():
                print("Cannot find %s" % bf.local)
                sys.exit(1)

        packages_path = join(self.download_path, "packages")
        if exists(packages_path):
            for dirpath, dirnames, filenames in os.walk(packages_path):
                path = dirpath.split(packages_path)[1][1:]
                for filename in filenames:
                    if not filename.endswith(".txz"):
                        continue
                    bootstrap_files[join(path, filename)] = BootstrapFile(
                        self,
                        join(path, filename),
                        **dict(
                            local=join(packages_path, join(path, filename)),
                            remote=join("/mnt/var/cache/pkg/All", filename),
                        )
                    )

        if self.ssh_keys is not None:
            for ssh_key_name, ssh_key_options in list(self.ssh_keys):
                ssh_key = join(self.custom_template_path, ssh_key_name)
                if exists(ssh_key):
                    pub_key_name = "%s.pub" % ssh_key_name
                    pub_key = "%s.pub" % ssh_key
                    if not exists(pub_key):
                        print("Public key '%s' for '%s' missing." % (pub_key, ssh_key))
                        sys.exit(1)
                    bootstrap_files[ssh_key_name] = BootstrapFile(
                        self, ssh_key_name, **dict(local=ssh_key, remote="/mnt/etc/ssh/%s" % ssh_key_name, mode=0600)
                    )
                    bootstrap_files[pub_key_name] = BootstrapFile(
                        self, pub_key_name, **dict(local=pub_key, remote="/mnt/etc/ssh/%s" % pub_key_name, mode=0644)
                    )
        return bootstrap_files
Beispiel #19
0
def bootstrap(**kwargs):
    """ bootstrap an instance booted into mfsbsd (http://mfsbsd.vx.sk)
    """
    env.shell = '/bin/sh -c'

    # default ssh settings for mfsbsd with possible overwrite by bootstrap-fingerprint
    fingerprint = env.instance.config.get(
        'bootstrap-fingerprint',
        '02:2e:b4:dd:c3:8a:b7:7b:ba:b2:4a:f0:ab:13:f4:2d')
    env.instance.config['fingerprint'] = fingerprint
    env.instance.config['password-fallback'] = True
    env.instance.config['password'] = '******'
    # allow overwrites from the commandline
    env.instance.config.update(kwargs)

    bu = BootstrapUtils()
    bu.generate_ssh_keys()
    bu.print_bootstrap_files()
    # gather infos
    if not bu.bsd_url:
        print("Found no FreeBSD system to install, please specify bootstrap-bsd-url and make sure mfsbsd is running")
        return
    # get realmem here, because it may fail and we don't want that to happen
    # in the middle of the bootstrap
    realmem = bu.realmem
    print("\nFound the following disk devices on the system:\n    %s" % ' '.join(bu.sysctl_devices))
    if bu.first_interface:
        print("\nFound the following network interfaces, now is your chance to update your rc.conf accordingly!\n    %s" % ' '.join(bu.phys_interfaces))
    else:
        print("\nWARNING! Found no suitable network interface!")

    template_context = {}
    # first the config, so we don't get something essential overwritten
    template_context.update(env.instance.config)
    template_context.update(
        devices=bu.sysctl_devices,
        interfaces=bu.phys_interfaces,
        hostname=env.instance.id)

    rc_conf = bu.bootstrap_files['rc.conf'].read(template_context)
    if not rc_conf.endswith('\n'):
        print("\nERROR! Your rc.conf doesn't end in a newline:\n==========\n%s<<<<<<<<<<\n" % rc_conf)
        return
    rc_conf_lines = rc_conf.split('\n')

    for interface in [bu.first_interface, env.instance.config.get('ansible-dhcp_host_sshd_interface')]:
        if interface is None:
            continue
        ifconfig = 'ifconfig_%s' % interface
        for line in rc_conf_lines:
            if line.strip().startswith(ifconfig):
                break
        else:
            if not yesno("\nDidn't find an '%s' setting in rc.conf. You sure that you want to continue?" % ifconfig):
                return

    yes = env.instance.config.get('bootstrap-yes', False)
    if not (yes or yesno("\nContinuing will destroy the existing data on the following devices:\n    %s\n\nContinue?" % ' '.join(bu.devices))):
        return

    # install FreeBSD in ZFS root
    devices_args = ' '.join('-d %s' % x for x in bu.devices)
    system_pool_name = env.instance.config.get('bootstrap-system-pool-name', 'system')
    data_pool_name = env.instance.config.get('bootstrap-data-pool-name', 'tank')
    swap_arg = ''
    swap_size = env.instance.config.get('bootstrap-swap-size', '%iM' % (realmem * 2))
    if swap_size:
        swap_arg = '-s %s' % swap_size
    system_pool_arg = ''
    system_pool_size = env.instance.config.get('bootstrap-system-pool-size', '20G')
    if system_pool_size:
        system_pool_arg = '-z %s' % system_pool_size
    run('destroygeom {devices_args} -p {system_pool_name} -p {data_pool_name}'.format(
        devices_args=devices_args,
        system_pool_name=system_pool_name,
        data_pool_name=data_pool_name))
    run('{zfsinstall} {devices_args} -p {system_pool_name} -V 28 -u {bsd_url} {swap_arg} {system_pool_arg}'.format(
        zfsinstall=bu.zfsinstall,
        devices_args=devices_args,
        system_pool_name=system_pool_name,
        bsd_url=bu.bsd_url,
        swap_arg=swap_arg,
        system_pool_arg=system_pool_arg))
    # create partitions for data pool, but only if the system pool doesn't use
    # the whole disk anyway
    if system_pool_arg:
        for device in bu.devices:
            run('gpart add -t freebsd-zfs -l {data_pool_name}_{device} {device}'.format(
                data_pool_name=data_pool_name,
                device=device))
    # mount devfs inside the new system
    if 'devfs on /rw/dev' not in bu.mounts:
        run('mount -t devfs devfs /mnt/dev')
    # setup bare essentials
    run('cp /etc/resolv.conf /mnt/etc/resolv.conf')
    bu.create_bootstrap_directories()
    bu.upload_bootstrap_files(template_context)
    # we need to install python here, because there is no way to install it via
    # ansible playbooks
    bu.install_pkg('/mnt', chroot=True, packages=['python27'])
    # set autoboot delay
    autoboot_delay = env.instance.config.get('bootstrap-autoboot-delay', '-1')
    run('echo autoboot_delay=%s >> /mnt/boot/loader.conf' % autoboot_delay)
    bu.generate_remote_ssh_keys()
    # reboot
    if value_asbool(env.instance.config.get('bootstrap-reboot', 'true')):
        with settings(hide('warnings'), warn_only=True):
            run('reboot')
Beispiel #20
0
    def bootstrap_files(self):
        """ we need some files to bootstrap the FreeBSD installation.
        Some...
            - need to be provided by the user (i.e. authorized_keys)
            - others have some (sensible) defaults (i.e. rc.conf)
            - some can be downloaded via URL (i.e.) http://pkg.freebsd.org/freebsd:10:x86:64/latest/Latest/pkg.txz

        For those which can be downloaded we check the downloads directory. if the file exists there
        (and if the checksum matches TODO!) we will upload it to the host. If not, we will fetch the file
        from the given URL from the host.

        For files that cannot be downloaded (authorized_keys, rc.conf etc.) we allow the user to provide their
        own version in a ``bootstrap-files`` folder. The location of this folder can either be explicitly provided
        via the ``bootstrap-files`` key in the host definition of the config file or it defaults to ``deployment/bootstrap-files``.

        User provided files can be rendered as Jinja2 templates, by providing ``use_jinja: True`` in the YAML file.
        They will be rendered with the instance configuration dictionary as context.

        If the file is not found there, we revert to the default
        files that are part of bsdploy. If the file cannot be found there either we either error out or for authorized_keys
        we look in ``~/.ssh/identity.pub``.
        """
        bootstrap_file_yamls = [
            abspath(join(self.default_template_path,
                         self.bootstrap_files_yaml)),
            abspath(join(self.custom_template_path, self.bootstrap_files_yaml))
        ]
        bootstrap_files = dict()
        if self.upload_authorized_keys:
            bootstrap_files['authorized_keys'] = BootstrapFile(
                self, 'authorized_keys', **{
                    'directory':
                    '/mnt/root/.ssh',
                    'directory_mode':
                    '0600',
                    'remote':
                    '/mnt/root/.ssh/authorized_keys',
                    'fallback': [
                        '~/.ssh/identity.pub', '~/.ssh/id_rsa.pub',
                        '~/.ssh/id_dsa.pub', '~/.ssh/id_ecdsa.pub'
                    ]
                })
        for bootstrap_file_yaml in bootstrap_file_yamls:
            if not exists(bootstrap_file_yaml):
                continue
            with open(bootstrap_file_yaml) as f:
                info = yaml.load(f, Loader=SafeLoader)
            if info is None:
                continue
            for k, v in info.items():
                bootstrap_files[k] = BootstrapFile(self, k, **v)

        for bf in bootstrap_files.values():
            if not exists(bf.local) and bf.raw_fallback:
                if not bf.existing_fallback:
                    print(
                        "Found no public key in %s, you have to create '%s' manually"
                        % (expanduser('~/.ssh'), bf.local))
                    sys.exit(1)
                print("The '%s' file is missing." % bf.local)
                for path in bf.existing_fallback:
                    yes = env.instance.config.get('bootstrap-yes', False)
                    if yes or yesno(
                            "Should we generate it using the key in '%s'?" %
                            path):
                        if not exists(bf.expected_path):
                            os.mkdir(bf.expected_path)
                        with open(bf.local, 'wb') as out:
                            with open(path, 'rb') as f:
                                out.write(f.read())
                        break
                else:
                    # answered no to all options
                    sys.exit(1)

            if not bf.check():
                print('Cannot find %s' % bf.local)
                sys.exit(1)

        packages_path = join(self.download_path, 'packages')
        if exists(packages_path):
            for dirpath, dirnames, filenames in os.walk(packages_path):
                path = dirpath.split(packages_path)[1][1:]
                for filename in filenames:
                    if not filename.endswith('.txz'):
                        continue
                    bootstrap_files[join(path, filename)] = BootstrapFile(
                        self, join(path, filename),
                        **dict(local=join(packages_path, join(path, filename)),
                               remote=join('/mnt/var/cache/pkg/All', filename),
                               encrypted=False))

        if self.ssh_keys is not None:
            for ssh_key_name, ssh_key_options in list(self.ssh_keys):
                ssh_key = join(self.custom_template_path, ssh_key_name)
                if exists(ssh_key):
                    pub_key_name = '%s.pub' % ssh_key_name
                    pub_key = '%s.pub' % ssh_key
                    if not exists(pub_key):
                        print("Public key '%s' for '%s' missing." %
                              (pub_key, ssh_key))
                        sys.exit(1)
                    bootstrap_files[ssh_key_name] = BootstrapFile(
                        self, ssh_key_name,
                        **dict(local=ssh_key,
                               remote='/mnt/etc/ssh/%s' % ssh_key_name,
                               mode=0600))
                    bootstrap_files[pub_key_name] = BootstrapFile(
                        self, pub_key_name,
                        **dict(local=pub_key,
                               remote='/mnt/etc/ssh/%s' % pub_key_name,
                               mode=0644))
        if hasattr(env.instance, 'get_vault_lib'):
            vaultlib = env.instance.get_vault_lib()
            for bf in bootstrap_files.values():
                if bf.encrypted is None and exists(bf.local):
                    with open(bf.local) as f:
                        data = f.read()
                    bf.info['encrypted'] = vaultlib.is_encrypted(data)
        return bootstrap_files
Beispiel #21
0
def _bootstrap():
    bu = BootstrapUtils()
    bu.generate_ssh_keys()
    bu.print_bootstrap_files()
    # gather infos
    if not bu.bsd_url:
        print("Found no FreeBSD system to install, please use 'special edition' or specify bootstrap-bsd-url and make sure mfsbsd is running")
        return
    # get realmem here, because it may fail and we don't want that to happen
    # in the middle of the bootstrap
    realmem = bu.realmem
    print("\nFound the following disk devices on the system:\n    %s" % ' '.join(bu.sysctl_devices))
    if bu.first_interface:
        print("\nFound the following network interfaces, now is your chance to update your rc.conf accordingly!\n    %s" % ' '.join(bu.phys_interfaces))
    else:
        print("\nWARNING! Found no suitable network interface!")

    template_context = {"ploy_jail_host_pkg_repository": "pkg+http://pkg.freeBSD.org/${ABI}/quarterly"}
    # first the config, so we don't get something essential overwritten
    template_context.update(env.instance.config)
    template_context.update(
        devices=bu.sysctl_devices,
        interfaces=bu.phys_interfaces,
        hostname=env.instance.id)

    rc_conf = bu.bootstrap_files['rc.conf'].read(template_context)
    if not rc_conf.endswith('\n'):
        print("\nERROR! Your rc.conf doesn't end in a newline:\n==========\n%s<<<<<<<<<<\n" % rc_conf)
        return
    rc_conf_lines = rc_conf.split('\n')

    for interface in [bu.first_interface, env.instance.config.get('ansible-dhcp_host_sshd_interface')]:
        if interface is None:
            continue
        ifconfig = 'ifconfig_%s' % interface
        for line in rc_conf_lines:
            if line.strip().startswith(ifconfig):
                break
        else:
            if not yesno("\nDidn't find an '%s' setting in rc.conf. You sure that you want to continue?" % ifconfig):
                return
    yes = env.instance.config.get('bootstrap-yes', False)
    if not (yes or yesno("\nContinuing will destroy the existing data on the following devices:\n    %s\n\nContinue?" % ' '.join(bu.devices))):
        return

    # install FreeBSD in ZFS root
    devices_args = ' '.join('-d %s' % x for x in bu.devices)
    system_pool_name = env.instance.config.get('bootstrap-system-pool-name', 'system')
    data_pool_name = env.instance.config.get('bootstrap-data-pool-name', 'tank')
    swap_arg = ''
    swap_size = env.instance.config.get('bootstrap-swap-size', '%iM' % (realmem * 2))
    if swap_size:
        swap_arg = '-s %s' % swap_size
    system_pool_arg = ''
    system_pool_size = env.instance.config.get('bootstrap-system-pool-size', '20G')
    if system_pool_size:
        system_pool_arg = '-z %s' % system_pool_size
    run('destroygeom {devices_args} -p {system_pool_name} -p {data_pool_name}'.format(
        devices_args=devices_args,
        system_pool_name=system_pool_name,
        data_pool_name=data_pool_name))
    run('{env_vars}{zfsinstall} {devices_args} -p {system_pool_name} -V 28 -u {bsd_url} {swap_arg} {system_pool_arg}'.format(
        env_vars=bu.env_vars,
        zfsinstall=bu.zfsinstall,
        devices_args=devices_args,
        system_pool_name=system_pool_name,
        bsd_url=bu.bsd_url,
        swap_arg=swap_arg,
        system_pool_arg=system_pool_arg), shell=False)
    # create partitions for data pool, but only if the system pool doesn't use
    # the whole disk anyway
    if system_pool_arg:
        for device in bu.devices:
            run('gpart add -t freebsd-zfs -l {data_pool_name}_{device} {device}'.format(
                data_pool_name=data_pool_name,
                device=device))
    # mount devfs inside the new system
    if 'devfs on /rw/dev' not in bu.mounts:
        run('mount -t devfs devfs /mnt/dev')
    # setup bare essentials
    run('cp /etc/resolv.conf /mnt/etc/resolv.conf', warn_only=True)
    bu.create_bootstrap_directories()
    bu.upload_bootstrap_files(template_context)

    bootstrap_packages = ['python27']
    if value_asbool(env.instance.config.get('firstboot-update', 'false')):
        bootstrap_packages.append('firstboot-freebsd-update')
        run('''touch /mnt/firstboot''')
        run('''sysrc -f /mnt/etc/rc.conf firstboot_freebsd_update_enable=YES''')

    # we need to install python here, because there is no way to install it via
    # ansible playbooks
    bu.install_pkg('/mnt', chroot=True, packages=bootstrap_packages)
    # set autoboot delay
    autoboot_delay = env.instance.config.get('bootstrap-autoboot-delay', '-1')
    run('echo autoboot_delay=%s >> /mnt/boot/loader.conf' % autoboot_delay)
    bu.generate_remote_ssh_keys()
    # reboot
    if value_asbool(env.instance.config.get('bootstrap-reboot', 'true')):
        with settings(hide('warnings'), warn_only=True):
            run('reboot')
Beispiel #22
0
 def ensure(self, instance):
     dhcpservers = instance.vb.list('dhcpservers')
     name = "HostInterfaceNetworking-%s" % self.name
     kw = {}
     for key in ('ip', 'netmask', 'lowerip', 'upperip'):
         if key not in self.config:
             log.error("The '%s' option is required for dhcpserver '%s'." %
                       (key, self.name))
             sys.exit(1)
         kw[key] = self.config[key]
     if name not in dhcpservers:
         try:
             instance.vb.dhcpserver('add', '--enable', netname=name, **kw)
         except subprocess.CalledProcessError as e:
             log.error("Failed to add dhcpserver '%s':\n%s" %
                       (self.name, e))
             sys.exit(1)
         log.info("Added dhcpserver '%s'." % self.name)
     dhcpserver = instance.vb.list('dhcpservers')[name]
     matches = True
     if 'ip' in self.config:
         if dhcpserver['IP'] != self.config['ip']:
             log.error(
                 "The host only interface '%s' has an IP '%s' that doesn't match the config '%s'."
                 % (self.name, dhcpserver['IP'], self.config['ip']))
             matches = False
     if 'netmask' in self.config:
         if dhcpserver['NetworkMask'] != self.config['netmask']:
             log.error(
                 "The host only interface '%s' has an netmask '%s' that doesn't match the config '%s'."
                 % (self.name, dhcpserver['NetworkMask'],
                    self.config['netmask']))
             matches = False
     if 'lower-ip' in self.config:
         if dhcpserver['lowerIPAddress'] != self.config['lower-ip']:
             log.error(
                 "The host only interface '%s' has a lower IP '%s' that doesn't match the config '%s'."
                 % (self.name, dhcpserver['lowerIPAddress'],
                    self.config['lower-ip']))
             matches = False
     if 'upper-ip' in self.config:
         if dhcpserver['upperIPAddress'] != self.config['upper-ip']:
             log.error(
                 "The host only interface '%s' has a upper IP '%s' that doesn't match the config '%s'."
                 % (self.name, dhcpserver['upperIPAddress'],
                    self.config['upper-ip']))
             matches = False
     if not matches:
         if not yesno(
                 "Should the dhcpserver '%s' be modified to match the config?"
                 % self.name):
             sys.exit(1)
         try:
             instance.vb.dhcpserver('modify',
                                    '--enable',
                                    netname=name,
                                    **kw)
         except subprocess.CalledProcessError as e:
             log.error("Failed to modify dhcpserver '%s':\n%s" %
                       (self.name, e))
             sys.exit(1)
Beispiel #23
0
def get_playbook(self, *args, **kwargs):
    inject_ansible_paths()
    import ansible.playbook
    import ansible.callbacks
    import ansible.errors
    import ansible.utils
    try:
        from ansible.utils.vault import VaultLib
    except ImportError:
        VaultLib = None
    from ploy_ansible.inventory import Inventory

    host = self.uid
    user = self.config.get('user', 'root')
    sudo = self.config.get('sudo')
    playbooks_directory = get_playbooks_directory(self.master.main_config)

    class PlayBook(ansible.playbook.PlayBook):
        def __init__(self, *args, **kwargs):
            self.roles = kwargs.pop('roles', None)
            if self.roles is not None:
                if isinstance(self.roles, basestring):
                    self.roles = self.roles.split()
                kwargs['playbook'] = '<dynamically generated from %s>' % self.roles
            ansible.playbook.PlayBook.__init__(self, *args, **kwargs)
            self.basedir = playbooks_directory

        def _load_playbook_from_file(self, *args, **kwargs):
            if self.roles is None:
                return ansible.playbook.PlayBook._load_playbook_from_file(
                    self, *args, **kwargs)
            settings = {
                'hosts': [host],
                'user': user,
                'roles': self.roles}
            if sudo is not None:
                settings['sudo'] = sudo
            return (
                [settings],
                [playbooks_directory])

    patch_connect(self.master.ctrl)
    playbook = kwargs.pop('playbook', None)
    if playbook is None:
        for instance_id in (self.uid, self.id):
            playbook_path = os.path.join(playbooks_directory, '%s.yml' % instance_id)
            if os.path.exists(playbook_path):
                playbook = playbook_path
                break
        if 'playbook' in self.config:
            if playbook is not None and playbook != self.config['playbook']:
                log.warning("Instance '%s' has the 'playbook' option set, but there is also a playbook at the default location '%s', which differs from '%s'." % (self.config_id, playbook, self.config['playbook']))
            playbook = self.config['playbook']
    if playbook is not None:
        log.info("Using playbook at '%s'." % playbook)
    roles = kwargs.pop('roles', None)
    if roles is None and 'roles' in self.config:
        roles = self.config['roles']
    if roles is not None and playbook is not None:
        log.error("You can't use a playbook and the 'roles' options at the same time for instance '%s'." % self.config_id)
        sys.exit(1)
    stats = ansible.callbacks.AggregateStats()
    callbacks = ansible.callbacks.PlaybookCallbacks(verbose=ansible.utils.VERBOSITY)
    runner_callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats, verbose=ansible.utils.VERBOSITY)
    skip_host_check = kwargs.pop('skip_host_check', False)
    if roles is None:
        kwargs['playbook'] = playbook
    else:
        kwargs['roles'] = roles
    if VaultLib is not None:
        kwargs['vault_password'] = get_vault_password_source(self.master.main_config).get()
    inventory = Inventory(self.master.ctrl, vault_password=kwargs.get('vault_password'))
    try:
        pb = PlayBook(
            *args,
            callbacks=callbacks,
            inventory=inventory,
            runner_callbacks=runner_callbacks,
            stats=stats,
            **kwargs)
    except ansible.errors.AnsibleError as e:
        log.error("AnsibleError: %s" % e)
        sys.exit(1)
    for (play_ds, play_basedir) in zip(pb.playbook, pb.play_basedirs):
        if 'user' not in play_ds:
            play_ds['user'] = self.config.get('user', 'root')
        if not skip_host_check:
            hosts = play_ds.get('hosts', '')
            if isinstance(hosts, basestring):
                hosts = hosts.split(':')
            if self.uid not in hosts:
                log.warning("The host '%s' is not in the list of hosts (%s) of '%s'.", self.uid, ','.join(hosts), playbook)
                if not yesno("Do you really want to apply '%s' to the host '%s'?" % (playbook, self.uid)):
                    sys.exit(1)
        play_ds['hosts'] = [self.uid]
    return pb
Beispiel #24
0
 def cmd_set(self, args, src):
     if src.get(fail_on_error=False) and not yesno("There is already a key stored, do you want to replace it?"):
         return
     src.set(getpass.getpass("Password for '%s': " % src.id))
Beispiel #25
0
 def cmd_delete(self, args, src):
     if yesno("Do you really want to delete the key for '%s'?" % src.id):
         src.delete()
Beispiel #26
0
def get_playbook(self, *args, **kwargs):
    inject_ansible_paths(self.master.ctrl)
    from ansible.playbook import Play, Playbook
    import ansible.errors

    (options, loader, inventory, variable_manager) = self.get_ansible_variablemanager(**kwargs)
    playbooks_directory = get_playbooks_directory(self.master.main_config)
    playbook = kwargs.pop('playbook', None)
    if playbook is None:
        for instance_id in (self.uid, self.id):
            playbook_path = os.path.join(playbooks_directory, '%s.yml' % instance_id)
            if os.path.exists(playbook_path):
                playbook = playbook_path
                break
        if 'playbook' in self.config:
            if playbook is not None and playbook != self.config['playbook']:
                log.warning("Instance '%s' has the 'playbook' option set, but there is also a playbook at the default location '%s', which differs from '%s'." % (self.config_id, playbook, self.config['playbook']))
            playbook = self.config['playbook']
    if playbook is not None:
        log.info("Using playbook at '%s'." % playbook)
    roles = kwargs.pop('roles', None)
    if roles is None and 'roles' in self.config:
        roles = self.config['roles']
    if roles is not None and playbook is not None:
        log.error("You can't use a playbook and the 'roles' options at the same time for instance '%s'." % self.config_id)
        sys.exit(1)
    if playbook is None and roles is None:
        return None
    skip_host_check = kwargs.pop('skip_host_check', False)
    try:
        if roles is None:
            pb = Playbook.load(playbook, variable_manager=variable_manager, loader=loader)
            plays = pb.get_plays()
        else:
            if isinstance(roles, basestring):
                roles = roles.split()
            data = {
                'hosts': [self.uid],
                'roles': roles}
            plays = [Play.load(data, variable_manager=variable_manager, loader=loader)]
            pb = Playbook(loader=loader)
            pb._entries.extend(plays)
    except ansible.errors.AnsibleError as e:
        log.error("AnsibleError: %s" % e)
        sys.exit(1)
    for play in plays:
        if play._attributes.get('remote_user') is None:
            play._attributes['remote_user'] = self.config.get('user', 'root')
        if self.config.get('sudo'):
            play._attributes['sudo'] = self.config.get('sudo')
        if not skip_host_check:
            hosts = play._attributes.get('hosts', None)
            if isinstance(hosts, basestring):
                hosts = hosts.split(':')
            if hosts is None:
                hosts = {}
            if self.uid not in hosts:
                log.warning("The host '%s' is not in the list of hosts (%s) of '%s'.", self.uid, ','.join(hosts), playbook)
                if not yesno("Do you really want to apply '%s' to the host '%s'?" % (playbook, self.uid)):
                    sys.exit(1)
        play._attributes['hosts'] = [self.uid]
    return pb
Beispiel #27
0
def _bootstrap():
    bu = BootstrapUtils()
    bu.generate_ssh_keys()
    bu.print_bootstrap_files()
    # gather infos
    if not bu.bsd_url:
        print(
            "Found no FreeBSD system to install, please specify bootstrap-bsd-url and make sure mfsbsd is running"
        )
        return
    # get realmem here, because it may fail and we don't want that to happen
    # in the middle of the bootstrap
    realmem = bu.realmem
    print("\nFound the following disk devices on the system:\n    %s" %
          ' '.join(bu.sysctl_devices))
    if bu.first_interface:
        print(
            "\nFound the following network interfaces, now is your chance to update your rc.conf accordingly!\n    %s"
            % ' '.join(bu.phys_interfaces))
    else:
        print("\nWARNING! Found no suitable network interface!")

    template_context = {
        "ploy_jail_host_pkg_repository":
        "pkg+http://pkg.freeBSD.org/${ABI}/quarterly"
    }
    # first the config, so we don't get something essential overwritten
    template_context.update(env.instance.config)
    template_context.update(devices=bu.sysctl_devices,
                            interfaces=bu.phys_interfaces,
                            hostname=env.instance.id)

    rc_conf = bu.bootstrap_files['rc.conf'].read(template_context)
    if not rc_conf.endswith('\n'):
        print(
            "\nERROR! Your rc.conf doesn't end in a newline:\n==========\n%s<<<<<<<<<<\n"
            % rc_conf)
        return
    rc_conf_lines = rc_conf.split('\n')

    for interface in [
            bu.first_interface,
            env.instance.config.get('ansible-dhcp_host_sshd_interface')
    ]:
        if interface is None:
            continue
        ifconfig = 'ifconfig_%s' % interface
        for line in rc_conf_lines:
            if line.strip().startswith(ifconfig):
                break
        else:
            if not yesno(
                    "\nDidn't find an '%s' setting in rc.conf. You sure that you want to continue?"
                    % ifconfig):
                return
    yes = env.instance.config.get('bootstrap-yes', False)
    if not (yes or yesno(
            "\nContinuing will destroy the existing data on the following devices:\n    %s\n\nContinue?"
            % ' '.join(bu.devices))):
        return

    # install FreeBSD in ZFS root
    devices_args = ' '.join('-d %s' % x for x in bu.devices)
    system_pool_name = env.instance.config.get('bootstrap-system-pool-name',
                                               'system')
    data_pool_name = env.instance.config.get('bootstrap-data-pool-name',
                                             'tank')
    swap_arg = ''
    swap_size = env.instance.config.get('bootstrap-swap-size',
                                        '%iM' % (realmem * 2))
    if swap_size:
        swap_arg = '-s %s' % swap_size
    system_pool_arg = ''
    system_pool_size = env.instance.config.get('bootstrap-system-pool-size',
                                               '20G')
    if system_pool_size:
        system_pool_arg = '-z %s' % system_pool_size
    run('destroygeom {devices_args} -p {system_pool_name} -p {data_pool_name}'.
        format(devices_args=devices_args,
               system_pool_name=system_pool_name,
               data_pool_name=data_pool_name))
    run('{env_vars}{zfsinstall} {devices_args} -p {system_pool_name} -V 28 -u {bsd_url} {swap_arg} {system_pool_arg}'
        .format(env_vars=bu.env_vars,
                zfsinstall=bu.zfsinstall,
                devices_args=devices_args,
                system_pool_name=system_pool_name,
                bsd_url=bu.bsd_url,
                swap_arg=swap_arg,
                system_pool_arg=system_pool_arg),
        shell=False)
    # create partitions for data pool, but only if the system pool doesn't use
    # the whole disk anyway
    if system_pool_arg:
        for device in bu.devices:
            run('gpart add -t freebsd-zfs -l {data_pool_name}_{device} {device}'
                .format(data_pool_name=data_pool_name, device=device))
    # mount devfs inside the new system
    if 'devfs on /rw/dev' not in bu.mounts:
        run('mount -t devfs devfs /mnt/dev')
    # setup bare essentials
    run('cp /etc/resolv.conf /mnt/etc/resolv.conf', warn_only=True)
    bu.create_bootstrap_directories()
    bu.upload_bootstrap_files(template_context)

    bootstrap_packages = ['python27']
    if value_asbool(env.instance.config.get('firstboot-update', 'false')):
        bootstrap_packages.append('firstboot-freebsd-update')
        run('''touch /mnt/firstboot''')
        run('''sysrc -f /mnt/etc/rc.conf firstboot_freebsd_update_enable=YES'''
            )

    # we need to install python here, because there is no way to install it via
    # ansible playbooks
    bu.install_pkg('/mnt', chroot=True, packages=bootstrap_packages)
    # set autoboot delay
    autoboot_delay = env.instance.config.get('bootstrap-autoboot-delay', '-1')
    run('echo autoboot_delay=%s >> /mnt/boot/loader.conf' % autoboot_delay)
    bu.generate_remote_ssh_keys()
    # reboot
    if value_asbool(env.instance.config.get('bootstrap-reboot', 'true')):
        with settings(hide('warnings'), warn_only=True):
            run('reboot')
Beispiel #28
0
 def cmd_delete(self, args, src):
     if yesno("Do you really want to delete the key for '%s'?" % src.id):
         src.delete()