Skip to content

Commit

Permalink
T2405: add Git support to commit-archive
Browse files Browse the repository at this point in the history
  • Loading branch information
yunzheng committed Sep 11, 2023
1 parent af398c5 commit 0b8c578
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 11 deletions.
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Depends:
frr-snmp,
fuse-overlayfs,
libpam-google-authenticator,
git,
grc,
haproxy,
hostapd,
Expand Down
2 changes: 1 addition & 1 deletion interface-definitions/system-config-mgmt.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<description>Uniform Resource Identifier</description>
</valueHelp>
<constraint>
<validator name="url --file-transport"/>
<regex>(http|https|ftp|ftps|sftp|ssh|scp|tftp|git|git\+(\w+)):\/\/.*</regex>
</constraint>
<multi/>
</properties>
Expand Down
16 changes: 13 additions & 3 deletions python/vyos/config_mgmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from typing import Optional, Tuple, Union
from filecmp import cmp
from datetime import datetime
from textwrap import dedent
from textwrap import dedent, indent
from pathlib import Path
from tabulate import tabulate
from shutil import copy
Expand Down Expand Up @@ -367,9 +367,19 @@ def commit_archive(self):
remote_file = f'config.boot-{hostname}.{timestamp}'
source_address = self.source_address

if self.effective_locations:
print("Archiving config...")
for location in self.effective_locations:
upload(archive_config_file, f'{location}/{remote_file}',
source_host=source_address)
print(f" {location}", end=" ", flush=True)
try:
upload(archive_config_file, f'{location}/{remote_file}',
source_host=source_address, raise_error=True)
print("OK")
except Exception as e:
print("FAILED!")
print()
print(indent(str(e), " > "))
print()

# op-mode functions
#
Expand Down
142 changes: 135 additions & 7 deletions python/vyos/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +23,9 @@
import tempfile
import urllib.parse

from contextlib import contextmanager
from pathlib import Path

from ftplib import FTP
from ftplib import FTP_TLS

Expand All @@ -37,11 +41,22 @@
from vyos.utils.io import make_progressbar
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

@contextmanager
def umask(mask: int):
"""
Context manager that temporarily sets the process umask.
"""
oldmask = os.umask(mask)
try:
yield
finally:
os.umask(oldmask)

class InteractivePolicy(MissingHostKeyPolicy):
"""
Paramiko policy for interactively querying the user on whether to proceed
Expand Down Expand Up @@ -308,29 +323,142 @@ def upload(self, location: str):
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"]

# Try to use /config/auth/commit-archive.key as SSH identity
# We copy over the key so we can control the permissions
try:
path_privatekey = Path(directory) / "private.key"
path_privatekey.write_bytes(
Path("/config/auth/commit-archive.key").read_bytes()
)
ssh_command += ["-i", str(path_privatekey)]
except Exception:
pass

# 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, *args, **kwargs):
def download(local_path, urlstring, raise_error=False, *args, **kwargs):
try:
urlc(urlstring, *args, **kwargs).download(local_path)
except Exception as err:
if raise_error:
raise
print_error(f'Unable to download "{urlstring}": {err}')

def upload(local_path, urlstring, *args, **kwargs):
def upload(local_path, urlstring, raise_error=False, *args, **kwargs):
try:
urlc(urlstring, *args, **kwargs).upload(local_path)
except Exception as err:
if raise_error:
raise
print_error(f'Unable to upload "{urlstring}": {err}')

def get_remote_config(urlstring, source_host='', source_port=0):
Expand Down
2 changes: 2 additions & 0 deletions python/vyos/utils/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
(default is OSError) with the error code
expect: a list of error codes to consider as normal
"""
command = command.lstrip() if isinstance(command, str) else command
decoded, code = popen(
command, flag,
stdout=stdout, stderr=stderr,
Expand Down Expand Up @@ -169,6 +170,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
% rc_cmd('ip link show dev eth99')
(1, 'Device "eth99" does not exist.')
"""
command = command.lstrip() if isinstance(command, str) else command
out, code = popen(
command, flag,
stdout=stdout, stderr=stderr,
Expand Down

0 comments on commit 0b8c578

Please sign in to comment.