diff options
Diffstat (limited to 'python/vyos/remote.py')
| -rw-r--r-- | python/vyos/remote.py | 131 |
1 files changed, 123 insertions, 8 deletions
diff --git a/python/vyos/remote.py b/python/vyos/remote.py index b7c9ed176..129e65772 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import pwd import shutil import socket import ssl @@ -22,6 +23,9 @@ import sys import tempfile import urllib.parse +from contextlib import contextmanager +from pathlib import Path + from ftplib import FTP from ftplib import FTP_TLS @@ -37,7 +41,7 @@ from vyos.utils.io import ask_yes_no from vyos.utils.io import is_interactive from vyos.utils.io import print_error from vyos.utils.misc import begin -from vyos.utils.process import cmd +from vyos.utils.process import cmd, rc_cmd from vyos.version import get_version CHUNK_SIZE = 8192 @@ -73,6 +77,17 @@ class SourceAdapter(HTTPAdapter): num_pools=connections, maxsize=maxsize, block=block, source_address=self._source_pair) +@contextmanager +def umask(mask: int): + """ + Context manager that temporarily sets the process umask. + """ + import os + oldmask = os.umask(mask) + try: + yield + finally: + os.umask(oldmask) def check_storage(path, size): """ @@ -133,7 +148,7 @@ class FtpC: # Almost all FTP servers support the `SIZE' command. size = conn.size(self.path) if self.check_space: - check_storage(path, size) + check_storage(location, size) # No progressbar if we can't determine the size or if the file is too small. if self.progressbar and size and size > CHUNK_SIZE: with Progressbar(CHUNK_SIZE / size) as p: @@ -310,25 +325,125 @@ class TftpC: with open(location, 'rb') as f: cmd(f'{self.command} -T - "{self.urlstring}"', input=f.read()) +class GitC: + def __init__(self, + url, + progressbar=False, + check_space=False, + source_host=None, + source_port=0, + timeout=10, + ): + self.command = 'git' + self.url = url + self.urlstring = urllib.parse.urlunsplit(url) + if self.urlstring.startswith("git+"): + self.urlstring = self.urlstring.replace("git+", "", 1) + + def download(self, location: str): + raise NotImplementedError("not supported") + + @umask(0o077) + def upload(self, location: str): + scheme = self.url.scheme + _, _, scheme = scheme.partition("+") + netloc = self.url.netloc + url = Path(self.url.path).parent + with tempfile.TemporaryDirectory(prefix="git-commit-archive-") as directory: + # Determine username, fullname, email for Git commit + pwd_entry = pwd.getpwuid(os.getuid()) + user = pwd_entry.pw_name + name = pwd_entry.pw_gecos.split(",")[0] or user + fqdn = socket.getfqdn() + email = f"{user}@{fqdn}" + + # environment vars for our git commands + env = { + "GIT_TERMINAL_PROMPT": "0", + "GIT_AUTHOR_NAME": name, + "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": name, + "GIT_COMMITTER_EMAIL": email, + } + + # build ssh command for git + ssh_command = ["ssh"] + + # if we are not interactive, we use StrictHostKeyChecking=yes to avoid any prompts + if not sys.stdout.isatty(): + ssh_command += ["-o", "StrictHostKeyChecking=yes"] + + env["GIT_SSH_COMMAND"] = " ".join(ssh_command) + + # git clone + path_repository = Path(directory) / "repository" + scheme = f"{scheme}://" if scheme else "" + rc, out = rc_cmd( + [self.command, "clone", f"{scheme}{netloc}{url}", str(path_repository), "--depth=1"], + env=env, + shell=False, + ) + if rc: + raise Exception(out) + + # git add + filename = Path(Path(self.url.path).name).stem + dst = path_repository / filename + shutil.copy2(location, dst) + rc, out = rc_cmd( + [self.command, "-C", str(path_repository), "add", filename], + env=env, + shell=False, + ) + + # git commit -m + commit_message = os.environ.get("COMMIT_COMMENT", "commit") + rc, out = rc_cmd( + [self.command, "-C", str(path_repository), "commit", "-m", commit_message], + env=env, + shell=False, + ) + + # git push + rc, out = rc_cmd( + [self.command, "-C", str(path_repository), "push"], + env=env, + shell=False, + ) + if rc: + raise Exception(out) + def urlc(urlstring, *args, **kwargs): """ Dynamically dispatch the appropriate protocol class. """ - url_classes = {'http': HttpC, 'https': HttpC, 'ftp': FtpC, 'ftps': FtpC, \ - 'sftp': SshC, 'ssh': SshC, 'scp': SshC, 'tftp': TftpC} + url_classes = { + "http": HttpC, + "https": HttpC, + "ftp": FtpC, + "ftps": FtpC, + "sftp": SshC, + "ssh": SshC, + "scp": SshC, + "tftp": TftpC, + "git": GitC, + } url = urllib.parse.urlsplit(urlstring) + scheme, _, _ = url.scheme.partition("+") try: - return url_classes[url.scheme](url, *args, **kwargs) + return url_classes[scheme](url, *args, **kwargs) except KeyError: - raise ValueError(f'Unsupported URL scheme: "{url.scheme}"') + raise ValueError(f'Unsupported URL scheme: "{scheme}"') def download(local_path, urlstring, progressbar=False, check_space=False, - source_host='', source_port=0, timeout=10.0): + source_host='', source_port=0, timeout=10.0, raise_error=False): try: progressbar = progressbar and is_interactive() urlc(urlstring, progressbar, check_space, source_host, source_port, timeout).download(local_path) except Exception as err: + if raise_error: + raise print_error(f'Unable to download "{urlstring}": {err}') sys.exit(1) except KeyboardInterrupt: @@ -339,7 +454,7 @@ def upload(local_path, urlstring, progressbar=False, source_host='', source_port=0, timeout=10.0): try: progressbar = progressbar and is_interactive() - urlc(urlstring, progressbar, source_host, source_port, timeout).upload(local_path) + urlc(urlstring, progressbar, False, source_host, source_port, timeout).upload(local_path) except Exception as err: print_error(f'Unable to upload "{urlstring}": {err}') sys.exit(1) |
