Skip to content

Commit f9c5d50

Browse files
committed
air: add a separate admin tool instead of mixing submit and --delete
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent 8ab0538 commit f9c5d50

2 files changed

Lines changed: 209 additions & 47 deletions

File tree

air-admin.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0
3+
4+
"""CLI tool for administrative operations on AIR reviews"""
5+
6+
import argparse
7+
import configparser
8+
import json
9+
import sys
10+
from pathlib import Path
11+
import requests
12+
13+
14+
# ANSI color codes
15+
class Colors:
16+
RESET = '\033[0m'
17+
RED = '\033[91m'
18+
GREEN = '\033[32m'
19+
YELLOW = '\033[93m'
20+
BLUE = '\033[94m'
21+
CYAN = '\033[96m'
22+
BOLD = '\033[1m'
23+
24+
25+
def colorize(text: str, color: str) -> str:
26+
"""Add color to text if output is a TTY
27+
28+
Args:
29+
text: Text to colorize
30+
color: Color code
31+
32+
Returns:
33+
Colored text if stdout is a TTY, otherwise plain text
34+
"""
35+
if sys.stdout.isatty():
36+
return f"{color}{text}{Colors.RESET}"
37+
return text
38+
39+
40+
def load_config() -> configparser.ConfigParser:
41+
"""Load configuration from ~/.air.conf
42+
43+
Returns:
44+
ConfigParser instance (may be empty if file doesn't exist)
45+
"""
46+
config = configparser.ConfigParser()
47+
config_path = Path.home() / '.air.conf'
48+
49+
if config_path.exists():
50+
try:
51+
config.read(config_path)
52+
except Exception as e:
53+
print(f"Warning: Failed to read config file {config_path}: {e}", file=sys.stderr)
54+
55+
return config
56+
57+
58+
def delete_review(url: str, token: str, review_id: str) -> bool:
59+
"""Delete a review (superuser only)
60+
61+
Args:
62+
url: AIR service URL
63+
token: API token (must be superuser)
64+
review_id: Review ID to delete
65+
66+
Returns:
67+
True if successful
68+
"""
69+
api_url = f"{url}/api/review"
70+
params = {
71+
'id': review_id,
72+
'token': token,
73+
}
74+
75+
try:
76+
response = requests.delete(api_url, params=params, timeout=30)
77+
response.raise_for_status()
78+
return response.json().get('success', False)
79+
except requests.exceptions.RequestException as e:
80+
print(f"Error deleting review: {e}", file=sys.stderr)
81+
sys.exit(1)
82+
except json.JSONDecodeError as e:
83+
print(f"Error parsing response: {e}", file=sys.stderr)
84+
sys.exit(1)
85+
86+
87+
def create_token(url: str, token: str, name: str) -> str:
88+
"""Create a new token (superuser only)
89+
90+
Args:
91+
url: AIR service URL
92+
token: API token (must be superuser)
93+
name: Human-readable name for the new token
94+
95+
Returns:
96+
The newly created token string
97+
"""
98+
api_url = f"{url}/api/token"
99+
payload = {
100+
'token': token,
101+
'name': name,
102+
}
103+
104+
try:
105+
response = requests.post(api_url, json=payload, timeout=30)
106+
response.raise_for_status()
107+
data = response.json()
108+
return data.get('token')
109+
except requests.exceptions.RequestException as e:
110+
print(f"Error creating token: {e}", file=sys.stderr)
111+
sys.exit(1)
112+
except json.JSONDecodeError as e:
113+
print(f"Error parsing response: {e}", file=sys.stderr)
114+
sys.exit(1)
115+
116+
117+
def main():
118+
# Load config file first to get defaults
119+
config = load_config()
120+
121+
parser = argparse.ArgumentParser(
122+
description='Administrative operations for AIR reviews',
123+
formatter_class=argparse.RawDescriptionHelpFormatter,
124+
epilog="""
125+
Examples:
126+
# Delete a review
127+
%(prog)s --url https://example.com/air --token mytoken --delete abc-123-def
128+
129+
# Create a new token
130+
%(prog)s --url https://example.com/air --token mytoken --create-token "User Name"
131+
132+
Configuration file:
133+
You can create ~/.air.conf to avoid repeating common parameters:
134+
135+
[air]
136+
url = https://example.com/air
137+
token = mytoken
138+
139+
Command-line arguments always override config file values.
140+
"""
141+
)
142+
143+
parser.add_argument('--url',
144+
help='AIR service URL (e.g., https://example.com/air)')
145+
parser.add_argument('--token',
146+
help='API authentication token (required for admin operations)')
147+
parser.add_argument('--delete', metavar='REVIEW_ID',
148+
help='Delete the specified review (requires superuser token)')
149+
parser.add_argument('--create-token', metavar='NAME',
150+
help='Create a new token with the given name (requires superuser token)')
151+
152+
args = parser.parse_args()
153+
154+
# Fill in missing arguments from config file
155+
if config.has_section('air'):
156+
if args.url is None and config.has_option('air', 'url'):
157+
args.url = config.get('air', 'url')
158+
if args.token is None and config.has_option('air', 'token'):
159+
args.token = config.get('air', 'token')
160+
161+
# Convert empty strings to None (allows unsetting config values)
162+
if args.token == '':
163+
args.token = None
164+
165+
# Validate that we have URL
166+
if not args.url:
167+
parser.error('--url is required (either via command-line or ~/.air.conf)')
168+
169+
args.url = args.url.rstrip('/')
170+
171+
# Check that at least one operation is specified
172+
if not args.delete and not getattr(args, 'create_token', None):
173+
parser.error('No operation specified. Use --delete REVIEW_ID or --create-token NAME')
174+
175+
# Handle --delete operation
176+
if args.delete:
177+
if not args.token:
178+
parser.error('--delete requires --token (must be superuser)')
179+
180+
review_id = args.delete
181+
print(f"Deleting review {review_id}...")
182+
success = delete_review(args.url, args.token, review_id)
183+
if success:
184+
print(colorize(f"Review {review_id} deleted successfully", Colors.GREEN))
185+
else:
186+
print(colorize("Failed to delete review", Colors.RED), file=sys.stderr)
187+
sys.exit(1)
188+
return
189+
190+
# Handle --create-token operation
191+
if getattr(args, 'create_token', None):
192+
if not args.token:
193+
parser.error('--create-token requires --token (must be superuser)')
194+
195+
name = args.create_token
196+
print(f"Creating token for '{name}'...")
197+
new_token = create_token(args.url, args.token, name)
198+
if new_token:
199+
print(colorize("Token created successfully", Colors.GREEN))
200+
print(f"Name: {name}")
201+
print(f"Token: {colorize(new_token, Colors.CYAN)}")
202+
else:
203+
print(colorize("Failed to create token", Colors.RED), file=sys.stderr)
204+
sys.exit(1)
205+
return
206+
207+
208+
if __name__ == '__main__':
209+
main()

air-submit.py

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -173,35 +173,6 @@ def get_review_status(url: str, token: Optional[str], review_id: str,
173173
sys.exit(1)
174174

175175

176-
def delete_review(url: str, token: str, review_id: str) -> bool:
177-
"""Delete a review (superuser only)
178-
179-
Args:
180-
url: AIR service URL
181-
token: API token (must be superuser)
182-
review_id: Review ID to delete
183-
184-
Returns:
185-
True if successful
186-
"""
187-
api_url = f"{url}/api/review"
188-
params = {
189-
'id': review_id,
190-
'token': token,
191-
}
192-
193-
try:
194-
response = requests.delete(api_url, params=params, timeout=30)
195-
response.raise_for_status()
196-
return response.json().get('success', False)
197-
except requests.exceptions.RequestException as e:
198-
print(f"Error deleting review: {e}", file=sys.stderr)
199-
sys.exit(1)
200-
except json.JSONDecodeError as e:
201-
print(f"Error parsing response: {e}", file=sys.stderr)
202-
sys.exit(1)
203-
204-
205176
def set_feedback(url: str, token: Optional[str], review_id: str, feedback: str) -> bool:
206177
"""Set feedback for a review
207178
@@ -362,8 +333,6 @@ def main():
362333
help='Git commit hash or range (e.g., abc123 or abc123..def456)')
363334
parser.add_argument('--review-id', metavar='ID',
364335
help='Existing review ID to check (skip submission)')
365-
parser.add_argument('--delete', action='store_true',
366-
help='Delete the specified review (requires --review-id and superuser token)')
367336
parser.add_argument('--feedback', choices=['emailed', 'false-positive', 'false-negative'],
368337
help='Set feedback for a review (requires --review-id)')
369338
parser.add_argument('patches', nargs='*', metavar='PATCH_FILE',
@@ -399,22 +368,6 @@ def main():
399368

400369
args.url = args.url.rstrip('/')
401370

402-
# Handle --delete operation
403-
if args.delete:
404-
if not args.review_id:
405-
parser.error('--delete requires --review-id')
406-
if not args.token:
407-
parser.error('--delete requires --token (must be superuser)')
408-
409-
print(f"Deleting review {args.review_id}...")
410-
success = delete_review(args.url, args.token, args.review_id)
411-
if success:
412-
print(colorize(f"Review {args.review_id} deleted successfully", Colors.GREEN))
413-
else:
414-
print(colorize("Failed to delete review", Colors.RED), file=sys.stderr)
415-
sys.exit(1)
416-
return
417-
418371
# Handle --feedback operation
419372
if args.feedback:
420373
if not args.review_id:

0 commit comments

Comments
 (0)