blob: da783081395ea8084b94cecd94c56d2f91c60e3b [file] [log] [blame]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""ASF Infrastructure Boxer - GitHub/GitBox accounts/teams suite"""
import argparse
import asyncio
import importlib
import json
import os
import sys
import traceback
import aiohttp.web
import yaml
import uuid
import plugins.background
import plugins.basetypes
import plugins.configuration
import plugins.database
import plugins.formdata
import plugins.session
BOXER_VERSION = "0.1.0"
class Server(plugins.basetypes.Server):
"""Main server class, responsible for handling requests and scheduling offloader threads """
def __init__(self, args: argparse.Namespace):
print(
"==== ASF Infra Boxer Suite v/%s starting... ====" % BOXER_VERSION
)
# Load configuration
yml = yaml.safe_load(open(args.config))
self.config = plugins.configuration.Configuration(yml)
self.data = plugins.configuration.InterData()
self.handlers = dict()
self.database = plugins.database.Database(self.config.database)
self.server = None
# Load each URL endpoint
for endpoint_file in os.listdir("endpoints"):
if endpoint_file.endswith(".py"):
endpoint = endpoint_file[:-3]
m = importlib.import_module(f"endpoints.{endpoint}")
if hasattr(m, "register"):
self.handlers[endpoint] = m.__getattribute__("register")(self)
print(f"Registered endpoint /api/{endpoint}")
else:
print(
f"Could not find entry point 'register()' in {endpoint_file}, skipping!"
)
async def handle_request(
self, request: aiohttp.web.BaseRequest
) -> aiohttp.web.Response:
"""Generic handler for all incoming HTTP requests"""
resp: aiohttp.web.Response
# Define response headers first...
headers = {
"Server": "ASF Infra Boxer Suite v/%s" % BOXER_VERSION,
}
# Figure out who is going to handle this request, if any
# We are backwards compatible with the old Lua interface URLs
body_type = "form"
handler = request.path.split("/")[-1]
if handler.endswith(".json"):
body_type = "json"
handler = handler[:-5]
# Parse form data if any
try:
indata = await plugins.formdata.parse_formdata(body_type, request)
except ValueError as e:
return aiohttp.web.Response(headers=headers, status=400, text=str(e))
# Find a handler, or 404
if handler in self.handlers:
session = await plugins.session.get_session(self, request)
try:
# Wait for endpoint response. This is typically JSON in case of success,
# but could be an exception (that needs a traceback) OR
# it could be a custom response, which we just pass along to the client.
output = await self.handlers[handler].exec(self, session, indata)
if session.database:
session.database = None
headers["content-type"] = "application/json"
if output and not isinstance(output, aiohttp.web.Response):
jsout = json.dumps(output, indent=2)
headers["Content-Length"] = str(len(jsout))
return aiohttp.web.Response(headers=headers, status=200, text=jsout)
elif isinstance(output, aiohttp.web.Response):
return output
else:
return aiohttp.web.Response(
headers=headers, status=404, text="Content not found"
)
# If a handler hit an exception, we need to print that exception somewhere,
# either to the web client or stderr:
except: # This is a broad exception on purpose!
if session.database:
session.database = None
exc_type, exc_value, exc_traceback = sys.exc_info()
err = "\n".join(
traceback.format_exception(exc_type, exc_value, exc_traceback)
)
# By default, we print the traceback to the user, for easy debugging.
if self.config.server.traceback:
return aiohttp.web.Response(
headers=headers, status=500, text="API error occurred: \n" + err
)
# If client traceback is disabled, we print it to stderr instead, but leave an
# error ID for the client to report back to the admin. Every line of the traceback
# will have this error ID at the beginning of the line, for easy grepping.
else:
# We only need a short ID here, let's pick 18 chars.
eid = str(uuid.uuid4())[:18]
sys.stderr.write("API Endpoint %s got into trouble (%s): \n" % (request.path, eid))
for line in err.split("\n"):
sys.stderr.write("%s: %s\n" % (eid, line))
return aiohttp.web.Response(
headers=headers, status=500, text="API error occurred. The application journal will have "
"information. Error ID: %s" % eid
)
else:
return aiohttp.web.Response(
headers=headers, status=404, text="API Endpoint not found!"
)
async def server_loop(self, loop: asyncio.AbstractEventLoop): # Note, loop never used.
self.server = aiohttp.web.Server(self.handle_request)
runner = aiohttp.web.ServerRunner(self.server)
await runner.setup()
site = aiohttp.web.TCPSite(
runner, self.config.server.ip, self.config.server.port
)
await site.start()
print(
"==== Serving up Infra goodness at %s:%s ===="
% (self.config.server.ip, self.config.server.port)
)
await plugins.background.run_tasks(self)
def run(self):
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(self.server_loop(loop))
except KeyboardInterrupt:
pass
loop.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--config",
help="Configuration file to load (default: boxer.yaml)",
default="boxer.yaml",
)
cliargs = parser.parse_args()
pubsub_server = Server(cliargs)
pubsub_server.run()