diff --git a/README.md b/README.md index ee76531f..6b7e7c0a 100644 --- a/README.md +++ b/README.md @@ -685,6 +685,19 @@ usage: serverStat servername options
+ + ssh-agent-helper + +usage: source ssh-agent-helper

+Helper script for starting the ssh agent if needed and doing an ssh-add -t 12h. +This will let anyone smoothly run github/ssh related scripts without multiple password prompts. +An ssh-agent process started by using this script will be automatically closed on logout. +

+This script is intended to be sourced. +Sourcing this script lets ssh-agent set the proper environment variables it needs to run properly. + + + startami diff --git a/scripts/ioc-deploy b/scripts/ioc-deploy deleted file mode 120000 index 34cc4a13..00000000 --- a/scripts/ioc-deploy +++ /dev/null @@ -1 +0,0 @@ -ioc_deploy.py \ No newline at end of file diff --git a/scripts/ioc-deploy b/scripts/ioc-deploy new file mode 100755 index 00000000..6365d5e8 --- /dev/null +++ b/scripts/ioc-deploy @@ -0,0 +1,18 @@ +#!/usr/bin/bash +# Wrapper script for deploying IOCs from github. +# See ioc_deploy.py or try --help for usage +if ! ssh-add -L &> /dev/null; then + >&2 echo "" + >&2 echo " WARNING: Your ssh key is not in the ssh agent." + >&2 echo " If you run ioc-deploy like this, you may need to input" + >&2 echo " your ssh key password multiple times!" + >&2 echo " Try '$(tput bold)source ssh-agent-helper$(tput sgr0)' to input your ssh key" + >&2 echo " password only once per login." + >&2 echo "" +fi + +THIS_SCRIPT="$(realpath "${BASH_SOURCE[0]}")" +THIS_DIR="$(dirname "${THIS_SCRIPT}")" + +# Run the deploy script +/usr/bin/python3 "${THIS_DIR}"/ioc_deploy.py "$@" diff --git a/scripts/ioc_deploy.py b/scripts/ioc_deploy.py old mode 100755 new mode 100644 index fc7d9c13..ccc656b8 --- a/scripts/ioc_deploy.py +++ b/scripts/ioc_deploy.py @@ -24,9 +24,8 @@ import os import subprocess import sys -import urllib.error -import urllib.request from pathlib import Path +from tempfile import TemporaryDirectory EPICS_SITE_TOP_DEFAULT = "/cds/group/pcds/epics" GITHUB_ORG_DEFAULT = "pcdshub" @@ -133,9 +132,14 @@ def main(args: CliArgs) -> int: ) logger.info("Running ioc-deploy: checking inputs") - upd_name = finalize_name(name=args.name, github_org=args.github_org) + upd_name = finalize_name( + name=args.name, github_org=args.github_org, verbose=args.verbose + ) upd_rel = finalize_tag( - name=upd_name, github_org=args.github_org, release=args.release + name=upd_name, + github_org=args.github_org, + release=args.release, + verbose=args.verbose, ) deploy_dir = get_target_dir(name=upd_name, ioc_dir=args.ioc_dir, release=upd_rel) @@ -153,6 +157,7 @@ def main(args: CliArgs) -> int: release=upd_rel, deploy_dir=deploy_dir, dry_run=args.dry_run, + verbose=args.verbose, ) if rval != ReturnCode.SUCCESS: logger.error(f"Nonzero return value {rval} from git clone") @@ -166,7 +171,7 @@ def main(args: CliArgs) -> int: return ReturnCode.SUCCESS -def finalize_name(name: str, github_org: str) -> str: +def finalize_name(name: str, github_org: str, verbose: bool) -> str: """ Check if name is present in org and is well-formed. @@ -189,20 +194,19 @@ def finalize_name(name: str, github_org: str) -> str: logger.warning(f"{name} is not an ioc name, trying {new_name}") name = new_name logger.debug(f"Checking for {name} in org {github_org}") - try: - resp = urllib.request.urlopen(f"https://github.com/{github_org}/{name}") - if resp.code == 200: - logger.info(f"{name} exists in {github_org}") - return name - else: - logger.error(f"Unexpected http error code {resp.code}") - except urllib.error.HTTPError as exc: - logger.error(exc) - logger.debug("", exc_info=True) - raise ValueError(f"{name} does not exist in {github_org}") + with TemporaryDirectory() as tmpdir: + try: + _clone( + name=name, github_org=github_org, working_dir=tmpdir, verbose=verbose + ) + except subprocess.CalledProcessError as exc: + raise ValueError( + f"Error cloning repo, make sure {name} exists in {github_org} and check your permissions!" + ) from exc + return name -def finalize_tag(name: str, github_org: str, release: str) -> str: +def finalize_tag(name: str, github_org: str, release: str, verbose: bool) -> str: """ Check if release is present in the org. @@ -223,19 +227,23 @@ def finalize_tag(name: str, github_org: str, release: str) -> str: else: try_release.extend([f"R{release}", f"v{release}"]) - for rel in try_release: - logger.debug(f"Checking for release {rel} in {github_org}/{name}") - try: - resp = urllib.request.urlopen( - f"https://github.com/{github_org}/{name}/releases/tag/{rel}" - ) - if resp.code == 200: + with TemporaryDirectory() as tmpdir: + for rel in try_release: + logger.debug(f"Checking for release {rel} in {github_org}/{name}") + try: + _clone( + name=name, + github_org=github_org, + release=rel, + working_dir=tmpdir, + target_dir=rel, + verbose=verbose, + ) + except subprocess.CalledProcessError: + logger.warning(f"Did not find release {rel} in {github_org}/{name}") + else: logger.info(f"Release {rel} exists in {github_org}/{name}") return rel - else: - logger.warning(f"Unexpected http error code {resp.code}") - except urllib.error.HTTPError: - logger.warning(f"Did not find release {rel} in {github_org}/{name}") raise ValueError(f"Unable to find {release} in {github_org}/{name}") @@ -251,7 +259,12 @@ def get_target_dir(name: str, ioc_dir: str, release: str) -> str: def clone_repo_tag( - name: str, github_org: str, release: str, deploy_dir: str, dry_run: bool + name: str, + github_org: str, + release: str, + deploy_dir: str, + dry_run: bool, + verbose: bool, ) -> int: """ Create a shallow clone of the git repository in the correct location. @@ -263,22 +276,18 @@ def clone_repo_tag( else: logger.debug(f"Ensure {parent_dir} exists") parent_dir.mkdir(parents=True, exist_ok=True) - # Shell out to git clone - cmd = [ - "git", - "clone", - f"https://github.com/{github_org}/{name}", - "--depth", - "1", - "-b", - release, - deploy_dir, - ] + if dry_run: - logger.debug(f"Dry-run: skip shell command \"{' '.join(cmd)}\"") + logger.debug("Dry-run: skip git clone") return ReturnCode.SUCCESS else: - return subprocess.run(cmd).returncode + return _clone( + name=name, + github_org=github_org, + release=release, + target_dir=deploy_dir, + verbose=verbose, + ).returncode def make_in(deploy_dir: str, dry_run: bool) -> int: @@ -313,6 +322,32 @@ def get_version() -> str: return "unknown.dev" +def _clone( + name: str, + github_org: str, + release: str = "", + working_dir: str = "", + target_dir: str = "", + verbose: bool = False, +) -> subprocess.CompletedProcess: + """ + Clone the repo or raise a subprocess.CalledProcessError + """ + cmd = ["git", "clone", f"git@github.com:{github_org}/{name}", "--depth", "1"] + if release: + cmd.extend(["-b", release]) + if target_dir: + cmd.append(target_dir) + kwds = {"check": True} + if working_dir: + kwds["cwd"] = working_dir + if not verbose: + kwds["stdout"] = subprocess.PIPE + kwds["stderr"] = subprocess.PIPE + logger.debug(f"Calling '{' '.join(cmd)}' with kwargs {kwds}") + return subprocess.run(cmd, **kwds) + + def _main() -> int: """ Thin wrapper of main() for log setup, args handling, and high-level error handling. diff --git a/scripts/ssh-agent-helper b/scripts/ssh-agent-helper new file mode 100644 index 00000000..b78b9632 --- /dev/null +++ b/scripts/ssh-agent-helper @@ -0,0 +1,44 @@ +#!/usr/bin/bash +# +# Helper script for starting the ssh agent if needed and doing an ssh-add -t 12h. +# This will let anyone smoothly run github/ssh related scripts without multiple password prompts. +# An ssh-agent process started by using this script will be automatically closed on logout. +# +# This script is intended to be sourced. +# Sourcing this script lets ssh-agent set the proper environment variables it needs to run properly. +# +# Expected usage: +# +# source ssh-agent-helper +# + +ssh-add -L &> /dev/null +rval=$? +# SSH agent check: return code is 1 if there are no identities, 2 if cannot connect to agent. +# Only start the agent on return code 2, otherwise we can just add our identity. +# On return code 0 we don't have to do anything, the user already has this set up. +# If the user is already forwarding their SSH key via ssh agent forwarding, this +# helpfully returns 0 and nothing needs to be done. +if [ "$rval" -eq 2 ]; then + echo "Starting ssh agent" + # This ssh-agent -s command starts the agent and outputs some environment variable + # set and export commands to stdout that the user must execute in their shell via eval. + # This sets SSH_AGENT_PID to be used to kill the agent later, + # and SSH_AUTH_SOCK to be used to authorise ssh commands via the agent. + eval "$(ssh-agent -s)" &> /dev/null + # If the agent hasn't already been killed, this helper will kill it. + ssh_agent_helper_cleanup() { + if [ -n "${SSH_AGENT_PID}" ]; then + echo "Cleaning up SSH agent" + # Kills the agent and un-sets the environment variables + eval "$(ssh-agent -k)" &> /dev/null + fi + } + # This will ensure that ssh_agent_helper_cleanup runs when the user's shell exits. + trap ssh_agent_helper_cleanup EXIT +fi +if [ "$rval" -gt 0 ]; then + echo "Running ssh-add, may prompt for ssh key password" + # Expire after 12h just in case to avoid infinite key storage + ssh-add -t 12h 2> /dev/null +fi