コード例 #1
0
    def create_file(
        self,
        *,
        destination: pathlib.Path,
        content: io.BytesIO,
        file_mode: str,
        group: str = "root",
        user: str = "root",
    ) -> None:
        """Create file with content and file mode.

        Multipass transfers data as "ubuntu" user, forcing us to first copy a
        file to a temporary location before moving to a (possibly) root-owned
        location and with appropriate permissions.

        :param destination: Path to file.
        :param content: Contents of file.
        :param file_mode: File mode string (e.g. '0644').
        :param group: File group owner/id.
        :param user: File user owner/id.
        """
        try:
            tmp_file_path = self._multipass.exec(
                instance_name=self.name,
                command=["mktemp"],
                runner=subprocess.run,
                capture_output=True,
                check=True,
                text=True,
            ).stdout.strip()

            self._multipass.transfer_source_io(
                source=content,
                destination=f"{self.name}:{tmp_file_path}",
            )

            self.execute_run(
                ["sudo", "chown", f"{user}:{group}", tmp_file_path],
                capture_output=True,
                check=True,
            )

            self.execute_run(
                ["sudo", "chmod", file_mode, tmp_file_path],
                capture_output=True,
                check=True,
            )

            self.execute_run(
                ["sudo", "mv", tmp_file_path,
                 destination.as_posix()],
                capture_output=True,
                check=True,
            )
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=
                f"Failed to create file {destination.as_posix()!r} in {self.name!r} VM.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #2
0
    def launch(
        self,
        *,
        instance_name: str,
        image: str,
        cpus: str = None,
        mem: str = None,
        disk: str = None,
    ) -> None:
        """Launch multipass VM.

        :param instance_name: The name the launched instance_name will have.
        :param image: Name of image to create the instance with.
        :param cpus: Amount of virtual CPUs to assign to the launched instance_name.
        :param mem: Amount of RAM to assign to the launched instance_name.
        :param disk: Amount of disk space the instance_name will see.

        :raises MultipassError: on error.
        """
        command = ["launch", image, "--name", instance_name]
        if cpus is not None:
            command.extend(["--cpus", cpus])
        if mem is not None:
            command.extend(["--mem", mem])
        if disk is not None:
            command.extend(["--disk", disk])

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to launch VM {instance_name!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #3
0
def test_details_from_called_process_error():
    error = subprocess.CalledProcessError(
        -1, ["test-command", "flags", "quote$me"], "test stdout", "test stderr"
    )

    details = errors.details_from_called_process_error(error)

    assert details == textwrap.dedent(
        """\
            * Command that failed: "test-command flags 'quote$me'"
            * Command exit code: -1
            * Command output: 'test stdout'
            * Command standard error output: 'test stderr'"""
    )
コード例 #4
0
    def start(self, *, instance_name: str) -> None:
        """Start VM instance.

        :param instance_name: the name of the instance to start.

        :raises MultipassError: on error.
        """
        command = ["start", instance_name]

        try:
            self._run(command, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to start VM {instance_name!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #5
0
def test_create_file_error(mock_multipass, instance):
    error = subprocess.CalledProcessError(-1, ["mktemp"], "test stdout",
                                          "test stderr")

    mock_multipass.exec.side_effect = error

    with pytest.raises(MultipassError) as exc_info:
        instance.create_file(
            destination=pathlib.Path("/etc/test.conf"),
            content=io.BytesIO(b"foo"),
            file_mode="0644",
        )

    assert exc_info.value == MultipassError(
        brief="Failed to create file '/etc/test.conf' in 'test-instance' VM.",
        details=errors.details_from_called_process_error(error),
    )
コード例 #6
0
    def umount(self, *, mount: str) -> None:
        """Unmount target in VM.

        :param mount: Mount point in <name>[:<path>] format, where <name> are
            instance names, and optional <path> are mount points.  If omitted,
            all mounts will be removed from the name instance.

        :raises MultipassError: On error.
        """
        command = ["umount", mount]

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to unmount {mount!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #7
0
    def info(self, *, instance_name: str) -> Dict[str, Any]:
        """Get information/state for instance.

        :returns: Parsed json data from info command.

        :raises MultipassError: On error.
        """
        command = ["info", instance_name, "--format", "json"]

        try:
            proc = self._run(command, capture_output=True, check=True, text=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to query info for VM {instance_name!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error

        return json.loads(proc.stdout)
コード例 #8
0
    def transfer(self, *, source: str, destination: str) -> None:
        """Transfer to destination path with source IO.

        :param source: The source path, prefixed with <name:> for a path inside
            the instance.
        :param destination: The destination path, prefixed with <name:> for a
            path inside the instance.

        :raises MultipassError: On error.
        """
        command = ["transfer", source, destination]

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to transfer {source!r} to {destination!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #9
0
    def list(self) -> List[str]:
        """List names of VMs.

        :returns: Data from stdout if instance exists, else None.

        :raises MultipassError: On error.
        """
        command = ["list", "--format", "json"]

        try:
            proc = self._run(command, capture_output=True, check=True, text=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief="Failed to query list of VMs.",
                details=errors.details_from_called_process_error(error),
            ) from error

        data_list = json.loads(proc.stdout).get("list", list())
        return [instance["name"] for instance in data_list]
コード例 #10
0
    def delete(self, *, instance_name: str, purge=True) -> None:
        """Passthrough for running multipass delete.

        :param instance_name: The name of the instance_name to delete.
        :param purge: Flag to purge the instance_name's image after deleting.

        :raises MultipassError: on error.

        """
        command = ["delete", instance_name]
        if purge:
            command.append("--purge")

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to delete VM {instance_name!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #11
0
    def stop(self, *, instance_name: str, delay_mins: int = 0) -> None:
        """Stop VM instance.

        :param instance_name: the name of the instance_name to stop.
        :param delay_mins: Delay shutdown for specified number of minutes.

        :raises MultipassError: on error.
        """
        command = ["stop", instance_name]

        if delay_mins != 0:
            command.extend(["--time", str(delay_mins)])

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to stop VM {instance_name!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error
コード例 #12
0
    def mount(
        self,
        *,
        source: pathlib.Path,
        target: str,
        uid_map: Dict[str, str] = None,
        gid_map: Dict[str, str] = None,
    ) -> None:
        """Mount host source path to target.

        :param source: Path of local directory to mount.
        :param target: Target mount points, in <name>[:<path>] format, where
            <name> is an instance name, and optional <path> is the mount point.
            If omitted, the mount point will be the same as the source's
            absolute path.
        :param uid_map: A mapping of user IDs for use in the mount of the form
            <host-id> -> <instance-name-id>.  File and folder ownership will be
            mapped from <host> to <instance-name> inside the instance_name.
        :param gid_map: A mapping of group IDs for use in the mount of the form
            <host-id> -> <instance-name-id>.  File and folder ownership will be
            mapped from <host> to <instance-name> inside the instance_name.
        """
        command = ["mount", str(source), target]

        if uid_map is not None:
            for host_id, instance_id in uid_map.items():
                command.extend(["--uid-map", f"{host_id}:{instance_id}"])

        if gid_map is not None:
            for host_id, instance_id in gid_map.items():
                command.extend(["--gid-map", f"{host_id}:{instance_id}"])

        try:
            self._run(command, capture_output=True, check=True)
        except subprocess.CalledProcessError as error:
            raise MultipassError(
                brief=f"Failed to mount {str(source)!r} to {target!r}.",
                details=errors.details_from_called_process_error(error),
            ) from error