Skip to content

Commit

Permalink
Merge pull request #5 from VallariAg/kill
Browse files Browse the repository at this point in the history
Add github oauth & teuthology-kill function
Reviewed-by: Kamoltat Sirivadhna <[email protected]>
  • Loading branch information
kamoltat authored Mar 13, 2023
2 parents a7e97f6 + ae5ed4b commit 6915413
Show file tree
Hide file tree
Showing 16 changed files with 406 additions and 80 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RUN apt-get update && \
apt-get clean all

COPY .teuthology.yaml /root

COPY .env* /teuthology_api/
WORKDIR /teuthology_api
COPY requirements.txt /teuthology_api/
RUN pip3 install -r requirements.txt
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ A REST API to execute [teuthology commands](https://docs.ceph.com/projects/teuth

The documentation can be accessed at http://localhost:8082/docs after running the application.

Note: To run commands, authenticate by visiting `http://localhost:8082/login` through browser and follow the github authentication steps (this stores the auth token in browser cookies).

### Route `/`

```
curl http://localhost:8082/
```
Returns `{"root": "success"}`.
Returns `{"root": "success", "session": { <authentication details> }}`.

### Route `/suite`

Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
fastapi
uvicorn[standard]
gunicorn
httpx
itsdangerous
# Temporarily, using teuthology without monkey patching the thread
git+https://github.com/VallariAg/teuthology@teuth-api#egg=teuthology[test]
# Original: git+https://github.com/ceph/teuthology#egg=teuthology[test]
17 changes: 14 additions & 3 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
from functools import lru_cache
from typing import List
import os
from pydantic import BaseSettings


class APISettings(BaseSettings):
"""
Class for API settings.
"""
PADDLES_URL: str = "http://paddles:8080"
# TODO: team names need to be changed below when created
admin_team: str = "ceph" # ceph's github team with *sudo* access to sepia
teuth_team: str = "teuth" # ceph's github team with access to sepia

class Config:
"""
Class for Config.
"""
# pylint: disable=too-few-public-methods
env_file = ".env"
env_file_encoding = 'utf-8'


@lru_cache()
def get_api_settings() -> APISettings:
"""
Returns the API settings.
"""
return APISettings() # reads variables from environment


settings = get_api_settings()
settings = get_api_settings()
36 changes: 31 additions & 5 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import logging
from fastapi import FastAPI
import os
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from routes import suite, kill, login, logout
from dotenv import load_dotenv

from routes import suite
load_dotenv()

DEPLOYMENT = os.getenv('DEPLOYMENT')
SESSION_SECRET_KEY = os.getenv('SESSION_SECRET_KEY')

log = logging.getLogger(__name__)
app = FastAPI()


@app.get("/")
def read_root():
return {"root": "success"}
def read_root(request: Request):
"""
GET route for root.
"""
return {
"root": "success",
"session": request.session.get('user', None)
}

if DEPLOYMENT == 'development':
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET_KEY)
app.include_router(suite.router)
app.include_router(kill.router)
app.include_router(login.router)
app.include_router(logout.router)
29 changes: 29 additions & 0 deletions src/routes/kill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging
from fastapi import APIRouter, Depends, Request
from services.kill import run
from services.helpers import get_token
from schemas.kill import KillArgs

log = logging.getLogger(__name__)

router = APIRouter(
prefix="/kill",
tags=["kill"],
)

@router.post("/", status_code=200)
def create_run(
request: Request,
args: KillArgs,
logs: bool = False,
access_token: str = Depends(get_token),
):
"""
POST route for killing a run or a job.
Note: I needed to put `request` before `args`
or else it will SyntaxError: non-dafault
argument follows default argument error.
"""
args = args.dict(by_alias=True)
return run(args, logs, access_token, request)
81 changes: 81 additions & 0 deletions src/routes/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import logging
import os
from fastapi import APIRouter, HTTPException, Request
from starlette.responses import RedirectResponse
from dotenv import load_dotenv
import httpx

load_dotenv()

GH_CLIENT_ID = os.getenv('GH_CLIENT_ID')
GH_CLIENT_SECRET = os.getenv('GH_CLIENT_SECRET')
GH_AUTHORIZATION_BASE_URL = os.getenv('GH_AUTHORIZATION_BASE_URL')
GH_TOKEN_URL = os.getenv('GH_TOKEN_URL')
GH_FETCH_MEMBERSHIP_URL = os.getenv('GH_FETCH_MEMBERSHIP_URL')

log = logging.getLogger(__name__)
router = APIRouter(
prefix="/login",
tags=["login"],
responses={404: {"description": "Not found"}},
)


@router.get("/", status_code=200)
async def github_login():
"""
GET route for /login, (If first time) will redirect to github login page
where you should authorize the app to gain access.
"""
scope = 'read:org'
return RedirectResponse(
f'{GH_AUTHORIZATION_BASE_URL}?client_id={GH_CLIENT_ID}&scope={scope}',
status_code=302
)

@router.get("/callback", status_code=200)
async def handle_callback(code: str, request: Request):
"""
Call back route after user login & authorize the app
for access.
"""
params = {
'client_id': GH_CLIENT_ID,
'client_secret': GH_CLIENT_SECRET,
'code': code
}
headers = {'Accept': 'application/json'}
async with httpx.AsyncClient() as client:
response_token = await client.post(url=GH_TOKEN_URL, params=params, headers=headers)
log.info(response_token.json())
response_token_dic = dict(response_token.json())
token = response_token_dic.get('access_token')
if response_token_dic.get('error') or not token:
log.error("The code is incorrect or expired.")
raise HTTPException(
status_code=401, detail="The code is incorrect or expired.")
headers = {'Authorization': 'token ' + token}
response_org = await client.get(url=GH_FETCH_MEMBERSHIP_URL, headers=headers)
log.info(response_org.json())
if response_org.status_code == 404:
log.error("User is not part of the Ceph Organization")
raise HTTPException(
status_code=404,
detail="User is not part of the Ceph Organization, please contact <admin>"
)
if response_org.status_code == 403:
log.error("The application doesn't have permission to view github org")
raise HTTPException(
status_code=403,
detail="The application doesn't have permission to view github org"
)
response_org_dic = dict(response_org.json())
data = {
"id": response_org_dic.get('user', {}).get('id'),
"username": response_org_dic.get('user', {}).get('login'),
"state": response_org_dic.get('state'),
"role": response_org_dic.get('role'),
"access_token": token,
}
request.session['user'] = data
return RedirectResponse(url='/')
24 changes: 24 additions & 0 deletions src/routes/logout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging
from fastapi import APIRouter, HTTPException, Request

log = logging.getLogger(__name__)
router = APIRouter(
prefix="/logout",
tags=["logout"],
responses={404: {"description": "Not found"}},
)

@router.get('/', status_code=200)
def logout(request: Request):
"""
GET route for logging out.
"""
user = request.session.get('user')
if user:
request.session.pop('user', None)
return {"logout": "success"}
log.warning("No session found, probably already logged out.")
raise HTTPException(
status_code=204,
detail="No session found, probably already logged out."
)
17 changes: 9 additions & 8 deletions src/routes/suite.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Depends
from services.suite import run
from services.helpers import get_token
from schemas.suite import SuiteArgs
import logging

Expand All @@ -12,10 +13,10 @@
)

@router.post("/", status_code=200)
def create_run(args: SuiteArgs, dry_run: bool = False, logs: bool = False):
try:
args = args.dict(by_alias=True)
results = run(args, dry_run, logs)
return results
except Exception as exc:
raise HTTPException(status_code=404, detail=repr(exc))
def create_run(args: SuiteArgs,
access_token: str = Depends(get_token),
dry_run: bool = False,
logs: bool = False
):
args = args.dict(by_alias=True)
return run(args, dry_run, logs, access_token)
6 changes: 5 additions & 1 deletion src/schemas/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from typing import Union, Optional
from typing import Union
from pydantic import BaseModel, Field


class BaseArgs(BaseModel):
# pylint: disable=too-few-public-methods
"""
Class for Base Args.
"""
dry_run: Union[bool, None] = Field(default=False, alias="--dry-run")
non_interactive: Union[bool, None] = Field(default=False, alias="--non-interactive")
verbose: Union[int, None] = Field(default=1, alias="--verbose")
Expand Down
18 changes: 18 additions & 0 deletions src/schemas/kill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Union
from pydantic import Field

from .base import BaseArgs


class KillArgs(BaseArgs):
# pylint: disable=too-few-public-methods
"""
Class for KillArgs.
"""
owner: Union[str, None] = Field(default=None, alias="--owner")
run: Union[str, None] = Field(default=None, alias="--run")
preserve_queue: Union[bool, None] = Field(default=None, alias="--preserve-queue")
job: Union[list, None] = Field(default=None, alias="--job")
jobspec: Union[str, None] = Field(default=None, alias="--jobspec")
machine_type: Union[str, None] = Field(default='default', alias="--machine-type")
archive: Union[str, None] = Field(default=None, alias="--archive")
6 changes: 5 additions & 1 deletion src/schemas/schedule.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from typing import Union, Optional
from typing import Union
from pydantic import Field

from .base import BaseArgs


class SchedulerArgs(BaseArgs):
# pylint: disable=too-few-public-methods
"""
Class for SchedulerArgs.
"""
owner: Union[str, None] = Field(default=None, alias="--owner")
num: Union[str, None] = Field(default='1', alias="--num")
priority: Union[str, None] = Field(default='70', alias="--priority")
Expand Down
Loading

0 comments on commit 6915413

Please sign in to comment.