arch-ops-server 0.1.3__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arch_ops_server/__init__.py +87 -1
- arch_ops_server/config.py +361 -0
- arch_ops_server/logs.py +345 -0
- arch_ops_server/mirrors.py +397 -0
- arch_ops_server/news.py +288 -0
- arch_ops_server/pacman.py +985 -0
- arch_ops_server/server.py +1257 -61
- arch_ops_server/system.py +307 -0
- arch_ops_server-3.0.0.dist-info/METADATA +250 -0
- arch_ops_server-3.0.0.dist-info/RECORD +16 -0
- arch_ops_server-0.1.3.dist-info/METADATA +0 -133
- arch_ops_server-0.1.3.dist-info/RECORD +0 -11
- {arch_ops_server-0.1.3.dist-info → arch_ops_server-3.0.0.dist-info}/WHEEL +0 -0
- {arch_ops_server-0.1.3.dist-info → arch_ops_server-3.0.0.dist-info}/entry_points.txt +0 -0
arch_ops_server/logs.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-only OR MIT
|
|
2
|
+
"""
|
|
3
|
+
Pacman transaction log parsing module.
|
|
4
|
+
Parses and analyzes pacman transaction logs for troubleshooting and auditing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
|
|
13
|
+
from .utils import (
|
|
14
|
+
IS_ARCH,
|
|
15
|
+
create_error_response,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Pacman log file path
|
|
21
|
+
PACMAN_LOG = "/var/log/pacman.log"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_log_line(line: str) -> Optional[Dict[str, Any]]:
|
|
25
|
+
"""
|
|
26
|
+
Parse a single line from pacman log.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
line: Log line to parse
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dict with parsed data or None if not a transaction line
|
|
33
|
+
"""
|
|
34
|
+
# Format: [YYYY-MM-DD HH:MM] [ACTION] package (version)
|
|
35
|
+
match = re.match(
|
|
36
|
+
r'\[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]\s+\[(\w+)\]\s+(.+)',
|
|
37
|
+
line
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if not match:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
date_str, time_str, action, details = match.groups()
|
|
44
|
+
timestamp = f"{date_str}T{time_str}:00"
|
|
45
|
+
|
|
46
|
+
# Parse package details
|
|
47
|
+
# Format: "package_name (version)" or "package_name (old -> new)"
|
|
48
|
+
pkg_match = re.match(r'(\S+)\s+\((.+)\)', details)
|
|
49
|
+
|
|
50
|
+
if pkg_match:
|
|
51
|
+
package = pkg_match.group(1)
|
|
52
|
+
version_info = pkg_match.group(2)
|
|
53
|
+
else:
|
|
54
|
+
# Some log lines don't have version info
|
|
55
|
+
package = details
|
|
56
|
+
version_info = ""
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"timestamp": timestamp,
|
|
60
|
+
"action": action,
|
|
61
|
+
"package": package,
|
|
62
|
+
"version_info": version_info,
|
|
63
|
+
"raw_line": line.strip()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def get_transaction_history(
|
|
68
|
+
limit: int = 50,
|
|
69
|
+
transaction_type: str = "all"
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
"""
|
|
72
|
+
Get recent package transactions from pacman log.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
limit: Maximum number of transactions to return (default 50)
|
|
76
|
+
transaction_type: Filter by type - install/remove/upgrade/all (default all)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dict with transaction history
|
|
80
|
+
"""
|
|
81
|
+
if not IS_ARCH:
|
|
82
|
+
return create_error_response(
|
|
83
|
+
"NotSupported",
|
|
84
|
+
"This feature is only available on Arch Linux"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
logger.info(f"Getting transaction history (limit={limit}, type={transaction_type})")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
pacman_log = Path(PACMAN_LOG)
|
|
91
|
+
|
|
92
|
+
if not pacman_log.exists():
|
|
93
|
+
return create_error_response(
|
|
94
|
+
"NotFound",
|
|
95
|
+
f"Pacman log file not found at {PACMAN_LOG}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
transactions = []
|
|
99
|
+
valid_actions = {
|
|
100
|
+
"all": ["installed", "upgraded", "removed", "downgraded", "reinstalled"],
|
|
101
|
+
"install": ["installed"],
|
|
102
|
+
"remove": ["removed"],
|
|
103
|
+
"upgrade": ["upgraded", "downgraded", "reinstalled"]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
actions_to_match = valid_actions.get(transaction_type, valid_actions["all"])
|
|
107
|
+
|
|
108
|
+
# Read log file from end (most recent first)
|
|
109
|
+
with open(pacman_log, 'r') as f:
|
|
110
|
+
lines = f.readlines()
|
|
111
|
+
|
|
112
|
+
# Process in reverse order for most recent first
|
|
113
|
+
for line in reversed(lines):
|
|
114
|
+
if len(transactions) >= limit:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
parsed = parse_log_line(line)
|
|
118
|
+
if parsed and parsed["action"].lower() in actions_to_match:
|
|
119
|
+
transactions.append(parsed)
|
|
120
|
+
|
|
121
|
+
logger.info(f"Found {len(transactions)} transactions")
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"count": len(transactions),
|
|
125
|
+
"transaction_type": transaction_type,
|
|
126
|
+
"transactions": transactions
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Failed to parse transaction history: {e}")
|
|
131
|
+
return create_error_response(
|
|
132
|
+
"LogParseError",
|
|
133
|
+
f"Failed to parse transaction history: {str(e)}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def find_when_installed(package_name: str) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Find when a package was first installed and its upgrade history.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
package_name: Name of the package to search for
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dict with installation date and upgrade history
|
|
146
|
+
"""
|
|
147
|
+
if not IS_ARCH:
|
|
148
|
+
return create_error_response(
|
|
149
|
+
"NotSupported",
|
|
150
|
+
"This feature is only available on Arch Linux"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
logger.info(f"Finding installation history for package: {package_name}")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
pacman_log = Path(PACMAN_LOG)
|
|
157
|
+
|
|
158
|
+
if not pacman_log.exists():
|
|
159
|
+
return create_error_response(
|
|
160
|
+
"NotFound",
|
|
161
|
+
f"Pacman log file not found at {PACMAN_LOG}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
first_install = None
|
|
165
|
+
upgrades = []
|
|
166
|
+
removals = []
|
|
167
|
+
|
|
168
|
+
with open(pacman_log, 'r') as f:
|
|
169
|
+
for line in f:
|
|
170
|
+
parsed = parse_log_line(line)
|
|
171
|
+
if not parsed or parsed["package"] != package_name:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
action = parsed["action"].lower()
|
|
175
|
+
|
|
176
|
+
if action == "installed":
|
|
177
|
+
if first_install is None:
|
|
178
|
+
first_install = parsed
|
|
179
|
+
elif action in ["upgraded", "downgraded", "reinstalled"]:
|
|
180
|
+
upgrades.append(parsed)
|
|
181
|
+
elif action == "removed":
|
|
182
|
+
removals.append(parsed)
|
|
183
|
+
|
|
184
|
+
if first_install is None:
|
|
185
|
+
return create_error_response(
|
|
186
|
+
"NotFound",
|
|
187
|
+
f"No installation record found for package: {package_name}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
logger.info(f"Package {package_name}: installed {first_install['timestamp']}, {len(upgrades)} upgrades, {len(removals)} removals")
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"package": package_name,
|
|
194
|
+
"first_installed": first_install,
|
|
195
|
+
"upgrade_count": len(upgrades),
|
|
196
|
+
"upgrades": upgrades,
|
|
197
|
+
"removal_count": len(removals),
|
|
198
|
+
"removals": removals,
|
|
199
|
+
"currently_removed": len(removals) > 0 and (not upgrades or removals[-1]["timestamp"] > upgrades[-1]["timestamp"])
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Failed to find installation history: {e}")
|
|
204
|
+
return create_error_response(
|
|
205
|
+
"LogParseError",
|
|
206
|
+
f"Failed to find installation history: {str(e)}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def find_failed_transactions() -> Dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Find failed package transactions in pacman log.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dict with failed transaction information
|
|
216
|
+
"""
|
|
217
|
+
if not IS_ARCH:
|
|
218
|
+
return create_error_response(
|
|
219
|
+
"NotSupported",
|
|
220
|
+
"This feature is only available on Arch Linux"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
logger.info("Searching for failed transactions")
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
pacman_log = Path(PACMAN_LOG)
|
|
227
|
+
|
|
228
|
+
if not pacman_log.exists():
|
|
229
|
+
return create_error_response(
|
|
230
|
+
"NotFound",
|
|
231
|
+
f"Pacman log file not found at {PACMAN_LOG}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
failed_transactions = []
|
|
235
|
+
error_keywords = ["error", "failed", "warning", "could not", "unable to", "conflict"]
|
|
236
|
+
|
|
237
|
+
with open(pacman_log, 'r') as f:
|
|
238
|
+
for line in f:
|
|
239
|
+
line_lower = line.lower()
|
|
240
|
+
|
|
241
|
+
# Check for error indicators
|
|
242
|
+
if any(keyword in line_lower for keyword in error_keywords):
|
|
243
|
+
# Extract timestamp if available
|
|
244
|
+
timestamp_match = re.match(r'\[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]', line)
|
|
245
|
+
timestamp = ""
|
|
246
|
+
if timestamp_match:
|
|
247
|
+
timestamp = f"{timestamp_match.group(1)}T{timestamp_match.group(2)}:00"
|
|
248
|
+
|
|
249
|
+
# Extract severity
|
|
250
|
+
severity = "error" if "error" in line_lower or "failed" in line_lower else "warning"
|
|
251
|
+
|
|
252
|
+
failed_transactions.append({
|
|
253
|
+
"timestamp": timestamp,
|
|
254
|
+
"severity": severity,
|
|
255
|
+
"message": line.strip()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
# Limit to most recent 100 failures
|
|
259
|
+
failed_transactions = failed_transactions[-100:]
|
|
260
|
+
|
|
261
|
+
logger.info(f"Found {len(failed_transactions)} failed/warning entries")
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
"count": len(failed_transactions),
|
|
265
|
+
"has_failures": len(failed_transactions) > 0,
|
|
266
|
+
"failures": failed_transactions
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"Failed to search for failures: {e}")
|
|
271
|
+
return create_error_response(
|
|
272
|
+
"LogParseError",
|
|
273
|
+
f"Failed to search for failed transactions: {str(e)}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def get_database_sync_history(limit: int = 20) -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Get database synchronization history.
|
|
280
|
+
Shows when 'pacman -Sy' was run.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
limit: Maximum number of sync events to return (default 20)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dict with database sync history
|
|
287
|
+
"""
|
|
288
|
+
if not IS_ARCH:
|
|
289
|
+
return create_error_response(
|
|
290
|
+
"NotSupported",
|
|
291
|
+
"This feature is only available on Arch Linux"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
logger.info(f"Getting database sync history (limit={limit})")
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
pacman_log = Path(PACMAN_LOG)
|
|
298
|
+
|
|
299
|
+
if not pacman_log.exists():
|
|
300
|
+
return create_error_response(
|
|
301
|
+
"NotFound",
|
|
302
|
+
f"Pacman log file not found at {PACMAN_LOG}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
sync_events = []
|
|
306
|
+
|
|
307
|
+
with open(pacman_log, 'r') as f:
|
|
308
|
+
lines = f.readlines()
|
|
309
|
+
|
|
310
|
+
# Process in reverse order for most recent first
|
|
311
|
+
for line in reversed(lines):
|
|
312
|
+
if len(sync_events) >= limit:
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
# Look for database synchronization entries
|
|
316
|
+
if "synchronizing package lists" in line.lower() or "starting full system upgrade" in line.lower():
|
|
317
|
+
timestamp_match = re.match(r'\[(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\]', line)
|
|
318
|
+
|
|
319
|
+
if timestamp_match:
|
|
320
|
+
timestamp = f"{timestamp_match.group(1)}T{timestamp_match.group(2)}:00"
|
|
321
|
+
|
|
322
|
+
event_type = "sync"
|
|
323
|
+
if "starting full system upgrade" in line.lower():
|
|
324
|
+
event_type = "full_upgrade"
|
|
325
|
+
|
|
326
|
+
sync_events.append({
|
|
327
|
+
"timestamp": timestamp,
|
|
328
|
+
"type": event_type,
|
|
329
|
+
"message": line.strip()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
logger.info(f"Found {len(sync_events)} sync events")
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
"count": len(sync_events),
|
|
336
|
+
"sync_events": sync_events
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.error(f"Failed to get sync history: {e}")
|
|
341
|
+
return create_error_response(
|
|
342
|
+
"LogParseError",
|
|
343
|
+
f"Failed to get database sync history: {str(e)}"
|
|
344
|
+
)
|
|
345
|
+
|