arch-ops-server 3.0.1__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.
@@ -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
+