195 lines
6.6 KiB
Python
Executable File
195 lines
6.6 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
import argparse
|
|
from http.cookies import SimpleCookie
|
|
from http.cookiejar import Cookie, LWPCookieJar
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from socketserver import ThreadingMixIn
|
|
import json
|
|
import tempfile
|
|
import os
|
|
from osclib import common
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from urllib.parse import urlparse
|
|
|
|
# Available in python 3.7.
|
|
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
|
pass
|
|
|
|
class RequestHandler(BaseHTTPRequestHandler):
|
|
COOKIE_NAME = 'openSUSE_session' # Both OBS and IBS.
|
|
POST_ACTIONS = ['select']
|
|
|
|
def do_GET(self):
|
|
if self.path != '/':
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/plain')
|
|
self.end_headers()
|
|
|
|
self.write_string('namespace: {}\n'.format(common.NAME))
|
|
self.write_string('name: {}\n'.format('OBS Operator'))
|
|
self.write_string('version: {}\n'.format(common.VERSION))
|
|
|
|
def do_POST(self):
|
|
action = self.path.lstrip('/')
|
|
if action not in self.POST_ACTIONS:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
|
|
data = self.data_parse()
|
|
user = data.get('user')
|
|
apiurl = self.apiurl_get()
|
|
if not data or not user or not apiurl:
|
|
self.send_response(400)
|
|
self.end_headers()
|
|
return
|
|
if self.debug:
|
|
print('data: {}'.format(data))
|
|
print('apiurl: {}'.format(apiurl))
|
|
|
|
session = self.session_get()
|
|
if not session:
|
|
self.send_response(401)
|
|
self.end_headers()
|
|
return
|
|
if self.debug:
|
|
print('session: {}'.format(session))
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/plain')
|
|
self.send_header('Access-Control-Allow-Credentials', 'true')
|
|
self.send_header('Access-Control-Allow-Origin', self.headers.get('Origin'))
|
|
self.end_headers()
|
|
|
|
with tempfile.NamedTemporaryFile() as cookiejar_file:
|
|
with tempfile.NamedTemporaryFile() as oscrc_file:
|
|
self.oscrc_create(oscrc_file, apiurl, cookiejar_file, user)
|
|
self.cookiejar_create(cookiejar_file, session)
|
|
|
|
func = getattr(self, 'handle_{}'.format(action))
|
|
commands = func(data)
|
|
for command in commands:
|
|
self.write_string('$ {}\n'.format(' '.join(command)))
|
|
if not self.execute(oscrc_file, command):
|
|
self.write_string('failed')
|
|
break
|
|
|
|
def data_parse(self):
|
|
data = self.rfile.read(int(self.headers['Content-Length']))
|
|
return json.loads(data.decode('utf-8'))
|
|
|
|
def apiurl_get(self):
|
|
if self.apiurl:
|
|
return self.apiurl
|
|
|
|
origin = self.headers.get('Origin')
|
|
if not origin:
|
|
return None
|
|
|
|
# Strip port if present.
|
|
domain = urlparse(origin).netloc.split(':', 2)[0]
|
|
if '.' not in domain:
|
|
return None
|
|
|
|
# Remove first subdomain and replace with api subdomain.
|
|
domain_parent = '.'.join(domain.split('.')[1:])
|
|
return 'https://api.{}'.format(domain_parent)
|
|
|
|
def session_get(self):
|
|
if self.session:
|
|
return self.session
|
|
else:
|
|
cookie = self.headers.get('Cookie')
|
|
if cookie:
|
|
cookie = SimpleCookie(cookie)
|
|
if self.COOKIE_NAME in cookie:
|
|
return cookie[self.COOKIE_NAME].value
|
|
|
|
return None
|
|
|
|
def oscrc_create(self, oscrc_file, apiurl, cookiejar_file, user):
|
|
oscrc_file.write('\n'.join([
|
|
'[general]',
|
|
'apiurl = {}'.format(apiurl),
|
|
'cookiejar = {}'.format(cookiejar_file.name),
|
|
'staging.color = 0',
|
|
'[{}]'.format(apiurl),
|
|
'user = {}'.format(user),
|
|
'pass = invalid',
|
|
'',
|
|
]).encode('utf-8'))
|
|
oscrc_file.flush()
|
|
|
|
# In order to avoid osc clearing the cookie file the modified time of
|
|
# the oscrc file must be set further into the past.
|
|
# if int(round(config_mtime)) > int(os.stat(cookie_file).st_mtime):
|
|
recent_past = time.time() - 3600
|
|
os.utime(oscrc_file.name, (recent_past, recent_past))
|
|
|
|
def cookiejar_create(self, cookiejar_file, session):
|
|
cookie_jar = LWPCookieJar(cookiejar_file.name)
|
|
cookie_jar.set_cookie(Cookie(0, self.COOKIE_NAME, session,
|
|
None, False,
|
|
'', False, True,
|
|
'/', True,
|
|
True,
|
|
None, None, None, None, {}))
|
|
cookie_jar.save()
|
|
cookiejar_file.flush()
|
|
|
|
def execute(self, oscrc_file, command):
|
|
env = os.environ
|
|
env['OSC_CONFIG'] = oscrc_file.name
|
|
|
|
# Would be preferrable to stream incremental output, but python http
|
|
# server does not seem to support this easily.
|
|
result = subprocess.run(command, env=env, stdout=self.wfile, stderr=self.wfile)
|
|
return result.returncode == 0
|
|
|
|
def write_string(self, string):
|
|
self.wfile.write(string.encode('utf-8'))
|
|
|
|
def staging_command(self, project, subcommand):
|
|
return ['osc', 'staging', '-p', project, subcommand]
|
|
|
|
def handle_select(self, data):
|
|
for staging, requests in data['selection'].items():
|
|
command = self.staging_command(data['project'], 'select')
|
|
if 'move' in data and data['move']:
|
|
command.append('--move')
|
|
command.append(staging)
|
|
command.extend(requests)
|
|
yield command
|
|
|
|
def main(args):
|
|
RequestHandler.apiurl = args.apiurl
|
|
RequestHandler.session = args.session
|
|
RequestHandler.debug = args.debug
|
|
|
|
with ThreadedHTTPServer((args.host, args.port), RequestHandler) as httpd:
|
|
print('listening on {}:{}'.format(args.host, args.port))
|
|
httpd.serve_forever()
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='OBS Operator server used to perform staging operations.')
|
|
parser.set_defaults(func=main)
|
|
|
|
parser.add_argument('--host', default='', help='host name to which to bind')
|
|
parser.add_argument('--port', type=int, default=8080, help='port number to which to bind')
|
|
parser.add_argument('-A', '--apiurl',
|
|
help='OBS instance API URL to use instead of basing from request origin')
|
|
parser.add_argument('--session',
|
|
help='session cookie value to use instead of any passed cookie')
|
|
parser.add_argument('-d', '--debug', action='store_true',
|
|
help='print debugging information')
|
|
|
|
args = parser.parse_args()
|
|
sys.exit(args.func(args))
|