Skip to content

Commit cb7d3b9

Browse files
committed
Add option to existing QEMU session for copy-in/copy-out
When --copy-using-image is specified, copy operations will be done using the main QEMU session instead of separate ones. This avoids the time spent starting up separate QEMU sessions before/after the main one to do file copying. However, use of this option requires that scp/rsync be present in the image
1 parent e3471a8 commit cb7d3b9

File tree

8 files changed

+164
-39
lines changed

8 files changed

+164
-39
lines changed

docs/cli/run_create_start_stop.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ ones common to both subcommands are described below:
9898
(/absolute/path/on/VM:path/on/host)
9999
- `--copy-timeout COPY_TIMEOUT`: The maximum time to wait for a copy-in-before or
100100
copy-out-after operation to complete
101+
- `--direct-copy`: Transfier files specified via `--copy-in-before` or `--copy-in-after` directly to the VM, instead of a background 'builder' VM
101102
- `--rsync`: Use rsync for copy-in-before/copy-out-after operations
102103

103104
#### VM Creation Flags
@@ -150,7 +151,7 @@ described below:
150151
### Create and Start an Alpine VM
151152

152153
```
153-
$ transient create --name bar --ssh alpine_rel3,http=https://github.com/ALSchwalm/transient-baseimages/releases/download/5/alpine-3.13.qcow2.xz
154+
$ transient create --name bar --ssh alpine_rel3,http=https://github.com/ALSchwalm/transient-baseimages/releases/download/6/alpine-3.13.qcow2.xz
154155
Unable to find image 'alpine' in backend
155156
Downloading image from 'https://github.com/ALSchwalm/transient-baseimages/releases/download/5/alpine-3.13.qcow2.xz'
156157
100% |##############################################| 12.7 MiB/s | 35.0 MiB | Time: 0:00:02

test/features/copy-in-copy-out.feature

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,88 @@ Feature: Copy-in and Copy-out Support
55
- copy the host file or directory to the guest directory before starting the VM
66
- copy the guest file or directory to the host directory after stopping the VM
77

8-
Scenario: Copy in a file before starting VM
8+
Scenario Outline: Copy in a file before starting VM
99
Given a transient run command
1010
And an http alpine disk image
1111
And a test file: "artifacts/copy-in-before-test-file"
1212
And a guest directory: "/home/vagrant/"
1313
And the test file is copied to the guest directory before starting
14+
And using the "<image_type>" image for copying
1415
And a ssh command "ls /home/vagrant"
1516
When the vm runs to completion
1617
Then the return code is 0
1718
And stdout contains "copy-in-before-test-file"
1819

19-
Scenario: Copy in a large file before starting VM
20+
Examples: Image Types
21+
| image_type |
22+
| dedicated |
23+
| same |
24+
25+
Scenario Outline: Copy in a large file before starting VM
2026
Given a transient run command
2127
And an http alpine disk image
2228
And a large test file: "artifacts/copy-in-large-test-file"
2329
And a guest directory: "/home/vagrant/"
2430
And the test file is copied to the guest directory before starting
31+
And using the "<image_type>" image for copying
2532
And a ssh command "ls /home/vagrant"
2633
When the vm runs to completion
2734
Then the return code is 0
2835
And stdout contains "copy-in-large-test-file"
2936

30-
Scenario: Copy out a file after stopping VM
37+
Examples: Image Types
38+
| image_type |
39+
| dedicated |
40+
| same |
41+
42+
Scenario Outline: Copy out a file after stopping VM
3143
Given a transient run command
3244
And an http alpine disk image
3345
And a host directory: "artifacts/"
3446
And a guest test file: "/home/vagrant/copy-out-after-test-file"
3547
And the guest test file is copied to the host directory after stopping
48+
And using the "<image_type>" image for copying
3649
And a ssh command "touch /home/vagrant/copy-out-after-test-file"
3750
When the vm runs to completion
3851
Then the return code is 0
3952
And the file "artifacts/copy-out-after-test-file" exists
4053

41-
Scenario: Copy out a file after stopping VM using rsync
54+
Examples: Image Types
55+
| image_type |
56+
| dedicated |
57+
| same |
58+
59+
Scenario Outline: Copy out a file after stopping VM using rsync
4260
Given a transient run command
4361
And an http alpine disk image
4462
And a host directory: "artifacts/"
4563
And a guest test file: "/home/vagrant/copy-out-after-test-file"
4664
And the guest test file is copied to the host directory after stopping
65+
And using the "<image_type>" image for copying
4766
And a ssh command "touch /home/vagrant/copy-out-after-test-file"
4867
And an extra argument "--rsync"
4968
When the vm runs to completion
5069
Then the return code is 0
5170
And the file "artifacts/copy-out-after-test-file" exists
5271

53-
Scenario: Copy in a symbolic link using rsync
72+
Examples: Image Types
73+
| image_type |
74+
| dedicated |
75+
| same |
76+
77+
Scenario Outline: Copy in a symbolic link using rsync
5478
Given a transient run command
5579
And an http alpine disk image
5680
And a symbolic link "artifacts/symlink" to "/etc/hostname"
5781
And a guest directory: "/home/vagrant/"
5882
And the test file is copied to the guest directory before starting
83+
And using the "<image_type>" image for copying
5984
And a ssh command "test -L /home/vagrant/symlink"
6085
And an extra argument "--rsync"
6186
When the vm runs to completion
6287
Then the return code is 0
88+
89+
Examples: Image Types
90+
| image_type |
91+
| dedicated |
92+
| same |

test/features/steps/steps.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def step_impl(context, image):
225225
def step_impl(context):
226226
context.vm_config[
227227
"transient-image"
228-
] = "alpine_rel3,http=https://github.com/ALSchwalm/transient-baseimages/releases/download/5/alpine-3.13.qcow2.xz"
228+
] = "alpine_rel3,http=https://github.com/ALSchwalm/transient-baseimages/releases/download/6/alpine-3.13.qcow2.xz"
229229

230230

231231
@given("an http centos disk image")
@@ -336,6 +336,13 @@ def step_impl(context):
336336
context.vm_config["transient-args"].extend(["--copy-out-after", directory_mapping])
337337

338338

339+
@given('using the "{image_type}" image for copying')
340+
def step_impl(context, image_type):
341+
assert image_type in ("same", "dedicated"), repr(image_type)
342+
if image_type == "same":
343+
context.vm_config["transient-args"].extend(["--direct-copy"])
344+
345+
339346
@given('a qemu flag "{flag}"')
340347
def step_impl(context, flag):
341348
context.vm_config["qemu-args"].append(flag)

transient/args.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ def set_default(value: Any) -> Any:
174174
type=int,
175175
help="The maximum time to wait for a copy-in-before or copy-out-after operation to complete",
176176
)
177+
common_oneshot_parser.add_argument(
178+
"--direct-copy",
179+
action="store_const",
180+
const=True,
181+
help="Transfier files specified via --copy-in-before or --copy-in-after directly to the VM, instead of a background 'builder' VM",
182+
)
177183
common_oneshot_parser.add_argument(
178184
"--rsync",
179185
action="store_const",

transient/configuration.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,10 @@ def config_requires_ssh_console(config: Union[RunConfig, CreateConfig]) -> bool:
276276
or config.ssh_command is not None
277277
or config.ssh_with_serial is True
278278
)
279+
280+
281+
def config_wants_rsync_transfer(config: Union[RunConfig, BuildConfig]) -> bool:
282+
if config.rsync is not None:
283+
assert isinstance(config.rsync, bool)
284+
return config.rsync is True
285+
return False

transient/editor.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,16 +283,21 @@ def run_command_in_guest(
283283
return None, None
284284

285285
def copy_in(self, host_path: str, guest_path: str) -> None:
286-
transfer = ssh.rsync if self.rsync is True else ssh.scp
287-
transfer(
288-
host_path, utils.join_absolute_paths("/mnt", guest_path), self.ssh_config
286+
use_rsync = self.rsync is True
287+
ssh.transfer(
288+
host_path,
289+
utils.join_absolute_paths("/mnt", guest_path),
290+
ssh_config=self.ssh_config,
291+
copy_from=False,
292+
use_rsync=use_rsync,
289293
)
290294

291295
def copy_out(self, guest_path: str, host_path: str) -> None:
292-
transfer = ssh.rsync if self.rsync is True else ssh.scp
293-
transfer(
296+
use_rsync = self.rsync is True
297+
ssh.transfer(
294298
utils.join_absolute_paths("/mnt", guest_path),
295299
host_path,
296-
self.ssh_config,
300+
ssh_config=self.ssh_config,
297301
copy_from=True,
302+
use_rsync=use_rsync,
298303
)

transient/ssh.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,19 @@ def rsync(
298298
capture_stdout=capture_stdout,
299299
capture_stderr=capture_stderr,
300300
)
301+
302+
303+
def transfer(
304+
host_path: str,
305+
guest_path: str,
306+
ssh_config: SshConfig,
307+
copy_from: bool,
308+
use_rsync: bool,
309+
) -> None:
310+
func = rsync if use_rsync is True else scp
311+
logging.debug(
312+
"Transfer host_path={} guest_path={} copy_from={} func={}".format(
313+
host_path, guest_path, copy_from, func
314+
)
315+
)
316+
func(host_path, guest_path, ssh_config, copy_from)

transient/transient.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class TransientVm:
5656
set_ssh_port: Optional[int]
5757
vmstate: Optional[store.VmPersistentState]
5858
state: TransientVmState
59+
copy_out_done: bool
5960

6061
def __init__(self, config: configuration.RunConfig, vmstore: store.VmStore) -> None:
6162
self.config = config
@@ -69,6 +70,7 @@ def __init__(self, config: configuration.RunConfig, vmstore: store.VmStore) -> N
6970
self.data_tempfile = tempfile.TemporaryFile("wb+", buffering=0)
7071
self.set_ssh_port = None
7172
self.vmstate = None
73+
self.copy_out_done = False
7274

7375
def __use_backend_images(self, names: List[str]) -> List[store.BackendImageInfo]:
7476
"""Ensure the backend images are download for each image spec in 'names'"""
@@ -88,6 +90,16 @@ def __needs_to_copy_in_files_before_running(self) -> bool:
8890
"""
8991
return len(self.config.copy_in_before) > 0
9092

93+
def __qemu_is_running(self) -> bool:
94+
return (self.qemu_runner and self.qemu_runner.is_running()) or False
95+
96+
def transfer(self, host_path: str, guest_path: str, copy_from: bool) -> None:
97+
""" Perform rsync/scp transfer, assumes guest is up """
98+
print("ALEX: config is ", type(self.config), repr(self.config))
99+
use_rsync = configuration.config_wants_rsync_transfer(self.config)
100+
assert self.ssh_config is not None
101+
ssh.transfer(host_path, guest_path, self.ssh_config, copy_from, use_rsync)
102+
91103
def __copy_in_files(self) -> None:
92104
"""Copies the given files or directories (located on the host) into the VM"""
93105
path_mappings = self.config.copy_in_before
@@ -110,19 +122,25 @@ def __copy_in(self, path_mapping: str) -> None:
110122
if not vm_absolute_path.startswith("/"):
111123
raise RuntimeError(f"Absolute path for guest required: {vm_absolute_path}")
112124

113-
assert isinstance(self.primary_image, store.FrontendImageInfo)
114-
assert self.primary_image.backend is not None
115-
logging.info(
116-
f"Copying from '{host_path}' to '{self.primary_image.backend.identifier}:{vm_absolute_path}'"
117-
)
125+
if not self.__qemu_is_running():
126+
assert isinstance(self.primary_image, store.FrontendImageInfo)
127+
assert self.primary_image.backend is not None
128+
logging.info(
129+
f"Copying from '{host_path}' to '{self.primary_image.backend.identifier}:{vm_absolute_path}'"
130+
)
118131

119-
with editor.ImageEditor(
120-
self.primary_image.path,
121-
self.config.ssh_timeout,
122-
self.config.qmp_timeout,
123-
self.config.rsync,
124-
) as edit:
125-
edit.copy_in(host_path, vm_absolute_path)
132+
with editor.ImageEditor(
133+
self.primary_image.path,
134+
self.config.ssh_timeout,
135+
self.config.qmp_timeout,
136+
self.config.rsync,
137+
) as edit:
138+
edit.copy_in(host_path, vm_absolute_path)
139+
else:
140+
logging.info(
141+
f"Copying from '{host_path}' to '(EXISTING QEMU):{vm_absolute_path}'"
142+
)
143+
self.transfer(host_path, vm_absolute_path, copy_from=False)
126144

127145
def __needs_to_copy_out_files_after_running(self) -> bool:
128146
"""Checks if at least one directory on the VM needs to be copied out
@@ -152,19 +170,25 @@ def __copy_out(self, path_mapping: str) -> None:
152170
if not vm_absolute_path.startswith("/"):
153171
raise RuntimeError(f"Absolute path for guest required: {vm_absolute_path}")
154172

155-
assert isinstance(self.primary_image, store.FrontendImageInfo)
156-
assert self.primary_image.backend is not None
157-
logging.info(
158-
f"Copying from '{self.primary_image.backend.identifier}:{vm_absolute_path}' to '{host_path}'"
159-
)
173+
if not self.__qemu_is_running():
174+
assert isinstance(self.primary_image, store.FrontendImageInfo)
175+
assert self.primary_image.backend is not None
176+
logging.info(
177+
f"Copying from '{self.primary_image.backend.identifier}:{vm_absolute_path}' to '{host_path}'"
178+
)
160179

161-
with editor.ImageEditor(
162-
self.primary_image.path,
163-
self.config.ssh_timeout,
164-
self.config.qmp_timeout,
165-
self.config.rsync,
166-
) as edit:
167-
edit.copy_out(vm_absolute_path, host_path)
180+
with editor.ImageEditor(
181+
self.primary_image.path,
182+
self.config.ssh_timeout,
183+
self.config.qmp_timeout,
184+
self.config.rsync,
185+
) as edit:
186+
edit.copy_out(vm_absolute_path, host_path)
187+
else:
188+
logging.info(
189+
f"Copying from '(EXISTING QEMU):{vm_absolute_path}' to '{host_path}'"
190+
)
191+
self.transfer(vm_absolute_path, host_path, copy_from=True)
168192

169193
def __qemu_added_args(self) -> List[str]:
170194
new_args = ["-name", self.name]
@@ -226,6 +250,12 @@ def __prepare_ssh(self) -> None:
226250
extra_options=self.config.ssh_option,
227251
)
228252

253+
def __ensure_ssh(self) -> None:
254+
assert self.ssh_config is not None
255+
client = ssh.SshClient(config=self.ssh_config, command="exit 0")
256+
conn = client.connect_stdout(timeout=self.config.ssh_timeout)
257+
conn.wait()
258+
229259
def __connect_ssh(self) -> int:
230260
assert self.ssh_config is not None
231261
client = ssh.SshClient(config=self.ssh_config, command=self.config.ssh_command)
@@ -281,8 +311,9 @@ def __qemu_sigchld_handler(self, sig: int, _frame: Any) -> None:
281311
def __post_run(self, returncode: int) -> None:
282312
self.state = TransientVmState.FINISHED
283313

284-
if self.__needs_to_copy_out_files_after_running():
314+
if self.__needs_to_copy_out_files_after_running() and not self.copy_out_done:
285315
self.__copy_out_files()
316+
self.copy_out_done = True
286317

287318
# If the config name is None, this is a temporary VM,
288319
# so remove any generated frontend images. However, if the
@@ -342,6 +373,13 @@ def run(self) -> None:
342373

343374
def __do_run(self) -> None:
344375
self.state = TransientVmState.RUNNING
376+
self.copy_out_done = False
377+
378+
# direct copy-in can only be done with SSH console (for the "before" part and only when requested with --direct-copy)
379+
will_direct_copy_in = (
380+
configuration.config_requires_ssh_console(self.config)
381+
and self.config.direct_copy
382+
)
345383

346384
if not self.__is_stateless():
347385
assert self.vmstate is not None
@@ -357,7 +395,7 @@ def __do_run(self) -> None:
357395
self.config.extra_image
358396
)
359397

360-
if self.__needs_to_copy_in_files_before_running():
398+
if self.__needs_to_copy_in_files_before_running() and not will_direct_copy_in:
361399
self.__copy_in_files()
362400

363401
print("Finished preparation. Starting virtual machine")
@@ -423,6 +461,10 @@ def __do_run(self) -> None:
423461
self.__prepare_proc_data()
424462

425463
if configuration.config_requires_ssh_console(self.config):
464+
if self.__needs_to_copy_in_files_before_running() and will_direct_copy_in:
465+
self.__ensure_ssh()
466+
self.__copy_in_files()
467+
426468
# Note that we always return the SSH exit code, even if the guest failed to
427469
# shut down. This ensures the shutdown_timeout=0 case is handled as expected.
428470
# (i.e., it returns the SSH code instead of a QEMU error)
@@ -433,6 +475,17 @@ def __do_run(self) -> None:
433475
# SIGCHLD exit.
434476
self.qemu_should_die = True
435477

478+
if self.__needs_to_copy_out_files_after_running() and self.config.direct_copy:
479+
# If the VM was shutdown or is otherwise inaccessible,
480+
# the copy-out operation will also be attempted in __post_run.
481+
try:
482+
self.__copy_out_files()
483+
self.copy_out_done = True
484+
except utils.TransientProcessError as e:
485+
logging.error(
486+
"copy_out during existing QEMU session failed: {}".format(e)
487+
)
488+
436489
try:
437490
# Wait a bit for the guest to finish the shutdown and QEMU to exit
438491
self.qemu_runner.shutdown(timeout=self.config.shutdown_timeout)

0 commit comments

Comments
 (0)