def clean() -> None: """ Remove containers, volumes and networks created by this tool. """ client = docker_client() filters = { 'label': [ '{key}={value}'.format( key=NODE_TYPE_LABEL_KEY, value=NODE_TYPE_LOOPBACK_SIDECAR_LABEL_VALUE, ), ], } loopback_sidecars = client.containers.list(filters=filters) for loopback_sidecar in loopback_sidecars: DockerLoopbackVolume.destroy(container=loopback_sidecar) node_filters = {'name': Docker().container_name_prefix} network_filters = {'name': Docker().container_name_prefix} node_containers = client.containers.list(filters=node_filters, all=True) for container in node_containers: container.stop() container.remove(v=True) networks = client.networks.list(filters=network_filters) for network in networks: network.remove()
def test_default(self) -> None: """ The default Linux distribution is CentOS 7. This test does not wait for DC/OS and we do not test DC/OS Enterprise because these are covered by other tests which use the default settings. """ with Cluster( cluster_backend=Docker(), masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters node_distribution = _get_node_distribution(node=master) assert node_distribution == Distribution.CENTOS_7 with Cluster( # The distribution is also CentOS 7 if it is explicitly set. cluster_backend=Docker( linux_distribution=Distribution.CENTOS_7), masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters node_distribution = _get_node_distribution(node=master) assert node_distribution == Distribution.CENTOS_7
def test_docker_exec_transport( self, docker_network: Network, tmpdir: local, ) -> None: """ ``Node`` operations with the Docker exec transport work even if the node is on a custom network. """ with Cluster( cluster_backend=Docker( network=docker_network, transport=Transport.DOCKER_EXEC, ), agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters content = str(uuid.uuid4()) local_file = tmpdir.join('example_file.txt') local_file.write(content) random = uuid.uuid4().hex master_destination_dir = '/etc/{random}'.format(random=random) master_destination_path = Path(master_destination_dir) / 'file.txt' master.send_file( local_path=Path(str(local_file)), remote_path=master_destination_path, transport=Transport.DOCKER_EXEC, ) args = ['cat', str(master_destination_path)] result = master.run(args=args, transport=Transport.DOCKER_EXEC) assert result.stdout.decode() == content
def test_custom_docker_network( self, docker_network: Network, ) -> None: """ When a network is specified on the Docker backend, each container is connected to the default bridge network ``docker0`` and in addition it also connected to the custom network. The ``Node``'s IP addresses correspond to the custom network. """ with Cluster( cluster_backend=Docker( network=docker_network, transport=Transport.DOCKER_EXEC, ), agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters container = _get_container_from_node(node=master) networks = container.attrs['NetworkSettings']['Networks'] assert networks.keys() == set(['bridge', docker_network.name]) custom_network_ip = networks[docker_network.name]['IPAddress'] assert custom_network_ip == str(master.public_ip_address) assert custom_network_ip == str(master.private_ip_address)
def _oss_distribution_test( distribution: Distribution, oss_artifact: Path, ) -> None: """ Assert that given a ``linux_distribution``, an open source DC/OS ``Cluster`` with the Linux distribution is started. We use this rather than pytest parameterization so that we can separate the tests in ``.travis.yml``. """ with Cluster( cluster_backend=Docker(linux_distribution=distribution), masters=1, agents=0, public_agents=0, ) as cluster: cluster.install_dcos_from_path( build_artifact=oss_artifact, dcos_config=cluster.base_config, log_output_live=True, ) cluster.wait_for_dcos_oss() (master, ) = cluster.masters node_distribution = _get_node_distribution(node=master) assert node_distribution == distribution
def test_custom_version(self, docker_version: DockerVersion) -> None: """ It is possible to set a custom version of Docker. Running this test requires ``aufs`` to be available. Depending on your system, it may be possible to make ``aufs`` available using the following commands: .. code $ apt-get install linux-image-extra-$(uname -r) $ modprobe aufs """ # We specify the storage driver because `overlay2` is not compatible # with old versions of Docker. with Cluster( cluster_backend=Docker( docker_version=docker_version, storage_driver=DockerStorageDriver.AUFS, ), masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters node_docker_version = self._get_docker_version(node=master) assert docker_version == node_docker_version
def test_one_master_host_port_map(self) -> None: """ It is possible to expose admin router to a host port. """ with Cluster( cluster_backend=Docker( one_master_host_port_map={'80/tcp': 8000}), masters=3, agents=0, public_agents=0, ) as cluster: masters_containers = [ _get_container_from_node(node=node) for node in cluster.masters ] masters_ports_settings = [ container.attrs['HostConfig']['PortBindings'] for container in masters_containers ] masters_ports_settings.remove(None) masters_ports_settings.remove(None) [master_port_settings] = masters_ports_settings expected_master_port_settings = { '80/tcp': [{ 'HostIp': '', 'HostPort': '8000', }], } assert master_port_settings == expected_master_port_settings
def docker_network(self) -> Iterator[Network]: """ Return a Docker network. """ client = docker.from_env(version='auto') ipam_pool = docker.types.IPAMPool( subnet='172.28.0.0/16', iprange='172.28.0.0/24', gateway='172.28.0.254', ) # We use the default container prefix so that the # ``minidcos docker clean`` command cleans this up. prefix = Docker().container_name_prefix random = uuid.uuid4() name = '{prefix}-network-{random}'.format(prefix=prefix, random=random) network = client.networks.create( name=name, driver='bridge', ipam=docker.types.IPAMConfig(pool_configs=[ipam_pool]), attachable=False, ) try: yield network finally: network.remove()
def node_transport_option(command: Callable[..., None]) -> Callable[..., None]: """ An option decorator for node transport options. """ transports = { 'ssh': Transport.SSH, 'docker-exec': Transport.DOCKER_EXEC, } backend_default = Docker().transport [default_option] = [ transport for transport in transports if transports[transport] == backend_default ] function = click.option( '--transport', type=click.Choice(sorted(transports.keys())), callback=lambda ctx, param, value: transports[value], default=default_option, show_default=True, envvar='MINIDCOS_DOCKER_TRANSPORT', help=( 'The communication transport to use. ' 'On macOS the SSH transport requires IP routing to be set up. ' 'See "minidcos docker setup-mac-network". ' 'It also requires the "ssh" command to be available. ' 'This can be provided by setting the `MINIDCOS_DOCKER_TRANSPORT` ' 'environment variable. ' 'When using a TTY, different transports may use different line ' 'endings.'), )(command) # type: Callable[..., None] return function
def _oss_distribution_test( distribution: Distribution, oss_installer: Path, ) -> None: """ Assert that given a ``linux_distribution``, an open source DC/OS ``Cluster`` with the Linux distribution is started. We use this rather than pytest parameterization so that we can separate the tests in ``.travis.yml``. """ cluster_backend = Docker(linux_distribution=distribution) with Cluster( cluster_backend=cluster_backend, masters=1, agents=0, public_agents=0, ) as cluster: cluster.install_dcos_from_path( dcos_installer=oss_installer, dcos_config=cluster.base_config, output=Output.CAPTURE, ip_detect_path=cluster_backend.ip_detect_path, ) cluster.wait_for_dcos_oss() (master, ) = cluster.masters node_distribution = _get_node_distribution(node=master) assert node_distribution == distribution
def docker_backend() -> Docker: """ Creates a common Docker backend configuration that works within the pytest environment directory. """ tmp_dir_path = Path(os.environ['DCOS_E2E_TMP_DIR_PATH']) assert tmp_dir_path.exists() and tmp_dir_path.is_dir() return Docker(workspace_dir=tmp_dir_path)
def base_config(self) -> Dict[str, Any]: """ Return a base configuration for installing DC/OS OSS. """ backend = Docker() return { **self.cluster.base_config, **backend.base_config, }
def test_install_dcos_from_url(self, oss_installer_url: str) -> None: """ It is possible to install DC/OS on a cluster with a Docker backend. """ cluster_backend = Docker() with Cluster(cluster_backend=cluster_backend) as cluster: cluster.install_dcos_from_url( dcos_installer=oss_installer_url, dcos_config=cluster.base_config, ip_detect_path=cluster_backend.ip_detect_path, output=Output.LOG_AND_CAPTURE, ) cluster.wait_for_dcos_oss()
def test_install_dcos_from_url(self, oss_artifact_url: str) -> None: """ It is possible to install DC/OS on a cluster with a Docker backend. """ # We use a specific version of Docker on the nodes because else we may # hit https://github.com/opencontainers/runc/issues/1175. cluster_backend = Docker(docker_version=DockerVersion.v17_12_1_ce) with Cluster(cluster_backend=cluster_backend) as cluster: cluster.install_dcos_from_url( build_artifact=oss_artifact_url, dcos_config=cluster.base_config, ) cluster.wait_for_dcos_oss()
def test_custom(self) -> None: """ It is possible to set node Docker container labels. """ cluster_key = uuid.uuid4().hex cluster_value = uuid.uuid4().hex cluster_labels = {cluster_key: cluster_value} master_key = uuid.uuid4().hex master_value = uuid.uuid4().hex master_labels = {master_key: master_value} agent_key = uuid.uuid4().hex agent_value = uuid.uuid4().hex agent_labels = {agent_key: agent_value} public_agent_key = uuid.uuid4().hex public_agent_value = uuid.uuid4().hex public_agent_labels = {public_agent_key: public_agent_value} with Cluster( cluster_backend=Docker( docker_container_labels=cluster_labels, docker_master_labels=master_labels, docker_agent_labels=agent_labels, docker_public_agent_labels=public_agent_labels, ), masters=1, agents=1, public_agents=1, ) as cluster: for node in cluster.masters: node_labels = self._get_labels(node=node) assert node_labels[cluster_key] == cluster_value assert node_labels[master_key] == master_value assert agent_key not in node_labels assert public_agent_key not in node_labels for node in cluster.agents: node_labels = self._get_labels(node=node) assert node_labels[cluster_key] == cluster_value assert node_labels[agent_key] == agent_value assert master_key not in node_labels assert public_agent_key not in node_labels for node in cluster.public_agents: node_labels = self._get_labels(node=node) assert node_labels[cluster_key] == cluster_value assert node_labels[public_agent_key] == public_agent_value assert master_key not in node_labels assert agent_key not in node_labels
def test_default(self, host_driver: str) -> None: """ By default, the Docker storage driver is the same as the host's storage driver, if that driver is supported. """ client = docker.from_env(version='auto') info = {**client.info(), **{'Driver': host_driver}} with Mocker(real_http=True) as mock: mock.get(url=self._docker_info_endpoint, json=info) cluster_backend = Docker() storage_driver = cluster_backend.docker_storage_driver assert storage_driver == self.DOCKER_STORAGE_DRIVERS[host_driver]
def test_default(self) -> None: """ By default, the Docker version is 1.13.1. """ with Cluster( cluster_backend=Docker(), masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters docker_version = self._get_docker_version(node=master) assert docker_version == DockerVersion.v1_13_1
def _check_can_mount_in_docker() -> CheckLevels: """ Check for an incompatibility between some systemd versions and some versions of Docker. """ docker_client() cluster_backend = Docker(docker_version=DockerVersion.v1_13_1) args = ['docker', 'run', '-v', '/foo', 'alpine'] error_message_substring = 'no subsystem for mount' with Cluster(cluster_backend=cluster_backend) as cluster: (public_agent, ) = cluster.public_agents try: public_agent.run(args=args) except subprocess.CalledProcessError as exc: if error_message_substring not in exc.stderr.decode(): raise message = ( 'An issue has been detected which means that, for some ' 'versions of Docker inside DC/OS nodes, it will not be ' 'possible to create containers with mounts. ' 'Some functionality may be affected by this, for example ' 'extracting the DC/OS installer on a node.' '\n' 'This issue is likely because the host\'s version of systemd ' 'is greater than version 232, which causes the following ' 'known issue: ' 'https://github.com/opencontainers/runc/issues/1175.' '\n' 'Newer versions of Docker, work well with new versions of ' 'systemd. ' 'To avoid issues caused by this incompatibility, do one of ' 'the following:' '\n* Set ``systemd.legacy_systemd_cgroup_controller=yes`` as ' 'a kernel parameter on your host.' '\n* Use versions of Docker newer than 1.13.1 inside the ' 'DC/OS nodes.' ' To do this in the ``dcos-docker`` CLI, use the ' '``--docker-version`` option on ``dcos-docker create``.' ' To do this in the Python library, pass a ' '``docker_version`` parameter to the ``Docker`` backend class.' ) warn(message=message) return CheckLevels.WARNING return CheckLevels.NONE
def test_default(self) -> None: """ By default, the only network a container is in is the Docker default bridge network. """ with Cluster( cluster_backend=Docker(), agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters container = _get_container_from_node(node=master) networks = container.attrs['NetworkSettings']['Networks'] assert networks.keys() == set(['bridge']) bridge_ip_address = networks['bridge']['IPAddress'] assert bridge_ip_address == str(master.public_ip_address) assert bridge_ip_address == str(master.private_ip_address)
def test_custom( self, host_driver: str, custom_driver: DockerStorageDriver, ) -> None: """ A custom storage driver can be used. """ client = docker.from_env(version='auto') info = {**client.info(), **{'Driver': host_driver}} with Mocker(real_http=True) as mock: mock.get(url=self._docker_info_endpoint, json=info) cluster_backend = Docker(storage_driver=custom_driver) storage_driver = cluster_backend.docker_storage_driver assert storage_driver == custom_driver
def _enterprise_distribution_test( distribution: Distribution, enterprise_artifact: Path, license_key_contents: str, ) -> None: """ Assert that given a ``linux_distribution``, a DC/OS Enterprise ``Cluster`` with the Linux distribution is started. We use this rather than pytest parameterization so that we can separate the tests in ``.travis.yml``. """ superuser_username = str(uuid.uuid4()) superuser_password = str(uuid.uuid4()) config = { 'superuser_username': superuser_username, 'superuser_password_hash': sha512_crypt.hash(superuser_password), 'fault_domain_enabled': False, 'license_key_contents': license_key_contents, } cluster_backend = Docker(linux_distribution=distribution) with Cluster( cluster_backend=cluster_backend, masters=1, agents=0, public_agents=0, ) as cluster: cluster.install_dcos_from_path( build_artifact=enterprise_artifact, dcos_config={ **cluster.base_config, **config, }, ip_detect_path=cluster_backend.ip_detect_path, log_output_live=True, ) cluster.wait_for_dcos_ee( superuser_username=superuser_username, superuser_password=superuser_password, ) (master, ) = cluster.masters node_distribution = _get_node_distribution(node=master) assert node_distribution == distribution
def dcos_node(request: SubRequest) -> Iterator[Node]: """ Return a ``Node``. This is module scoped as we do not intend to modify the cluster in ways that make tests interfere with one another. """ # We use the Docker backend because it is currently the only one which # supports all transports. cluster_backend = Docker(transport=request.param) with Cluster( cluster_backend=cluster_backend, masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters yield master
def test_pass_bridge(self) -> None: """ If the bridge network is given, the only network a container is in is the Docker default bridge network. """ client = docker.from_env(version='auto') network = client.networks.get(network_id='bridge') with Cluster( cluster_backend=Docker(network=network), agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters container = _get_container_from_node(node=master) networks = container.attrs['NetworkSettings']['Networks'] assert networks.keys() == set(['bridge']) bridge_ip_address = networks['bridge']['IPAddress'] assert bridge_ip_address == str(master.public_ip_address) assert bridge_ip_address == str(master.private_ip_address)
def test_windows_agents( workspace_dir: Path, artifact_path: Path, request: SubRequest, log_dir: Path, ) -> None: """ Enabling Windows agents creates additional configuration package and does not break Linux installation. """ docker_backend = Docker(workspace_dir=workspace_dir) config = { 'enable_windows_agents': True, } with Cluster( cluster_backend=docker_backend, agents=0, public_agents=0, ) as cluster: cluster.install_dcos_from_path( dcos_installer=artifact_path, dcos_config={ **cluster.base_config, **config, }, output=Output.LOG_AND_CAPTURE, ip_detect_path=docker_backend.ip_detect_path, ) # Check that dcos-config-win.tar.xz was created paths = [] for root, _, files in os.walk(str(workspace_dir)): for file in files: if file.startswith('dcos-config-win--setup_'): paths.append(Path(root) / file) assert len(paths) == 1 wait_for_dcos_oss( cluster=cluster, request=request, log_dir=log_dir, )
def _check_can_build() -> CheckLevels: """ Check that the default cluster images can be built. """ cluster_backend = Docker(docker_version=DockerVersion.v1_13_1) try: with Cluster(cluster_backend=cluster_backend): pass except docker.errors.BuildError as exc: message = ('There was an error building a Docker image. ' 'The Docker logs follow.\n' '\n') for item in exc.build_log: if 'stream' in item: message += '\t' + item['stream'] error(message=message) return CheckLevels.ERROR return CheckLevels.NONE
def test_install_dcos_from_path(self, oss_artifact: Path) -> None: """ It is possible to install DC/OS on a node from a path. """ # We use a specific version of Docker on the nodes because else we may # hit https://github.com/opencontainers/runc/issues/1175. cluster_backend = Docker(docker_version=DockerVersion.v17_12_1_ce) with Cluster(cluster_backend=cluster_backend) as cluster: for nodes, role in ( (cluster.masters, Role.MASTER), (cluster.agents, Role.AGENT), (cluster.public_agents, Role.PUBLIC_AGENT), ): for node in nodes: node.install_dcos_from_path( build_artifact=oss_artifact, dcos_config=cluster.base_config, role=role, ) cluster.wait_for_dcos_oss()
def test_install_dcos_from_path(self, oss_installer: Path) -> None: """ It is possible to install DC/OS on a node from a path. """ cluster_backend = Docker() with Cluster(cluster_backend=cluster_backend) as cluster: for nodes, role in ( (cluster.masters, Role.MASTER), (cluster.agents, Role.AGENT), (cluster.public_agents, Role.PUBLIC_AGENT), ): for node in nodes: node.install_dcos_from_path( dcos_installer=oss_installer, dcos_config=cluster.base_config, ip_detect_path=cluster_backend.ip_detect_path, role=role, output=Output.LOG_AND_CAPTURE, ) cluster.wait_for_dcos_oss()
def test_install_dcos_from_url(self, oss_artifact_url: str) -> None: """ The Docker backend requires a build artifact in order to launch a DC/OS cluster. """ with Cluster( cluster_backend=Docker(), masters=1, agents=0, public_agents=0, ) as cluster: with pytest.raises(NotImplementedError) as excinfo: cluster.install_dcos_from_url(oss_artifact_url) expected_error = ( 'The Docker backend does not support the installation of DC/OS ' 'by build artifacts passed via URL string. This is because a more ' 'efficient installation method exists in `install_dcos_from_path`.' ) assert str(excinfo.value) == expected_error
def test_host_driver_not_supported(self) -> None: """ If the host's storage driver is not supported, `aufs` is used. """ client = docker.from_env(version='auto') info = {**client.info(), **{'Driver': 'not_supported'}} with Mocker(real_http=True) as mock: mock.get(url=self._docker_info_endpoint, json=info) backend = Docker() assert backend.docker_storage_driver == DockerStorageDriver.AUFS with Cluster( cluster_backend=backend, masters=1, agents=0, public_agents=0, ) as cluster: (master, ) = cluster.masters node_driver = self._get_storage_driver(node=master) assert node_driver == DockerStorageDriver.AUFS
def create( ctx: click.core.Context, agents: int, installer: Path, cluster_id: str, docker_storage_driver: Optional[DockerStorageDriver], docker_version: DockerVersion, extra_config: Dict[str, Any], linux_distribution: Distribution, masters: int, public_agents: int, license_key: Optional[Path], security_mode: Optional[str], copy_to_master: List[Tuple[Path, Path]], genconf_dir: Optional[Path], workspace_dir: Path, custom_volume: List[Mount], custom_master_volume: List[Mount], custom_agent_volume: List[Mount], custom_public_agent_volume: List[Mount], variant: str, transport: Transport, wait_for_dcos: bool, network: Network, one_master_host_port_map: Dict[str, int], mount_sys_fs_cgroup: bool, ) -> None: """ Create a DC/OS cluster. """ check_cluster_id_unique( new_cluster_id=cluster_id, existing_cluster_ids=existing_cluster_ids(), ) http_checks = bool(transport == Transport.SSH) wait_command_name = command_path(sibling_ctx=ctx, command=wait) doctor_command_name = command_path(sibling_ctx=ctx, command=doctor) doctor_message = get_doctor_message( doctor_command_name=doctor_command_name, ) public_key_path = workspace_dir / 'id_rsa.pub' private_key_path = workspace_dir / 'id_rsa' write_key_pair( public_key_path=public_key_path, private_key_path=private_key_path, ) dcos_variant = get_install_variant( given_variant=variant, installer_path=installer, workspace_dir=workspace_dir, doctor_message=doctor_message, ) # This is useful for some people to identify containers. container_name_prefix = Docker().container_name_prefix + '-' + cluster_id cluster_backend = Docker( container_name_prefix=container_name_prefix, custom_container_mounts=custom_volume, custom_master_mounts=custom_master_volume, custom_agent_mounts=custom_agent_volume, custom_public_agent_mounts=custom_public_agent_volume, linux_distribution=linux_distribution, docker_version=docker_version, storage_driver=docker_storage_driver, docker_container_labels={ CLUSTER_ID_LABEL_KEY: cluster_id, WORKSPACE_DIR_LABEL_KEY: str(workspace_dir), }, docker_master_labels={ NODE_TYPE_LABEL_KEY: NODE_TYPE_MASTER_LABEL_VALUE, }, docker_agent_labels={NODE_TYPE_LABEL_KEY: NODE_TYPE_AGENT_LABEL_VALUE}, docker_public_agent_labels={ NODE_TYPE_LABEL_KEY: NODE_TYPE_PUBLIC_AGENT_LABEL_VALUE, }, workspace_dir=workspace_dir, transport=transport, network=network, one_master_host_port_map=one_master_host_port_map, mount_sys_fs_cgroup=mount_sys_fs_cgroup, ) cluster = create_cluster( cluster_backend=cluster_backend, masters=masters, agents=agents, public_agents=public_agents, doctor_message=doctor_message, ) cluster_containers = ClusterContainers( cluster_id=cluster_id, transport=transport, ) private_ssh_key_path = cluster_containers.ssh_key_path private_ssh_key_path.parent.mkdir(parents=True) private_key_path.replace(private_ssh_key_path) add_authorized_key(cluster=cluster, public_key_path=public_key_path) for node in cluster.masters: for path_pair in copy_to_master: local_path, remote_path = path_pair node.send_file( local_path=local_path, remote_path=remote_path, ) dcos_config = get_config( cluster_representation=cluster_containers, extra_config=extra_config, dcos_variant=dcos_variant, security_mode=security_mode, license_key=license_key, ) install_dcos_from_path( cluster_representation=cluster_containers, dcos_config=dcos_config, ip_detect_path=cluster_backend.ip_detect_path, doctor_message=doctor_message, dcos_installer=installer, local_genconf_dir=genconf_dir, ) run_post_install_steps( cluster=cluster, cluster_id=cluster_id, dcos_config=dcos_config, doctor_command_name=doctor_command_name, http_checks=http_checks, wait_command_name=wait_command_name, wait_for_dcos=wait_for_dcos, )