Files
agent-workbench/scripts/control.py
T

95 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Authenticated control client. No credentials are accepted on the command line."""
import argparse
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import urllib.error
import urllib.request
class APIError(RuntimeError):
def __init__(self,status):
self.status=status
super().__init__('Control API returned HTTP %s' % status)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,req,fp,code,msg,headers,newurl):
return None
class Client:
def __init__(self, config):
self.config = config
def call(self, path, data=None, method=None):
if not path.startswith('/api/') or '\n' in path:
raise ValueError('Only control API paths are accepted')
if self.config.get('transport') == 'ssh':
cfg = self.config
command = ['ssh', '-i', str(Path(cfg['identity_file']).expanduser()),
'-o', 'IdentitiesOnly=yes', '-o', 'BatchMode=yes',
'-o', 'StrictHostKeyChecking=yes', '-o', 'ConnectTimeout=10',
cfg['ssh_host'], 'python3 -X utf8 ' + shlex.quote(cfg['remote_client']) +
' --config ' + shlex.quote(cfg['remote_config']) + ' rpc']
result = subprocess.run(command, input=json.dumps({'path': path, 'data': data, 'method': method}),
capture_output=True, text=True, encoding='utf-8', errors='strict', timeout=45)
if result.returncode:
try:
problem=json.loads(result.stdout)
except ValueError:
problem={}
if set(problem)=={'api_error'} and isinstance(problem['api_error'],int):
raise APIError(problem['api_error'])
raise RuntimeError('SSH control request failed; check connection and remote configuration')
return json.loads(result.stdout)
cfg = self.config
url = cfg.get('url') or ('http://%s:%s' % (cfg['bind'], cfg['port']))
token_path = Path(cfg['token_file']).expanduser()
if os.name != 'nt' and token_path.stat().st_mode & 0o077:
raise ValueError('Token file must not be readable by group or other users')
token = token_path.read_text(encoding='utf-8').strip()
if not token:
raise ValueError('Empty token file')
body = None if data is None else json.dumps(data).encode()
request = urllib.request.Request(url.rstrip('/') + path, data=body,
headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json',
'X-CSRF-Token': '1'}, method=method or ('GET' if data is None else 'POST'))
try:
opener=urllib.request.build_opener(NoRedirect(),urllib.request.ProxyHandler({}))
with opener.open(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as exc:
status = exc.code
exc.close()
raise APIError(status) from None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--config', required=True)
parser.add_argument('action', choices=['state', 'rpc'])
args = parser.parse_args()
client = Client(json.loads(Path(args.config).read_text(encoding='utf-8-sig')))
if args.action == 'state':
result = client.call('/api/state')
else:
payload = json.load(sys.stdin)
result = client.call(payload['path'], payload.get('data'), payload.get('method'))
print(json.dumps(result, ensure_ascii=True))
if __name__ == '__main__':
try:
main()
except APIError as exc:
print(json.dumps({'api_error':exc.status}))
sys.exit(2)
except Exception as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)