@@ -21,6 +21,13 @@ Prefix guest filenames with the instance name and a colon.
21
21
Example: limactl copy default:/etc/os-release .
22
22
`
23
23
24
+ type copyTool string
25
+
26
+ const (
27
+ rsync copyTool = "rsync"
28
+ scp copyTool = "scp"
29
+ )
30
+
24
31
func newCopyCommand () * cobra.Command {
25
32
copyCommand := & cobra.Command {
26
33
Use : "copy SOURCE ... TARGET" ,
@@ -49,13 +56,6 @@ func copyAction(cmd *cobra.Command, args []string) error {
49
56
return err
50
57
}
51
58
52
- arg0 , err := exec .LookPath ("scp" )
53
- if err != nil {
54
- return err
55
- }
56
- instances := make (map [string ]* store.Instance )
57
- scpFlags := []string {}
58
- scpArgs := []string {}
59
59
debug , err := cmd .Flags ().GetBool ("debug" )
60
60
if err != nil {
61
61
return err
@@ -65,6 +65,45 @@ func copyAction(cmd *cobra.Command, args []string) error {
65
65
verbose = true
66
66
}
67
67
68
+ cpTool := rsync
69
+ arg0 , err := exec .LookPath (string (cpTool ))
70
+ if err != nil {
71
+ arg0 , err = exec .LookPath (string (cpTool ))
72
+ if err != nil {
73
+ return err
74
+ }
75
+ }
76
+ logrus .Infof ("using copy tool %q" , arg0 )
77
+
78
+ var copyCmd * exec.Cmd
79
+ switch cpTool {
80
+ case scp :
81
+ copyCmd , err = scpCommand (arg0 , args , verbose , recursive )
82
+ case rsync :
83
+ copyCmd , err = rsyncCommand (arg0 , args , verbose , recursive )
84
+ default :
85
+ err = fmt .Errorf ("invalid copy tool %q" , cpTool )
86
+ }
87
+ if err != nil {
88
+ return err
89
+ }
90
+
91
+ copyCmd .Stdin = cmd .InOrStdin ()
92
+ copyCmd .Stdout = cmd .OutOrStdout ()
93
+ copyCmd .Stderr = cmd .ErrOrStderr ()
94
+ logrus .Debugf ("executing %v (may take a long time)" , copyCmd )
95
+
96
+ // TODO: use syscall.Exec directly (results in losing tty?)
97
+ return copyCmd .Run ()
98
+ }
99
+
100
+ func scpCommand (command string , args []string , verbose , recursive bool ) (* exec.Cmd , error ) {
101
+ instances := make (map [string ]* store.Instance )
102
+
103
+ scpFlags := []string {}
104
+ scpArgs := []string {}
105
+ var err error
106
+
68
107
if verbose {
69
108
scpFlags = append (scpFlags , "-v" )
70
109
} else {
@@ -74,6 +113,7 @@ func copyAction(cmd *cobra.Command, args []string) error {
74
113
if recursive {
75
114
scpFlags = append (scpFlags , "-r" )
76
115
}
116
+
77
117
// this assumes that ssh and scp come from the same place, but scp has no -V
78
118
legacySSH := sshutil .DetectOpenSSHVersion ("ssh" ).LessThan (* semver .New ("8.0.0" ))
79
119
for _ , arg := range args {
@@ -86,12 +126,12 @@ func copyAction(cmd *cobra.Command, args []string) error {
86
126
inst , err := store .Inspect (instName )
87
127
if err != nil {
88
128
if errors .Is (err , os .ErrNotExist ) {
89
- return fmt .Errorf ("instance %q does not exist, run `limactl create %s` to create a new instance" , instName , instName )
129
+ return nil , fmt .Errorf ("instance %q does not exist, run `limactl create %s` to create a new instance" , instName , instName )
90
130
}
91
- return err
131
+ return nil , err
92
132
}
93
133
if inst .Status == store .StatusStopped {
94
- return fmt .Errorf ("instance %q is stopped, run `limactl start %s` to start the instance" , instName , instName )
134
+ return nil , fmt .Errorf ("instance %q is stopped, run `limactl start %s` to start the instance" , instName , instName )
95
135
}
96
136
if legacySSH {
97
137
scpFlags = append (scpFlags , "-P" , fmt .Sprintf ("%d" , inst .SSHLocalPort ))
@@ -101,11 +141,11 @@ func copyAction(cmd *cobra.Command, args []string) error {
101
141
}
102
142
instances [instName ] = inst
103
143
default :
104
- return fmt .Errorf ("path %q contains multiple colons" , arg )
144
+ return nil , fmt .Errorf ("path %q contains multiple colons" , arg )
105
145
}
106
146
}
107
147
if legacySSH && len (instances ) > 1 {
108
- return errors .New ("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher" )
148
+ return nil , errors .New ("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher" )
109
149
}
110
150
scpFlags = append (scpFlags , "-3" , "--" )
111
151
scpArgs = append (scpFlags , scpArgs ... )
@@ -118,24 +158,90 @@ func copyAction(cmd *cobra.Command, args []string) error {
118
158
for _ , inst := range instances {
119
159
sshOpts , err = sshutil .SSHOpts ("ssh" , inst .Dir , * inst .Config .User .Name , false , false , false , false )
120
160
if err != nil {
121
- return err
161
+ return nil , err
122
162
}
123
163
}
124
164
} else {
125
165
// Copying among multiple hosts; we can't pass in host-specific options.
126
166
sshOpts , err = sshutil .CommonOpts ("ssh" , false )
127
167
if err != nil {
128
- return err
168
+ return nil , err
129
169
}
130
170
}
131
171
sshArgs := sshutil .SSHArgsFromOpts (sshOpts )
132
172
133
- sshCmd := exec .Command (arg0 , append (sshArgs , scpArgs ... )... )
134
- sshCmd .Stdin = cmd .InOrStdin ()
135
- sshCmd .Stdout = cmd .OutOrStdout ()
136
- sshCmd .Stderr = cmd .ErrOrStderr ()
137
- logrus .Debugf ("executing scp (may take a long time): %+v" , sshCmd .Args )
173
+ return exec .Command (command , append (sshArgs , scpArgs ... )... ), nil
174
+ }
138
175
139
- // TODO: use syscall.Exec directly (results in losing tty?)
140
- return sshCmd .Run ()
176
+ func rsyncCommand (command string , args []string , verbose , recursive bool ) (* exec.Cmd , error ) {
177
+ instances := make (map [string ]* store.Instance )
178
+
179
+ var instName string
180
+
181
+ rsyncFlags := []string {}
182
+ rsyncArgs := []string {}
183
+
184
+ if verbose {
185
+ rsyncFlags = append (rsyncFlags , "-v" , "--progress" )
186
+ } else {
187
+ rsyncFlags = append (rsyncFlags , "-q" )
188
+ }
189
+
190
+ if recursive {
191
+ rsyncFlags = append (rsyncFlags , "-r" )
192
+ }
193
+
194
+ for _ , arg := range args {
195
+ path := strings .Split (arg , ":" )
196
+ switch len (path ) {
197
+ case 1 :
198
+ inst , ok := instances [instName ]
199
+ if ! ok {
200
+ return nil , fmt .Errorf ("instance %q does not exist, run `limactl create %s` to create a new instance" , instName , instName )
201
+ }
202
+ guestVM := fmt .
Sprintf (
"%[email protected] :%s" ,
* inst .
Config .
User .
Name ,
path [
0 ])
203
+ rsyncArgs = append (rsyncArgs , guestVM )
204
+ case 2 :
205
+ instName = path [0 ]
206
+ inst , err := store .Inspect (instName )
207
+ if err != nil {
208
+ if errors .Is (err , os .ErrNotExist ) {
209
+ return nil , fmt .Errorf ("instance %q does not exist, run `limactl create %s` to create a new instance" , instName , instName )
210
+ }
211
+ return nil , err
212
+ }
213
+ sshOpts , err := sshutil .SSHOpts ("ssh" , inst .Dir , * inst .Config .User .Name , false , false , false , false )
214
+ if err != nil {
215
+ return nil , err
216
+ }
217
+
218
+ sshArgs := sshutil .SSHArgsFromOpts (sshOpts )
219
+ sshStr := fmt .Sprintf ("ssh -p %s %s" , fmt .Sprintf ("%d" , inst .SSHLocalPort ), strings .Join (sshArgs , " " ))
220
+
221
+ destDir := args [1 ]
222
+ mkdirCmd := exec .Command (
223
+ "ssh" ,
224
+ "-p" , fmt .Sprintf ("%d" , inst .SSHLocalPort ),
225
+ )
226
+ mkdirCmd .Args = append (mkdirCmd .Args , sshArgs ... )
227
+ mkdirCmd .Args = append (mkdirCmd .Args ,
228
+ fmt .Sprintf ("%s@%s" , * inst .Config .User .Name , "127.0.0.1" ),
229
+ fmt .Sprintf ("sudo mkdir -p %q && sudo chown %s:%s %s" , destDir , * inst .Config .User .Name , * inst .Config .User .Name , destDir ),
230
+ )
231
+ mkdirCmd .Stdout = os .Stdout
232
+ mkdirCmd .Stderr = os .Stderr
233
+ if err := mkdirCmd .Run (); err != nil {
234
+ return nil , fmt .Errorf ("failed to create directory %q on remote: %w" , destDir , err )
235
+ }
236
+
237
+ rsyncArgs = append (rsyncArgs , "-avz" , "-e" , sshStr , path [1 ])
238
+ instances [instName ] = inst
239
+ default :
240
+ return nil , fmt .Errorf ("path %q contains multiple colons" , arg )
241
+ }
242
+ }
243
+
244
+ rsyncArgs = append (rsyncFlags , rsyncArgs ... )
245
+
246
+ return exec .Command (command , rsyncArgs ... ), nil
141
247
}
0 commit comments