diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go index 4b5fc3e3cf6..8bd980a1229 100644 --- a/cmd/limactl/copy.go +++ b/cmd/limactl/copy.go @@ -21,6 +21,13 @@ Prefix guest filenames with the instance name and a colon. Example: limactl copy default:/etc/os-release . ` +type copyTool string + +const ( + rsync copyTool = "rsync" + scp copyTool = "scp" +) + func newCopyCommand() *cobra.Command { copyCommand := &cobra.Command{ Use: "copy SOURCE ... TARGET", @@ -49,13 +56,6 @@ func copyAction(cmd *cobra.Command, args []string) error { return err } - arg0, err := exec.LookPath("scp") - if err != nil { - return err - } - instances := make(map[string]*store.Instance) - scpFlags := []string{} - scpArgs := []string{} debug, err := cmd.Flags().GetBool("debug") if err != nil { return err @@ -65,6 +65,45 @@ func copyAction(cmd *cobra.Command, args []string) error { verbose = true } + cpTool := rsync + arg0, err := exec.LookPath(string(cpTool)) + if err != nil { + arg0, err = exec.LookPath(string(cpTool)) + if err != nil { + return err + } + } + logrus.Infof("using copy tool %q", arg0) + + var copyCmd *exec.Cmd + switch cpTool { + case scp: + copyCmd, err = scpCommand(arg0, args, verbose, recursive) + case rsync: + copyCmd, err = rsyncCommand(arg0, args, verbose, recursive) + default: + err = fmt.Errorf("invalid copy tool %q", cpTool) + } + if err != nil { + return err + } + + copyCmd.Stdin = cmd.InOrStdin() + copyCmd.Stdout = cmd.OutOrStdout() + copyCmd.Stderr = cmd.ErrOrStderr() + logrus.Debugf("executing %v (may take a long time)", copyCmd) + + // TODO: use syscall.Exec directly (results in losing tty?) + return copyCmd.Run() +} + +func scpCommand(command string, args []string, verbose, recursive bool) (*exec.Cmd, error) { + instances := make(map[string]*store.Instance) + + scpFlags := []string{} + scpArgs := []string{} + var err error + if verbose { scpFlags = append(scpFlags, "-v") } else { @@ -74,6 +113,7 @@ func copyAction(cmd *cobra.Command, args []string) error { if recursive { scpFlags = append(scpFlags, "-r") } + // this assumes that ssh and scp come from the same place, but scp has no -V legacySSH := sshutil.DetectOpenSSHVersion("ssh").LessThan(*semver.New("8.0.0")) for _, arg := range args { @@ -86,12 +126,12 @@ func copyAction(cmd *cobra.Command, args []string) error { inst, err := store.Inspect(instName) if err != nil { if errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) + return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) } - return err + return nil, err } if inst.Status == store.StatusStopped { - return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) + return nil, fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) } if legacySSH { scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort)) @@ -101,11 +141,11 @@ func copyAction(cmd *cobra.Command, args []string) error { } instances[instName] = inst default: - return fmt.Errorf("path %q contains multiple colons", arg) + return nil, fmt.Errorf("path %q contains multiple colons", arg) } } if legacySSH && len(instances) > 1 { - return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher") + return nil, errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher") } scpFlags = append(scpFlags, "-3", "--") scpArgs = append(scpFlags, scpArgs...) @@ -118,24 +158,90 @@ func copyAction(cmd *cobra.Command, args []string) error { for _, inst := range instances { sshOpts, err = sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false) if err != nil { - return err + return nil, err } } } else { // Copying among multiple hosts; we can't pass in host-specific options. sshOpts, err = sshutil.CommonOpts("ssh", false) if err != nil { - return err + return nil, err } } sshArgs := sshutil.SSHArgsFromOpts(sshOpts) - sshCmd := exec.Command(arg0, append(sshArgs, scpArgs...)...) - sshCmd.Stdin = cmd.InOrStdin() - sshCmd.Stdout = cmd.OutOrStdout() - sshCmd.Stderr = cmd.ErrOrStderr() - logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args) + return exec.Command(command, append(sshArgs, scpArgs...)...), nil +} - // TODO: use syscall.Exec directly (results in losing tty?) - return sshCmd.Run() +func rsyncCommand(command string, args []string, verbose, recursive bool) (*exec.Cmd, error) { + instances := make(map[string]*store.Instance) + + var instName string + + rsyncFlags := []string{} + rsyncArgs := []string{} + + if verbose { + rsyncFlags = append(rsyncFlags, "-v", "--progress") + } else { + rsyncFlags = append(rsyncFlags, "-q") + } + + if recursive { + rsyncFlags = append(rsyncFlags, "-r") + } + + for _, arg := range args { + path := strings.Split(arg, ":") + switch len(path) { + case 1: + inst, ok := instances[instName] + if !ok { + return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) + } + guestVM := fmt.Sprintf("%s@127.0.0.1:%s", *inst.Config.User.Name, path[0]) + rsyncArgs = append(rsyncArgs, guestVM) + case 2: + instName = path[0] + inst, err := store.Inspect(instName) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) + } + return nil, err + } + sshOpts, err := sshutil.SSHOpts("ssh", inst.Dir, *inst.Config.User.Name, false, false, false, false) + if err != nil { + return nil, err + } + + sshArgs := sshutil.SSHArgsFromOpts(sshOpts) + sshStr := fmt.Sprintf("ssh -p %s %s", fmt.Sprintf("%d", inst.SSHLocalPort), strings.Join(sshArgs, " ")) + + destDir := args[1] + mkdirCmd := exec.Command( + "ssh", + "-p", fmt.Sprintf("%d", inst.SSHLocalPort), + ) + mkdirCmd.Args = append(mkdirCmd.Args, sshArgs...) + mkdirCmd.Args = append(mkdirCmd.Args, + fmt.Sprintf("%s@%s", *inst.Config.User.Name, "127.0.0.1"), + fmt.Sprintf("sudo mkdir -p %q && sudo chown %s:%s %s", destDir, *inst.Config.User.Name, *inst.Config.User.Name, destDir), + ) + mkdirCmd.Stdout = os.Stdout + mkdirCmd.Stderr = os.Stderr + if err := mkdirCmd.Run(); err != nil { + return nil, fmt.Errorf("failed to create directory %q on remote: %w", destDir, err) + } + + rsyncArgs = append(rsyncArgs, "-avz", "-e", sshStr, path[1]) + instances[instName] = inst + default: + return nil, fmt.Errorf("path %q contains multiple colons", arg) + } + } + + rsyncArgs = append(rsyncFlags, rsyncArgs...) + + return exec.Command(command, rsyncArgs...), nil } diff --git a/hack/test-templates.sh b/hack/test-templates.sh index 8dfe2e24835..aebebb11cab 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -192,7 +192,7 @@ if [ "$got" != "$expected" ]; then fi INFO "Testing limactl copy command" -tmpfile="$HOME/lima-hostname" +tmpfile="/var/tmp/lima-hostname" rm -f "$tmpfile" limactl cp "$NAME":/etc/hostname "$tmpfile" defer "rm -f \"$tmpfile\"" diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 396401a4896..9d2c54d85da 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -424,6 +424,41 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { return info, nil } +func (a *HostAgent) installPackage() error { + logrus.Debugf("installing packages") + + faScript := `#!/bin/bash + if ! output=$(type rsync 2>&1); then + echo "rsync is not installed. Attempting to install..." + + # Try to install rsync based on the OS + if [ -f /etc/debian_version ]; then + sudo apt-get update && sudo apt-get install -y rsync + elif [ -f /etc/alpine-release ]; then + sudo apk add rsync + elif [ -f /etc/redhat-release ]; then + sudo yum install -y rsync + elif [ -f /etc/arch-release ]; then + sudo pacman -S --noconfirm rsync + else + echo "Unsupported Linux distribution. Please install rsync manually." + fi + + echo "rsync installation complete." + else + echo "rsync is already installed." + fi` + faDesc := "installing rsync" + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc) + logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) + if err != nil { + err = fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err) + return err + } + + return nil +} + func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { if *a.instConfig.Plain { logrus.Info("Running in plain mode. Mounts, port forwarding, containerd, etc. will be ignored. Guest agent will not be running.") @@ -439,6 +474,11 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { if err := a.waitForRequirements("essential", a.essentialRequirements()); err != nil { errs = append(errs, err) } + + if err := a.installPackage(); err != nil { + errs = append(errs, err) + } + if *a.instConfig.SSH.ForwardAgent { faScript := `#!/bin/bash set -eux -o pipefail