build.run: implement SSH remote builds using Paramiko.

This commit is contained in:
William D. Jones 2020-08-26 18:49:49 -04:00 committed by GitHub
parent 07a3685da8
commit ef7a3bcfb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 109 additions and 4 deletions

View file

@ -7,9 +7,11 @@ import subprocess
import tempfile
import zipfile
import hashlib
import pathlib
__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts"]
__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts", "RemoteSSHBuildProducts"]
class BuildPlan:
@ -74,9 +76,10 @@ class BuildPlan:
os.chdir(root)
for filename, content in self.files.items():
filename = os.path.normpath(filename)
# Just to make sure we don't accidentally overwrite anything outside of build root.
assert not filename.startswith("..")
filename = pathlib.Path(filename)
# Forbid parent directory components completely to avoid the possibility
# of writing outside the build root.
assert ".." not in filename.parts
dirname = os.path.dirname(filename)
if dirname:
os.makedirs(dirname, exist_ok=True)
@ -99,6 +102,82 @@ class BuildPlan:
finally:
os.chdir(cwd)
def execute_remote_ssh(self, *, connect_to = {}, root, run_script=True):
"""
Execute build plan using the remote SSH strategy. Files from the build
plan are transferred via SFTP to the directory ``root`` on a remote
server. If ``run_script`` is ``True``, the ``paramiko`` SSH client will
then run ``{script}.sh``. ``root`` can either be an absolute or
relative (to the login directory) path.
``connect_to`` is a dictionary that holds all input arguments to
``paramiko``'s ``SSHClient.connect``
(`documentation <http://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient.connect>`_).
At a minimum, the ``hostname`` input argument must be supplied in this
dictionary as the remote server.
Returns :class:`RemoteSSHBuildProducts`.
"""
from paramiko import SSHClient
with SSHClient() as client:
client.load_system_host_keys()
client.connect(**connect_to)
with client.open_sftp() as sftp:
def mkdir_exist_ok(path):
try:
sftp.mkdir(str(path))
except IOError as e:
# mkdir fails if directory exists. This is fine in nmigen.build.
# Reraise errors containing e.errno info.
if e.errno:
raise e
def mkdirs(path):
# Iteratively create parent directories of a file by iterating over all
# parents except for the root ("."). Slicing the parents results in
# TypeError, so skip over the root ("."); this also handles files
# already in the root directory.
for parent in reversed(path.parents):
if parent == pathlib.PurePosixPath("."):
continue
else:
mkdir_exist_ok(parent)
mkdir_exist_ok(root)
sftp.chdir(root)
for filename, content in self.files.items():
filename = pathlib.PurePosixPath(filename)
assert ".." not in filename.parts
mkdirs(filename)
mode = "wt" if isinstance(content, str) else "wb"
with sftp.file(str(filename), mode) as f:
# "b/t" modifier ignored in SFTP.
if mode == "wt":
f.write(content.encode("utf-8"))
else:
f.write(content)
if run_script:
transport = client.get_transport()
channel = transport.open_session()
channel.set_combine_stderr(True)
cmd = "if [ -f ~/.profile ]; then . ~/.profile; fi && cd {} && sh {}.sh".format(root, self.script)
channel.exec_command(cmd)
# Show the output from the server while products are built.
buf = channel.recv(1024)
while buf:
print(buf.decode("utf-8"), end="")
buf = channel.recv(1024)
return RemoteSSHBuildProducts(connect_to, root)
def execute(self):
"""
Execute build plan using the default strategy. Use one of the ``execute_*`` methods
@ -162,3 +241,28 @@ class LocalBuildProducts(BuildProducts):
super().get(filename, mode)
with open(os.path.join(self.__root, filename), "r" + mode) as f:
return f.read()
class RemoteSSHBuildProducts(BuildProducts):
def __init__(self, connect_to, root):
self.__connect_to = connect_to
self.__root = root
def get(self, filename, mode="b"):
super().get(filename, mode)
from paramiko import SSHClient
with SSHClient() as client:
client.load_system_host_keys()
client.connect(**self.__connect_to)
with client.open_sftp() as sftp:
sftp.chdir(self.__root)
with sftp.file(filename, "r" + mode) as f:
# "b/t" modifier ignored in SFTP.
if mode == "t":
return f.read().decode("utf-8")
else:
return f.read()

View file

@ -46,6 +46,7 @@ setup(
extras_require={
# this version requirement needs to be synchronized with the one in nmigen.back.verilog!
"builtin-yosys": ["nmigen-yosys>=0.9.*"],
"remote-build": ["paramiko~=2.7"],
},
packages=find_packages(exclude=["*.test*"]),
entry_points={