build.run: implement SSH remote builds using Paramiko.
This commit is contained in:
parent
07a3685da8
commit
ef7a3bcfb1
|
@ -7,9 +7,11 @@ import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts"]
|
__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts", "RemoteSSHBuildProducts"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BuildPlan:
|
class BuildPlan:
|
||||||
|
@ -74,9 +76,10 @@ class BuildPlan:
|
||||||
os.chdir(root)
|
os.chdir(root)
|
||||||
|
|
||||||
for filename, content in self.files.items():
|
for filename, content in self.files.items():
|
||||||
filename = os.path.normpath(filename)
|
filename = pathlib.Path(filename)
|
||||||
# Just to make sure we don't accidentally overwrite anything outside of build root.
|
# Forbid parent directory components completely to avoid the possibility
|
||||||
assert not filename.startswith("..")
|
# of writing outside the build root.
|
||||||
|
assert ".." not in filename.parts
|
||||||
dirname = os.path.dirname(filename)
|
dirname = os.path.dirname(filename)
|
||||||
if dirname:
|
if dirname:
|
||||||
os.makedirs(dirname, exist_ok=True)
|
os.makedirs(dirname, exist_ok=True)
|
||||||
|
@ -99,6 +102,82 @@ class BuildPlan:
|
||||||
finally:
|
finally:
|
||||||
os.chdir(cwd)
|
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):
|
def execute(self):
|
||||||
"""
|
"""
|
||||||
Execute build plan using the default strategy. Use one of the ``execute_*`` methods
|
Execute build plan using the default strategy. Use one of the ``execute_*`` methods
|
||||||
|
@ -162,3 +241,28 @@ class LocalBuildProducts(BuildProducts):
|
||||||
super().get(filename, mode)
|
super().get(filename, mode)
|
||||||
with open(os.path.join(self.__root, filename), "r" + mode) as f:
|
with open(os.path.join(self.__root, filename), "r" + mode) as f:
|
||||||
return f.read()
|
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()
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -46,6 +46,7 @@ setup(
|
||||||
extras_require={
|
extras_require={
|
||||||
# this version requirement needs to be synchronized with the one in nmigen.back.verilog!
|
# this version requirement needs to be synchronized with the one in nmigen.back.verilog!
|
||||||
"builtin-yosys": ["nmigen-yosys>=0.9.*"],
|
"builtin-yosys": ["nmigen-yosys>=0.9.*"],
|
||||||
|
"remote-build": ["paramiko~=2.7"],
|
||||||
},
|
},
|
||||||
packages=find_packages(exclude=["*.test*"]),
|
packages=find_packages(exclude=["*.test*"]),
|
||||||
entry_points={
|
entry_points={
|
||||||
|
|
Loading…
Reference in a new issue