Ejemplo n.º 1
0
    def run(
        self,
        args: List[str],
        user: str,
        log_output_live: bool,
        env: Dict[str, Any],
        tty: bool,
        ssh_key_path: Path,
        public_ip_address: IPv4Address,
        capture_output: bool,
    ) -> subprocess.CompletedProcess:
        """
        Run a command on this node the given user.

        Args:
            args: The command to run on the node.
            user: The username to communicate as.
            log_output_live: If ``True``, log output live. If ``True``, stderr
                is merged into stdout in the return value.
            env: Environment variables to be set on the node before running
                the command. A mapping of environment variable names to
                values.
            tty: If ``True``, allocate a pseudo-tty. This means that the users
                terminal is attached to the streams of the process.
                This means that the values of stdout and stderr will not be in
                the returned ``subprocess.CompletedProcess``.
            ssh_key_path: The path to an SSH key which can be used to SSH to
                the node as the ``user`` user.
            public_ip_address: The public IP address of the node.
            capture_output: Whether to capture output in the result.

        Returns:
            The representation of the finished process.

        Raises:
            subprocess.CalledProcessError: The process exited with a non-zero
                code.
        """
        ssh_args = _compose_ssh_command(
            args=args,
            user=user,
            env=env,
            tty=tty,
            ssh_key_path=ssh_key_path,
            public_ip_address=public_ip_address,
        )

        return run_subprocess(
            args=ssh_args,
            log_output_live=log_output_live,
            pipe_output=capture_output,
        )
Ejemplo n.º 2
0
    def __init__(  # pylint: disable=super-init-not-called
        self,
        generate_config_path: Optional[Path],
        masters: int,
        agents: int,
        public_agents: int,
        extra_config: Dict[str, Any],
        log_output_live: bool,
        files_to_copy_to_installer: Dict[Path, Path],
        files_to_copy_to_masters: Dict[Path, Path],
        cluster_backend: DCOS_Docker,
    ) -> None:
        """
        Create a DC/OS Docker cluster.

        Args:
            generate_config_path: The path to a build artifact to install.
            masters: The number of master nodes to create.
            agents: The number of agent nodes to create.
            public_agents: The number of public agent nodes to create.
            extra_config: DC/OS Docker comes with a "base" configuration.
                This dictionary can contain extra installation configuration
                variables.
            log_output_live: If `True`, log output of subprocesses live.
                If `True`, stderr is merged into stdout in the return value.
            files_to_copy_to_installer: A mapping of host paths to paths on
                the installer node. These are files to copy from the host to
                the installer node before installing DC/OS. Currently on DC/OS
                Docker the only supported paths on the installer are in the
                `/genconf` directory.
            files_to_copy_to_masters: A mapping of host paths to paths on the
                master nodes. These are files to copy from the host to
                the master nodes before installing DC/OS. On DC/OS Docker the
                files are mounted, read only, to the masters.
            cluster_backend: Details of the specific DC/OS Docker backend to
                use.

        Raises:
            ValueError: There is no file at `generate_config_path`.
            CalledProcessError: The step to create and install containers
                exited with a non-zero code.
        """
        if generate_config_path is None or not generate_config_path.exists():
            raise ValueError()

        self.log_output_live = log_output_live

        # To avoid conflicts, we use random container names.
        # We use the same random string for each container in a cluster so
        # that they can be associated easily.
        #
        # Starting with "dcos-e2e" allows `make clean` to remove these and
        # only these containers.
        unique = 'dcos-e2e-{random}'.format(random=uuid.uuid4())

        # We create a new instance of DC/OS Docker and we work in this
        # directory.
        # This helps running tests in parallel without conflicts and it
        # reduces the chance of side-effects affecting sequential tests.
        self._path = Path(
            TemporaryDirectory(
                suffix=unique,
                dir=(str(cluster_backend.workspace_dir)
                     if cluster_backend.workspace_dir else None),
            ).name)

        copytree(
            src=str(cluster_backend.dcos_docker_path),
            dst=str(self._path),
            # If there is already a config, we do not copy it as it will be
            # overwritten and therefore copying it is wasteful.
            ignore=ignore_patterns('dcos_generate_config.sh'),
        )

        # Files in the DC/OS Docker directory's `genconf` directory are mounted
        # to the installer at `/genconf`.
        # Therefore, every file which we want to copy to `/genconf` on the
        # installer is put into the genconf directory in DC/OS Docker.
        # The way to fix this if we want to be able to put files anywhere is
        # to add an variable to `dcos_generate_config.sh.in` which allows
        # `-v` mounts.
        # Then `INSTALLER_MOUNTS` can be added to DC/OS Docker.
        genconf_dir = self._path / 'genconf'
        # We wrap this in `Path` to work around
        # https://github.com/PyCQA/pylint/issues/224.
        Path(genconf_dir).mkdir(exist_ok=True)
        for host_path, installer_path in files_to_copy_to_installer.items():
            relative_installer_path = installer_path.relative_to('/genconf')
            destination_path = genconf_dir / relative_installer_path
            copyfile(src=str(host_path), dst=str(destination_path))

        extra_genconf_config = ''
        if extra_config:
            extra_genconf_config = yaml.dump(
                data=extra_config,
                default_flow_style=False,
            )

        master_mounts = []
        for host_path, master_path in files_to_copy_to_masters.items():
            mount = '-v {host_path}:{master_path}:ro'.format(
                host_path=host_path.absolute(),
                master_path=master_path,
            )
            master_mounts.append(mount)

        # Only overlay, overlay2, and aufs storage drivers are supported.
        # This chooses the overlay2 driver if the host's driver is not
        # supported for speed reasons.
        client = docker.from_env(version='auto')
        host_driver = client.info()['Driver']
        storage_driver = host_driver if host_driver in ('overlay', 'overlay2',
                                                        'aufs') else 'overlay2'

        self._master_prefix = '{unique}-master-'.format(unique=unique)
        self._agent_prefix = '{unique}-agent-'.format(unique=unique)
        self._public_agent_prefix = '{unique}-pub-agent-'.format(unique=unique)

        variables = {
            # This version of Docker supports `overlay2`.
            'DOCKER_VERSION': '1.13.1',
            'DOCKER_STORAGEDRIVER': storage_driver,
            # Some platforms support systemd and some do not.
            # Disabling support makes all platforms consistent in this aspect.
            'MESOS_SYSTEMD_ENABLE_SUPPORT': 'false',
            # Number of nodes.
            'MASTERS': str(masters),
            'AGENTS': str(agents),
            'PUBLIC_AGENTS': str(public_agents),
            # Container names.
            'MASTER_CTR': self._master_prefix,
            'AGENT_CTR': self._agent_prefix,
            'PUBLIC_AGENT_CTR': self._public_agent_prefix,
            'INSTALLER_CTR': '{unique}-installer'.format(unique=unique),
            'INSTALLER_PORT': str(_get_open_port()),
            'EXTRA_GENCONF_CONFIG': extra_genconf_config,
            'MASTER_MOUNTS': ' '.join(master_mounts),
            'DCOS_GENERATE_CONFIG_PATH': str(generate_config_path),
            # Make sure that there are no home mounts.
            # If $HOME is set to a directory we use, like `/root`, home mounts
            # can cause problems.
            'HOME_MOUNTS': '',
        }  # type: Dict[str, str]

        make_args = []
        for key, value in variables.items():
            # See https://stackoverflow.com/a/7860705 for details on escaping
            # Make variables.
            escaped_value = value.replace('$', '$$')
            escaped_value = escaped_value.replace('#', '\\#')
            set_variable = '{key}={value}'.format(key=key, value=escaped_value)
            make_args.append(set_variable)

        run_subprocess(args=['make'] + make_args + ['install'],
                       cwd=str(self._path),
                       log_output_live=self.log_output_live)
Ejemplo n.º 3
0
    def install_dcos_from_path_with_bootstrap_node(
        self,
        build_artifact: Path,
        dcos_config: Dict[str, Any],
        log_output_live: bool,
    ) -> None:
        """
        Install DC/OS from a given build artifact.

        Args:
            build_artifact: The ``Path`` to a build artifact to install DC/OS
                from.
            dcos_config: The DC/OS configuration to use.
            log_output_live: If ``True``, log output of the installation live.

        Raises:
            CalledProcessError: There was an error installing DC/OS on a node.
        """
        config_yaml = yaml.dump(data=dcos_config)
        config_file_path = self._genconf_dir / 'config.yaml'
        config_file_path.write_text(data=config_yaml)

        genconf_args = [
            'bash',
            str(build_artifact),
            '--offline',
            '-v',
            '--genconf',
        ]

        installer_ctr = '{cluster_id}-installer'.format(
            cluster_id=self._cluster_id, )
        installer_port = _get_open_port()

        run_subprocess(
            args=genconf_args,
            env={
                'PORT': str(installer_port),
                'DCOS_INSTALLER_CONTAINER_NAME': installer_ctr,
            },
            log_output_live=log_output_live,
            cwd=str(self._path),
        )

        for role, nodes in [
            ('master', self.masters),
            ('slave', self.agents),
            ('slave_public', self.public_agents),
        ]:
            dcos_install_args = [
                '/bin/bash',
                str(self._bootstrap_tmp_path / 'dcos_install.sh'),
                '--no-block-dcos-setup',
                role,
            ]

            for node in nodes:
                try:
                    node.run(args=dcos_install_args)
                except subprocess.CalledProcessError as ex:  # pragma: no cover
                    LOGGER.error(ex.stdout)
                    LOGGER.error(ex.stderr)
                    raise
Ejemplo n.º 4
0
    def install_dcos_from_path_with_bootstrap_node(
        self,
        dcos_installer: Path,
        dcos_config: Dict[str, Any],
        ip_detect_path: Path,
        output: Output,
        files_to_copy_to_genconf_dir: Iterable[Tuple[Path, Path]],
    ) -> None:
        """
        Install DC/OS from a given installer.

        Args:
            dcos_installer: The ``Path`` to an installer to install DC/OS
                from.
            dcos_config: The DC/OS configuration to use.
            ip_detect_path: The ``ip-detect`` script that is used for
                installing DC/OS.
            output: What happens with stdout and stderr.
            files_to_copy_to_genconf_dir: Pairs of host paths to paths on
                the installer node. These are files to copy from the host to
                the installer node before installing DC/OS.

        Raises:
            CalledProcessError: There was an error installing DC/OS on a node.
        """
        copyfile(
            src=str(ip_detect_path),
            dst=str(self._genconf_dir / 'ip-detect'),
        )

        config_yaml = yaml.dump(data=dcos_config)
        config_file_path = self._genconf_dir / 'config.yaml'
        config_file_path.write_text(data=config_yaml)

        for host_path, installer_path in files_to_copy_to_genconf_dir:
            relative_installer_path = installer_path.relative_to('/genconf')
            destination_path = self._genconf_dir / relative_installer_path
            if host_path.is_dir():
                destination_path = destination_path / host_path.stem
                copytree(src=str(host_path), dst=str(destination_path))
            else:
                copyfile(src=str(host_path), dst=str(destination_path))

        genconf_args = [
            'bash',
            str(dcos_installer),
            '--offline',
            '-v',
            '--genconf',
        ]

        installer_ctr = '{cluster_id}-installer'.format(
            cluster_id=self._cluster_id,
        )
        installer_port = _get_open_port()

        log_output_live = {
            Output.CAPTURE: False,
            Output.LOG_AND_CAPTURE: True,
            Output.NO_CAPTURE: False,
        }[output]

        capture_output = {
            Output.CAPTURE: True,
            Output.LOG_AND_CAPTURE: True,
            Output.NO_CAPTURE: False,
        }[output]

        run_subprocess(
            args=genconf_args,
            env={
                'PORT': str(installer_port),
                'DCOS_INSTALLER_CONTAINER_NAME': installer_ctr,
            },
            log_output_live=log_output_live,
            cwd=str(self._path),
            pipe_output=capture_output,
        )

        for role, nodes in [
            ('master', self.masters),
            ('slave', self.agents),
            ('slave_public', self.public_agents),
        ]:
            dcos_install_args = [
                '/bin/bash',
                str(self._bootstrap_tmp_path / 'dcos_install.sh'),
                '--no-block-dcos-setup',
                role,
            ]

            for node in nodes:
                try:
                    node.run(args=dcos_install_args)
                except subprocess.CalledProcessError as ex:  # pragma: no cover
                    LOGGER.error(ex.stdout)
                    LOGGER.error(ex.stderr)
                    raise
Ejemplo n.º 5
0
    def install_dcos_from_path(
        self,
        build_artifact: Path,
        extra_config: Dict[str, Any],
        log_output_live: bool,
    ) -> None:
        """
        Args:
            build_artifact: The `Path` to a build artifact to install DC/OS
                from.
            extra_config: May contain extra installation configuration
                variables that are applied on top of the default DC/OS
                configuration of the Docker backend.
            log_output_live: If `True`, log output of the installation live.
        """
        ssh_user = self._default_ssh_user

        def ip_list(nodes: Set[Node]) -> List[str]:
            return list(map(lambda node: str(node.public_ip_address), nodes))

        config = {
            'agent_list': ip_list(nodes=self.agents),
            'bootstrap_url': 'file://' + str(self._bootstrap_tmp_path),
            # Without this, we see errors like:
            # "Time is not synchronized / marked as bad by the kernel.".
            # Adam saw this on Docker for Mac 17.09.0-ce-mac35.
            #
            # In that case this was fixable with:
            #   $ docker run --rm --privileged alpine hwclock -s
            'check_time': 'false',
            'cluster_name': 'DCOS',
            'exhibitor_storage_backend': 'static',
            'master_discovery': 'static',
            'master_list': ip_list(nodes=self.masters),
            'process_timeout': 10000,
            'public_agent_list': ip_list(nodes=self.public_agents),
            'resolvers': ['8.8.8.8'],
            'ssh_port': 22,
            'ssh_user': ssh_user,
        }

        config_data = {**config, **extra_config}
        config_yaml = yaml.dump(data=config_data)  # type: ignore
        config_file_path = self._genconf_dir / 'config.yaml'
        config_file_path.write_text(data=config_yaml)

        genconf_args = [
            'bash',
            str(build_artifact),
            '--offline',
            '-v',
            '--genconf',
        ]

        installer_ctr = '{cluster_id}-installer'.format(
            cluster_id=self._cluster_id)
        installer_port = _get_open_port()

        run_subprocess(
            args=genconf_args,
            env={
                'PORT': str(installer_port),
                'DCOS_INSTALLER_CONTAINER_NAME': installer_ctr,
            },
            log_output_live=log_output_live,
            cwd=str(self._path),
        )

        for role, nodes in [
            ('master', self.masters),
            ('slave', self.agents),
            ('slave_public', self.public_agents),
        ]:
            dcos_install_args = [
                '/bin/bash',
                str(self._bootstrap_tmp_path / 'dcos_install.sh'),
                '--no-block-dcos-setup',
                role,
            ]

            for node in nodes:
                node.run(args=dcos_install_args, user=ssh_user)