Skip to content

Commit c0e2849

Browse files
committed
ssh runner: partially migrated RemoteConnection from conan-io#17357
1 parent 2825235 commit c0e2849

File tree

1 file changed

+78
-30
lines changed

1 file changed

+78
-30
lines changed

conan/internal/runner/ssh.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from pathlib import Path
2+
from typing import Iterable
3+
import fnmatch
24
import pathlib
35
import tempfile
46

@@ -9,6 +11,8 @@
911
from io import BytesIO
1012
import sys
1113

14+
from conan.internal.runner.output import RunnerOutput
15+
1216
def ssh_info(msg, error=False):
1317
fg=Color.BRIGHT_MAGENTA
1418
if error:
@@ -48,6 +52,8 @@ def __init__(self, conan_api, command, host_profile, build_profile, args, raw_ar
4852
self.client = SSHClient()
4953
self.client.load_system_host_keys()
5054
self.client.connect(hostname)
55+
self.runner_output = RunnerOutput(hostname)
56+
self.remote_conn = RemoteConnection(self.client, self.runner_output)
5157

5258

5359
def run(self, use_cache=True):
@@ -138,14 +144,7 @@ def ensure_runner_environment(self):
138144
ssh_info(f"Expected remote conan command: {conan_cmd}")
139145

140146
# Check if remote Conan executable exists, otherwise invoke pip inside venv
141-
sftp = self.client.open_sftp()
142-
try:
143-
sftp.stat(conan_cmd)
144-
has_remote_conan = True
145-
except FileNotFoundError:
146-
has_remote_conan = False
147-
finally:
148-
sftp.close()
147+
has_remote_conan = self.remote_conn.check_file_exists(conan_cmd)
149148

150149
if not has_remote_conan:
151150
_, _stdout, _stderr = self.client.exec_command(f"{python_command} -m venv {conan_venv}")
@@ -183,26 +182,15 @@ def ensure_runner_environment(self):
183182
_, _stdout, _stderr = self.client.exec_command(f"{self.remote_conan} config home")
184183
ssh_info(f"Remote conan config home returned: {_stdout.read().decode().strip()}")
185184
_, _stdout, _stderr = self.client.exec_command(f"{self.remote_conan} profile detect --force")
186-
self._copy_profiles()
187-
188-
189-
def _copy_profiles(self):
190-
sftp = self.client.open_sftp()
185+
self._sync_conan_config()
191186

192-
# TODO: very questionable choices here
193-
try:
194-
profiles = {
195-
self.args.profile_host[0]: self.host_profile.dumps(),
196-
self.args.profile_build[0]: self.build_profile.dumps()
197-
}
198-
199-
for name, contents in profiles.items():
200-
dest_filename = self.remote_conan_home + f"/profiles/{name}"
201-
sftp.putfo(BytesIO(contents.encode()), dest_filename)
202-
except:
203-
raise ConanException("Unable to copy profiles to remote")
204-
finally:
205-
sftp.close()
187+
def _sync_conan_config(self):
188+
# Transfer conan config to remote
189+
self.remote_conn.put_dir(
190+
self.conan_api.config.home(),
191+
self.remote_conan_home,
192+
exclude_patterns=["p", ".conan.db", "*.pyc", "__pycache__", ".DS_Store", ".git"]
193+
)
206194

207195
def copy_working_conanfile_path(self):
208196
resolved_path = Path(self.args.path).resolve()
@@ -220,6 +208,7 @@ def copy_working_conanfile_path(self):
220208
self.remote_create_dir = _stdout.read().decode().strip().replace("\\", '/')
221209

222210
# Copy current folder to destination using sftp
211+
# self.remote_conn.put_dir(resolved_path.as_posix(), self.remote_create_dir)
223212
_Path = pathlib.PureWindowsPath if self.remote_is_windows else pathlib.PurePath
224213
sftp = self.client.open_sftp()
225214
for root, dirs, files in os.walk(resolved_path.as_posix()):
@@ -265,8 +254,67 @@ def update_local_cache(self, json_result):
265254
if stdout.channel.recv_exit_status() != 0:
266255
raise ConanException("Unable to save remote conan cache state")
267256

268-
sftp = self.client.open_sftp()
269257
with tempfile.TemporaryDirectory() as tmp:
270258
local_cache_tgz = os.path.join(tmp, 'cache.tgz')
271-
sftp.get(conan_cache_tgz, local_cache_tgz)
272-
package_list = self.conan_api.cache.restore(local_cache_tgz)
259+
self.remote_conn.get(conan_cache_tgz, local_cache_tgz)
260+
self.conan_api.cache.restore(local_cache_tgz)
261+
262+
263+
class RemoteConnection:
264+
def __init__(self, client, runner_output: RunnerOutput):
265+
from paramiko.client import SSHClient
266+
self.client: SSHClient = client
267+
self.runner_output = runner_output
268+
269+
def put(self, src: str, dst: str) -> None:
270+
try:
271+
sftp = self.client.open_sftp()
272+
sftp.put(src, dst)
273+
sftp.close()
274+
except IOError as e:
275+
self.runner_output.error(f"Unable to copy {src} to {dst}:\n{e}")
276+
277+
def put_dir(self, src: str, dst: str, exclude_patterns: Iterable[str] = []) -> None:
278+
source_folder = Path(src)
279+
destination_folder = Path(dst)
280+
for item in source_folder.iterdir():
281+
dest_item = (destination_folder / item.name).as_posix()
282+
# Check if item matches any exclude pattern
283+
if any(fnmatch.fnmatch(item.name, pattern) for pattern in exclude_patterns):
284+
continue
285+
if item.is_file():
286+
self.runner_output.verbose(f"Copying file {item.as_posix()} to {dest_item}")
287+
self.put(item.as_posix(), dest_item)
288+
elif item.is_dir():
289+
self.runner_output.verbose(f"Copying directory {item.as_posix()} to {dest_item}")
290+
self.mkdir(dest_item, ignore_existing=True)
291+
self.put_dir(item.as_posix(), dest_item, exclude_patterns)
292+
293+
def get(self, src: str, dst: str) -> None:
294+
try:
295+
sftp = self.client.open_sftp()
296+
sftp.get(src, dst)
297+
sftp.close()
298+
except IOError as e:
299+
self.runner_output.error(f"Unable to copy from remote {src} to {dst}:\n{e}")
300+
301+
def mkdir(self, folder: str, ignore_existing=False) -> None:
302+
sftp = self.client.open_sftp()
303+
try:
304+
sftp.mkdir(folder)
305+
except IOError:
306+
if ignore_existing:
307+
pass
308+
else:
309+
raise
310+
finally:
311+
sftp.close()
312+
313+
def check_file_exists(self, file: str) -> bool:
314+
try:
315+
sftp = self.client.open_sftp()
316+
sftp.stat(file)
317+
sftp.close()
318+
return True
319+
except FileNotFoundError:
320+
return False

0 commit comments

Comments
 (0)