]>
Commit | Line | Data |
---|---|---|
1ca0323e | 1 | #! @PYTHON3@ |
b828c2f5 EJ |
2 | |
3 | # This library is free software; you can redistribute it and/or | |
4 | # modify it under the terms of version 2.1 of the GNU Lesser General Public | |
5 | # License as published by the Free Software Foundation. | |
6 | # | |
7 | # This library is distributed in the hope that it will be useful, | |
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
10 | # Lesser General Public License for more details. | |
11 | # | |
12 | # You should have received a copy of the GNU Lesser General Public | |
13 | # License along with this library; if not, write to the Free Software | |
14 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
15 | # | |
16 | # Copyright (c) 2005, 2007 XenSource Ltd. | |
0b2c7e69 | 17 | # Copyright (c) 2010, 2011, 2012, 2013, 2015, 2016, 2017 Nicira, Inc. |
b828c2f5 EJ |
18 | |
19 | # | |
20 | # To add new entries to the bugtool, you need to: | |
21 | # | |
22 | # Create a new capability. These declare the new entry to the GUI, including | |
23 | # the expected size, time to collect, privacy implications, and whether the | |
24 | # capability should be selected by default. One capability may refer to | |
25 | # multiple files, assuming that they can be reasonably grouped together, and | |
26 | # have the same privacy implications. You need: | |
27 | # | |
28 | # A new CAP_ constant. | |
29 | # A cap() invocation to declare the capability. | |
30 | # | |
31 | # You then need to add calls to main() to collect the files. These will | |
32 | # typically be calls to the helpers file_output(), tree_output(), cmd_output(), | |
33 | # or func_output(). | |
34 | # | |
35 | ||
9e6c00bc | 36 | from io import BytesIO |
6c7050b5 | 37 | import fcntl |
b828c2f5 | 38 | import getopt |
6c7050b5 | 39 | import hashlib |
b828c2f5 | 40 | import os |
6c7050b5 | 41 | import platform |
42 | import re | |
b828c2f5 EJ |
43 | import sys |
44 | import tarfile | |
45 | import time | |
6c7050b5 | 46 | import warnings |
b828c2f5 | 47 | import zipfile |
b828c2f5 | 48 | from select import select |
441d0746 | 49 | from signal import SIGTERM |
9e6c00bc | 50 | from subprocess import PIPE, Popen, check_output |
6c7050b5 | 51 | |
52 | from xml.dom.minidom import getDOMImplementation, parse | |
53 | ||
441d0746 | 54 | warnings.filterwarnings(action="ignore", category=DeprecationWarning) |
b828c2f5 | 55 | |
b828c2f5 EJ |
56 | OS_RELEASE = platform.release() |
57 | ||
58 | # | |
59 | # Files & directories | |
60 | # | |
61 | ||
a004a607 BP |
62 | APT_SOURCES_LIST = "/etc/apt/sources.list" |
63 | APT_SOURCES_LIST_D = "/etc/apt/sources.list.d" | |
36f0987f | 64 | BUG_DIR = "/var/log/ovs-bugtool" |
4b38c153 | 65 | PLUGIN_DIR = "@pkgdatadir@/bugtool-plugins" |
b828c2f5 EJ |
66 | GRUB_CONFIG = '/boot/grub/menu.lst' |
67 | BOOT_KERNEL = '/boot/vmlinuz-' + OS_RELEASE | |
68 | BOOT_INITRD = '/boot/initrd-' + OS_RELEASE + '.img' | |
69 | PROC_PARTITIONS = '/proc/partitions' | |
70 | FSTAB = '/etc/fstab' | |
71 | PROC_MOUNTS = '/proc/mounts' | |
899d3c2d BP |
72 | ISCSI_CONF = '/etc/iscsi/iscsid.conf' |
73 | ISCSI_INITIATOR = '/etc/iscsi/initiatorname.iscsi' | |
b828c2f5 EJ |
74 | PROC_CPUINFO = '/proc/cpuinfo' |
75 | PROC_MEMINFO = '/proc/meminfo' | |
76 | PROC_IOPORTS = '/proc/ioports' | |
77 | PROC_INTERRUPTS = '/proc/interrupts' | |
78 | PROC_SCSI = '/proc/scsi/scsi' | |
79 | PROC_VERSION = '/proc/version' | |
80 | PROC_MODULES = '/proc/modules' | |
81 | PROC_DEVICES = '/proc/devices' | |
82 | PROC_FILESYSTEMS = '/proc/filesystems' | |
83 | PROC_CMDLINE = '/proc/cmdline' | |
84 | PROC_CONFIG = '/proc/config.gz' | |
85 | PROC_USB_DEV = '/proc/bus/usb/devices' | |
899d3c2d BP |
86 | PROC_NET_BONDING_DIR = '/proc/net/bonding' |
87 | IFCFG_RE = re.compile(r'^.*/ifcfg-.*') | |
88 | ROUTE_RE = re.compile(r'^.*/route-.*') | |
89 | SYSCONFIG_HWCONF = '/etc/sysconfig/hwconf' | |
90 | SYSCONFIG_NETWORK = '/etc/sysconfig/network' | |
91 | SYSCONFIG_NETWORK_SCRIPTS = '/etc/sysconfig/network-scripts' | |
85282542 GS |
92 | INTERFACES = '/etc/network/interfaces' |
93 | INTERFACESD = '/etc/network/interfaces.d' | |
899d3c2d | 94 | PROC_NET_VLAN_DIR = '/proc/net/vlan' |
b828c2f5 | 95 | PROC_NET_SOFTNET_STAT = '/proc/net/softnet_stat' |
899d3c2d | 96 | MODPROBE_CONF = '/etc/modprobe.conf' |
b828c2f5 EJ |
97 | MODPROBE_DIR = '/etc/modprobe.d' |
98 | RESOLV_CONF = '/etc/resolv.conf' | |
899d3c2d BP |
99 | MPP_CONF = '/etc/mpp.conf' |
100 | MULTIPATH_CONF = '/etc/multipath.conf' | |
b828c2f5 EJ |
101 | NSSWITCH_CONF = '/etc/nsswitch.conf' |
102 | NTP_CONF = '/etc/ntp.conf' | |
899d3c2d | 103 | IPTABLES_CONFIG = '/etc/sysconfig/iptables-config' |
b828c2f5 EJ |
104 | HOSTS = '/etc/hosts' |
105 | HOSTS_ALLOW = '/etc/hosts.allow' | |
106 | HOSTS_DENY = '/etc/hosts.deny' | |
899d3c2d | 107 | DHCP_LEASE_DIR = ['/var/lib/dhclient', '/var/lib/dhcp3'] |
d0c0b87f | 108 | OPENVSWITCH_LOG_DIR = '@LOGDIR@/' |
03fcb5df GS |
109 | OPENVSWITCH_DEFAULT_SWITCH = '/etc/default/openvswitch-switch' # Debian |
110 | OPENVSWITCH_SYSCONFIG_SWITCH = '/etc/sysconfig/openvswitch' # RHEL | |
f973f2af | 111 | OPENVSWITCH_CONF_DB = '@DBDIR@/conf.db' |
34758022 | 112 | OPENVSWITCH_COMPACT_DB = '@DBDIR@/bugtool-compact-conf.db' |
b2df0225 | 113 | OPENVSWITCH_VSWITCHD_PID = '@RUNDIR@/ovs-vswitchd.pid' |
b828c2f5 | 114 | VAR_LOG_DIR = '/var/log/' |
36f0987f | 115 | VAR_LOG_CORE_DIR = '/var/log/core' |
899d3c2d BP |
116 | YUM_LOG = '/var/log/yum.log' |
117 | YUM_REPOS_DIR = '/etc/yum.repos.d' | |
b828c2f5 EJ |
118 | |
119 | # | |
120 | # External programs | |
121 | # | |
122 | ||
03fcb5df GS |
123 | os.environ['PATH'] = '/usr/local/sbin:/usr/local/bin:' \ |
124 | '/usr/sbin:/usr/bin:/sbin:/bin:@pkgdatadir@/scripts' | |
899d3c2d | 125 | ARP = 'arp' |
899d3c2d BP |
126 | CAT = 'cat' |
127 | CHKCONFIG = 'chkconfig' | |
8979dce3 | 128 | DATE = 'date' |
899d3c2d BP |
129 | DF = 'df' |
130 | DMESG = 'dmesg' | |
131 | DMIDECODE = 'dmidecode' | |
132 | DMSETUP = 'dmsetup' | |
a004a607 | 133 | DPKG_QUERY = 'dpkg-query' |
899d3c2d BP |
134 | ETHTOOL = 'ethtool' |
135 | FDISK = 'fdisk' | |
136 | FIND = 'find' | |
0b2c7e69 | 137 | IP = 'ip' |
899d3c2d BP |
138 | IPTABLES = 'iptables' |
139 | ISCSIADM = 'iscsiadm' | |
140 | LOSETUP = 'losetup' | |
141 | LS = 'ls' | |
142 | LSPCI = 'lspci' | |
899d3c2d BP |
143 | MODINFO = 'modinfo' |
144 | MPPUTIL = 'mppUtil' | |
145 | MULTIPATHD = 'multipathd' | |
146 | NETSTAT = 'netstat' | |
147 | OVS_DPCTL = 'ovs-dpctl' | |
148 | OVS_OFCTL = 'ovs-ofctl' | |
149 | OVS_VSCTL = 'ovs-vsctl' | |
899d3c2d | 150 | PS = 'ps' |
899d3c2d BP |
151 | ROUTE = 'route' |
152 | RPM = 'rpm' | |
153 | SG_MAP = 'sg_map' | |
1134de88 | 154 | SHA256_SUM = 'sha256sum' |
899d3c2d BP |
155 | SYSCTL = 'sysctl' |
156 | TC = 'tc' | |
157 | UPTIME = 'uptime' | |
899d3c2d | 158 | ZCAT = 'zcat' |
2b8e39ae | 159 | |
b828c2f5 EJ |
160 | # |
161 | # PII -- Personally identifiable information. Of particular concern are | |
162 | # things that would identify customers, or their network topology. | |
163 | # Passwords are never to be included in any bug report, regardless of any PII | |
164 | # declaration. | |
165 | # | |
166 | # NO -- No PII will be in these entries. | |
167 | # YES -- PII will likely or certainly be in these entries. | |
168 | # MAYBE -- The user may wish to audit these entries for PII. | |
169 | # IF_CUSTOMIZED -- If the files are unmodified, then they will contain no PII, | |
170 | # but since we encourage customers to edit these files, PII may have been | |
171 | # introduced by the customer. This is used in particular for the networking | |
172 | # scripts in dom0. | |
173 | # | |
174 | ||
03fcb5df GS |
175 | PII_NO = 'no' |
176 | PII_YES = 'yes' | |
177 | PII_MAYBE = 'maybe' | |
b828c2f5 | 178 | PII_IF_CUSTOMIZED = 'if_customized' |
03fcb5df GS |
179 | KEY = 0 |
180 | PII = 1 | |
b828c2f5 EJ |
181 | MIN_SIZE = 2 |
182 | MAX_SIZE = 3 | |
183 | MIN_TIME = 4 | |
184 | MAX_TIME = 5 | |
03fcb5df GS |
185 | MIME = 6 |
186 | CHECKED = 7 | |
187 | HIDDEN = 8 | |
b828c2f5 EJ |
188 | |
189 | MIME_DATA = 'application/data' | |
190 | MIME_TEXT = 'text/plain' | |
191 | ||
192 | INVENTORY_XML_ROOT = "system-status-inventory" | |
193 | INVENTORY_XML_SUMMARY = 'system-summary' | |
194 | INVENTORY_XML_ELEMENT = 'inventory-entry' | |
195 | CAP_XML_ROOT = "system-status-capabilities" | |
196 | CAP_XML_ELEMENT = 'capability' | |
197 | ||
198 | ||
03fcb5df GS |
199 | CAP_BOOT_LOADER = 'boot-loader' |
200 | CAP_DISK_INFO = 'disk-info' | |
201 | CAP_HARDWARE_INFO = 'hardware-info' | |
202 | CAP_KERNEL_INFO = 'kernel-info' | |
203 | CAP_LOSETUP_A = 'loopback-devices' | |
204 | CAP_MULTIPATH = 'multipath' | |
205 | CAP_NETWORK_CONFIG = 'network-config' | |
206 | CAP_NETWORK_INFO = 'network-info' | |
207 | CAP_NETWORK_STATUS = 'network-status' | |
208 | CAP_OPENVSWITCH_LOGS = 'ovs-system-logs' | |
209 | CAP_PROCESS_LIST = 'process-list' | |
210 | CAP_SYSTEM_LOGS = 'system-logs' | |
211 | CAP_SYSTEM_SERVICES = 'system-services' | |
212 | CAP_YUM = 'yum' | |
b828c2f5 EJ |
213 | |
214 | KB = 1024 | |
215 | MB = 1024 * 1024 | |
216 | ||
217 | caps = {} | |
218 | cap_sizes = {} | |
219 | unlimited_data = False | |
220 | dbg = False | |
b1361289 | 221 | # Default value for the number of days to collect logs. |
54e536a6 | 222 | log_days = 20 |
b1361289 | 223 | log_last_mod_time = None |
4e0abe6e | 224 | free_disk_space = None |
8979dce3 DPP |
225 | # Default value for delay between repeated commands |
226 | command_delay = 10 | |
b828c2f5 | 227 | |
03fcb5df | 228 | |
b828c2f5 EJ |
229 | def cap(key, pii=PII_MAYBE, min_size=-1, max_size=-1, min_time=-1, |
230 | max_time=-1, mime=MIME_TEXT, checked=True, hidden=False): | |
231 | caps[key] = (key, pii, min_size, max_size, min_time, max_time, mime, | |
232 | checked, hidden) | |
233 | cap_sizes[key] = 0 | |
234 | ||
235 | ||
03fcb5df GS |
236 | cap(CAP_BOOT_LOADER, PII_NO, max_size=3 * KB, max_time=5) |
237 | cap(CAP_DISK_INFO, PII_MAYBE, max_size=50 * KB, max_time=20) | |
238 | cap(CAP_HARDWARE_INFO, PII_MAYBE, max_size=2 * MB, max_time=20) | |
239 | cap(CAP_KERNEL_INFO, PII_MAYBE, max_size=120 * KB, max_time=5) | |
240 | cap(CAP_LOSETUP_A, PII_MAYBE, max_size=KB, max_time=5) | |
241 | cap(CAP_MULTIPATH, PII_MAYBE, max_size=20 * KB, max_time=10) | |
242 | cap(CAP_NETWORK_CONFIG, PII_IF_CUSTOMIZED, min_size=0, max_size=5 * MB) | |
243 | cap(CAP_NETWORK_INFO, PII_YES, max_size=50 * MB, max_time=30) | |
244 | cap(CAP_NETWORK_STATUS, PII_YES, max_size=-1, max_time=30) | |
245 | cap(CAP_OPENVSWITCH_LOGS, PII_MAYBE, max_size=-1, max_time=5) | |
246 | cap(CAP_PROCESS_LIST, PII_YES, max_size=30 * KB, max_time=20) | |
247 | cap(CAP_SYSTEM_LOGS, PII_MAYBE, max_size=200 * MB, max_time=5) | |
248 | cap(CAP_SYSTEM_SERVICES, PII_NO, max_size=5 * KB, max_time=20) | |
249 | cap(CAP_YUM, PII_IF_CUSTOMIZED, max_size=10 * KB, max_time=30) | |
b828c2f5 EJ |
250 | |
251 | ANSWER_YES_TO_ALL = False | |
252 | SILENT_MODE = False | |
253 | entries = None | |
254 | data = {} | |
255 | dev_null = open('/dev/null', 'r+') | |
256 | ||
03fcb5df | 257 | |
b828c2f5 EJ |
258 | def output(x): |
259 | global SILENT_MODE | |
260 | if not SILENT_MODE: | |
588d807c | 261 | print(x) |
b828c2f5 | 262 | |
03fcb5df | 263 | |
b828c2f5 EJ |
264 | def output_ts(x): |
265 | output("[%s] %s" % (time.strftime("%x %X %Z"), x)) | |
266 | ||
03fcb5df | 267 | |
8979dce3 DPP |
268 | def cmd_output(cap, args, label=None, filter=None, |
269 | binary=False, repeat_count=1): | |
b828c2f5 EJ |
270 | if cap in entries: |
271 | if not label: | |
272 | if isinstance(args, list): | |
273 | a = [aa for aa in args] | |
274 | a[0] = os.path.basename(a[0]) | |
275 | label = ' '.join(a) | |
276 | else: | |
277 | label = args | |
4a39a40a | 278 | data[label] = {'cap': cap, 'cmd_args': args, 'filter': filter, |
8979dce3 | 279 | 'binary': binary, 'repeat_count': repeat_count} |
b828c2f5 | 280 | |
4e0abe6e | 281 | |
b1361289 | 282 | def file_output(cap, path_list, newest_first=False, last_mod_time=None): |
42d89d2a RS |
283 | """ |
284 | If newest_first is True, the list of files in path_list is sorted | |
285 | by file modification time in descending order, else its sorted | |
286 | in ascending order. | |
287 | """ | |
b828c2f5 | 288 | if cap in entries: |
42d89d2a RS |
289 | path_entries = [] |
290 | for path in path_list: | |
291 | try: | |
292 | s = os.stat(path) | |
0344512c | 293 | except OSError: |
42d89d2a | 294 | continue |
b1361289 SHL |
295 | if last_mod_time is None or s.st_mtime >= last_mod_time: |
296 | path_entries.append((path, s)) | |
42d89d2a | 297 | |
41550bb9 GS |
298 | def mtime(arg): |
299 | (path, stat) = arg | |
300 | return stat.st_mtime | |
301 | ||
42d89d2a RS |
302 | path_entries.sort(key=mtime, reverse=newest_first) |
303 | for p in path_entries: | |
4e0abe6e | 304 | if check_space(cap, p[0], p[1].st_size): |
42d89d2a | 305 | data[p] = {'cap': cap, 'filename': p[0]} |
4e0abe6e | 306 | |
b828c2f5 | 307 | |
b1361289 SHL |
308 | def tree_output(cap, path, pattern=None, negate=False, newest_first=False, |
309 | last_mod_time=None): | |
42d89d2a RS |
310 | """ |
311 | Walks the directory tree rooted at path. Files in current dir are processed | |
312 | before files in sub-dirs. | |
313 | """ | |
b828c2f5 EJ |
314 | if cap in entries: |
315 | if os.path.exists(path): | |
42d89d2a RS |
316 | for root, dirs, files in os.walk(path): |
317 | fns = [fn for fn in [os.path.join(root, f) for f in files] | |
318 | if os.path.isfile(fn) and matches(fn, pattern, negate)] | |
b1361289 SHL |
319 | file_output(cap, fns, newest_first=newest_first, |
320 | last_mod_time=last_mod_time) | |
321 | ||
322 | ||
323 | def prefix_output(cap, prefix, newest_first=False, last_mod_time=None): | |
324 | """ | |
325 | Output files with the same prefix. | |
326 | """ | |
327 | fns = [] | |
328 | for root, dirs, files in os.walk(os.path.dirname(prefix)): | |
329 | fns += [fn for fn in [os.path.join(root, f) for f in files] | |
330 | if fn.startswith(prefix)] | |
331 | file_output(cap, fns, newest_first=newest_first, | |
332 | last_mod_time=last_mod_time) | |
333 | ||
b828c2f5 EJ |
334 | |
335 | def func_output(cap, label, func): | |
336 | if cap in entries: | |
b828c2f5 EJ |
337 | data[label] = {'cap': cap, 'func': func} |
338 | ||
03fcb5df | 339 | |
b828c2f5 | 340 | def collect_data(): |
8979dce3 DPP |
341 | first_run = True |
342 | ||
343 | while True: | |
344 | process_lists = {} | |
345 | ||
346 | for (k, v) in data.items(): | |
347 | cap = v['cap'] | |
348 | if 'cmd_args' in v: | |
349 | if 'output' not in v.keys(): | |
9e6c00bc | 350 | v['output'] = BytesIOmtime() |
8979dce3 DPP |
351 | if v['repeat_count'] > 0: |
352 | if cap not in process_lists: | |
353 | process_lists[cap] = [] | |
354 | process_lists[cap].append( | |
355 | ProcOutput(v['cmd_args'], caps[cap][MAX_TIME], | |
356 | v['output'], v['filter'], v['binary'])) | |
357 | v['repeat_count'] -= 1 | |
358 | ||
359 | if bool(process_lists): | |
360 | if not first_run: | |
361 | output_ts("Waiting %d sec between repeated commands" % | |
362 | command_delay) | |
363 | time.sleep(command_delay) | |
364 | else: | |
365 | first_run = False | |
366 | run_procs(process_lists.values()) | |
367 | else: | |
368 | break | |
b828c2f5 EJ |
369 | |
370 | for (k, v) in data.items(): | |
371 | cap = v['cap'] | |
8979dce3 | 372 | if 'filename' in v and v['filename'].startswith('/proc/'): |
b828c2f5 EJ |
373 | # proc files must be read into memory |
374 | try: | |
9e6c00bc | 375 | f = open(v['filename'], 'rb') |
b828c2f5 EJ |
376 | s = f.read() |
377 | f.close() | |
4e0abe6e | 378 | if check_space(cap, v['filename'], len(s)): |
9e6c00bc | 379 | v['output'] = BytesIOmtime(s) |
b828c2f5 EJ |
380 | except: |
381 | pass | |
b0195f47 | 382 | elif 'func' in v: |
b828c2f5 EJ |
383 | try: |
384 | s = v['func'](cap) | |
588d807c | 385 | except Exception as e: |
9e6c00bc | 386 | s = str(e).encode() |
4e0abe6e | 387 | if check_space(cap, k, len(s)): |
9e6c00bc TR |
388 | if isinstance(s, str): |
389 | v['output'] = BytesIOmtime(s.encode()) | |
390 | else: | |
391 | v['output'] = BytesIOmtime(s) | |
b828c2f5 | 392 | |
b828c2f5 | 393 | |
b592e726 | 394 | def main(argv=None): |
b828c2f5 | 395 | global ANSWER_YES_TO_ALL, SILENT_MODE |
b1361289 | 396 | global entries, data, dbg, unlimited_data, free_disk_space |
8979dce3 | 397 | global log_days, log_last_mod_time, command_delay |
b828c2f5 | 398 | |
b592e726 AS |
399 | # Filter flags |
400 | only_ovs_info = False | |
401 | collect_all_info = True | |
402 | ||
7be0b8a0 | 403 | if '--help' in sys.argv: |
588d807c | 404 | print(""" |
7be0b8a0 BP |
405 | %(argv0)s: create status report bundles to assist in problem diagnosis |
406 | usage: %(argv0)s OPTIONS | |
407 | ||
408 | By default, %(argv0)s prompts for permission to collect each form of status | |
409 | information and produces a .tar.gz file as output. | |
410 | ||
411 | The following options are available. | |
412 | --help display this help message, then exit | |
413 | -s, --silent suppress most output to stdout | |
414 | ||
415 | Options for categories of data to collect: | |
416 | --entries=CAP_A,CAP_B,... set categories of data to collect | |
417 | --all collect all categories | |
418 | --ovs collect only directly OVS-related info | |
419 | --log-days=DAYS collect DAYS worth of old logs | |
8979dce3 | 420 | --delay=DELAY set delay between repeated command |
7be0b8a0 BP |
421 | -y, --yestoall suppress prompts to confirm collection |
422 | --capabilities print categories as XML on stdout, then exit | |
423 | ||
424 | Output options: | |
425 | --output=FORMAT set output format to one of tar tar.bz2 tar.gz zip | |
426 | --outfile=FILE write output to FILE | |
427 | --outfd=FD write output to FD (requires --output=tar) | |
428 | --unlimited ignore default limits on sizes of data collected | |
429 | --debug print ovs-bugtool debug info on stdout\ | |
588d807c | 430 | """ % {'argv0': sys.argv[0]}) |
7be0b8a0 BP |
431 | sys.exit(0) |
432 | ||
b828c2f5 EJ |
433 | # we need access to privileged files, exit if we are not running as root |
434 | if os.getuid() != 0: | |
588d807c | 435 | print("Error: ovs-bugtool must be run as root", file=sys.stderr) |
b828c2f5 EJ |
436 | return 1 |
437 | ||
cfc693d6 | 438 | output_file = None |
f07902cd | 439 | output_type = 'tar.gz' |
b828c2f5 EJ |
440 | output_fd = -1 |
441 | ||
442 | if argv is None: | |
443 | argv = sys.argv | |
444 | ||
445 | try: | |
446 | (options, params) = getopt.gnu_getopt( | |
447 | argv, 'sy', ['capabilities', 'silent', 'yestoall', 'entries=', | |
cfc693d6 | 448 | 'output=', 'outfd=', 'outfile=', 'all', 'unlimited', |
8979dce3 | 449 | 'debug', 'ovs', 'log-days=', 'delay=']) |
588d807c RB |
450 | except getopt.GetoptError as opterr: |
451 | print(opterr, file=sys.stderr) | |
b828c2f5 EJ |
452 | return 2 |
453 | ||
454 | try: | |
455 | load_plugins(True) | |
456 | except: | |
457 | pass | |
458 | ||
459 | entries = [e for e in caps.keys() if caps[e][CHECKED]] | |
460 | ||
461 | for (k, v) in options: | |
462 | if k == '--capabilities': | |
463 | update_capabilities() | |
464 | print_capabilities() | |
465 | return 0 | |
466 | ||
467 | if k == '--output': | |
03fcb5df | 468 | if v in ['tar', 'tar.bz2', 'tar.gz', 'zip']: |
b828c2f5 EJ |
469 | output_type = v |
470 | else: | |
588d807c | 471 | print("Invalid output format '%s'" % v, file=sys.stderr) |
b828c2f5 EJ |
472 | return 2 |
473 | ||
474 | # "-s" or "--silent" means suppress output (except for the final | |
475 | # output filename at the end) | |
476 | if k in ['-s', '--silent']: | |
477 | SILENT_MODE = True | |
478 | ||
479 | if k == '--entries' and v != '': | |
480 | entries = v.split(',') | |
481 | ||
482 | # If the user runs the script with "-y" or "--yestoall" we don't ask | |
483 | # all the really annoying questions. | |
484 | if k in ['-y', '--yestoall']: | |
485 | ANSWER_YES_TO_ALL = True | |
486 | ||
487 | if k == '--outfd': | |
488 | output_fd = int(v) | |
489 | try: | |
490 | old = fcntl.fcntl(output_fd, fcntl.F_GETFD) | |
491 | fcntl.fcntl(output_fd, fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) | |
492 | except: | |
588d807c RB |
493 | print("Invalid output file descriptor", output_fd, |
494 | file=sys.stderr) | |
b828c2f5 EJ |
495 | return 2 |
496 | ||
cfc693d6 SHL |
497 | if k == '--outfile': |
498 | output_file = v | |
499 | ||
b828c2f5 EJ |
500 | elif k == '--all': |
501 | entries = caps.keys() | |
502 | elif k == '--unlimited': | |
503 | unlimited_data = True | |
504 | elif k == '--debug': | |
505 | dbg = True | |
506 | ProcOutput.debug = True | |
507 | ||
b592e726 AS |
508 | if k == '--ovs': |
509 | only_ovs_info = True | |
510 | collect_all_info = False | |
511 | ||
ced17198 | 512 | if k == '--log-days': |
54e536a6 | 513 | log_days = int(v) |
ced17198 | 514 | |
8979dce3 DPP |
515 | if k == '--delay': |
516 | command_delay = int(v) | |
517 | ||
b828c2f5 | 518 | if len(params) != 1: |
588d807c | 519 | print("Invalid additional arguments", str(params), file=sys.stderr) |
b828c2f5 EJ |
520 | return 2 |
521 | ||
522 | if output_fd != -1 and output_type != 'tar': | |
588d807c RB |
523 | print("Option '--outfd' only valid with '--output=tar'", |
524 | file=sys.stderr) | |
b828c2f5 EJ |
525 | return 2 |
526 | ||
cfc693d6 | 527 | if output_fd != -1 and output_file is not None: |
588d807c | 528 | print("Cannot set both '--outfd' and '--outfile'", file=sys.stderr) |
cfc693d6 SHL |
529 | return 2 |
530 | ||
4e0abe6e SHL |
531 | if output_file is not None and not unlimited_data: |
532 | free_disk_space = get_free_disk_space(output_file) * 90 / 100 | |
533 | ||
b1361289 SHL |
534 | log_last_mod_time = int(time.time()) - log_days * 86400 |
535 | ||
b828c2f5 | 536 | if ANSWER_YES_TO_ALL: |
03fcb5df GS |
537 | output("Warning: '--yestoall' argument provided, will not prompt " |
538 | "for individual files.") | |
b828c2f5 EJ |
539 | |
540 | output(''' | |
541 | This application will collate dmesg output, details of the | |
542 | hardware configuration of your machine, information about the build of | |
543 | openvswitch that you are using, plus, if you allow it, various logs. | |
544 | ||
545 | The collated information will be saved as a .%s for archiving or | |
546 | sending to a Technical Support Representative. | |
547 | ||
548 | The logs may contain private information, and if you are at all | |
549 | worried about that, you should exit now, or you should explicitly | |
550 | exclude those logs from the archive. | |
551 | ||
552 | ''' % output_type) | |
553 | ||
554 | # assemble potential data | |
555 | ||
556 | file_output(CAP_BOOT_LOADER, [GRUB_CONFIG]) | |
557 | cmd_output(CAP_BOOT_LOADER, [LS, '-lR', '/boot']) | |
1134de88 BP |
558 | cmd_output(CAP_BOOT_LOADER, [SHA256_SUM, BOOT_KERNEL, BOOT_INITRD], |
559 | label='vmlinuz-initrd.sha256sum') | |
b828c2f5 EJ |
560 | |
561 | cmd_output(CAP_DISK_INFO, [FDISK, '-l']) | |
562 | file_output(CAP_DISK_INFO, [PROC_PARTITIONS, PROC_MOUNTS]) | |
899d3c2d | 563 | file_output(CAP_DISK_INFO, [FSTAB, ISCSI_CONF, ISCSI_INITIATOR]) |
b828c2f5 EJ |
564 | cmd_output(CAP_DISK_INFO, [DF, '-alT']) |
565 | cmd_output(CAP_DISK_INFO, [DF, '-alTi']) | |
899d3c2d BP |
566 | if len(pidof('iscsid')) != 0: |
567 | cmd_output(CAP_DISK_INFO, [ISCSIADM, '-m', 'node']) | |
b828c2f5 EJ |
568 | cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_host']) |
569 | cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/scsi_disk']) | |
570 | cmd_output(CAP_DISK_INFO, [LS, '-R', '/sys/class/fc_transport']) | |
899d3c2d | 571 | cmd_output(CAP_DISK_INFO, [SG_MAP, '-x']) |
b828c2f5 | 572 | func_output(CAP_DISK_INFO, 'scsi-hosts', dump_scsi_hosts) |
b828c2f5 | 573 | |
03fcb5df GS |
574 | file_output(CAP_HARDWARE_INFO, |
575 | [PROC_CPUINFO, PROC_MEMINFO, PROC_IOPORTS, PROC_INTERRUPTS]) | |
b828c2f5 EJ |
576 | cmd_output(CAP_HARDWARE_INFO, [DMIDECODE]) |
577 | cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-n']) | |
578 | cmd_output(CAP_HARDWARE_INFO, [LSPCI, '-vv']) | |
579 | file_output(CAP_HARDWARE_INFO, [PROC_USB_DEV, PROC_SCSI]) | |
899d3c2d | 580 | file_output(CAP_HARDWARE_INFO, [SYSCONFIG_HWCONF]) |
b828c2f5 | 581 | cmd_output(CAP_HARDWARE_INFO, [LS, '-lR', '/dev']) |
899d3c2d | 582 | |
03fcb5df GS |
583 | file_output(CAP_KERNEL_INFO, [PROC_VERSION, PROC_MODULES, |
584 | PROC_DEVICES, PROC_FILESYSTEMS, PROC_CMDLINE]) | |
b828c2f5 EJ |
585 | cmd_output(CAP_KERNEL_INFO, [ZCAT, PROC_CONFIG], label='config') |
586 | cmd_output(CAP_KERNEL_INFO, [SYSCTL, '-A']) | |
899d3c2d | 587 | file_output(CAP_KERNEL_INFO, [MODPROBE_CONF]) |
b828c2f5 EJ |
588 | tree_output(CAP_KERNEL_INFO, MODPROBE_DIR) |
589 | func_output(CAP_KERNEL_INFO, 'modinfo', module_info) | |
590 | ||
591 | cmd_output(CAP_LOSETUP_A, [LOSETUP, '-a']) | |
592 | ||
899d3c2d BP |
593 | file_output(CAP_MULTIPATH, [MULTIPATH_CONF, MPP_CONF]) |
594 | cmd_output(CAP_MULTIPATH, [DMSETUP, 'table']) | |
595 | func_output(CAP_MULTIPATH, 'multipathd_topology', multipathd_topology) | |
596 | cmd_output(CAP_MULTIPATH, [MPPUTIL, '-a']) | |
b592e726 | 597 | if CAP_MULTIPATH in entries and collect_all_info: |
899d3c2d BP |
598 | dump_rdac_groups(CAP_MULTIPATH) |
599 | ||
600 | tree_output(CAP_NETWORK_CONFIG, SYSCONFIG_NETWORK_SCRIPTS, IFCFG_RE) | |
601 | tree_output(CAP_NETWORK_CONFIG, SYSCONFIG_NETWORK_SCRIPTS, ROUTE_RE) | |
85282542 GS |
602 | tree_output(CAP_NETWORK_CONFIG, INTERFACESD) |
603 | file_output(CAP_NETWORK_CONFIG, [SYSCONFIG_NETWORK, RESOLV_CONF, | |
604 | NSSWITCH_CONF, HOSTS, INTERFACES]) | |
03fcb5df GS |
605 | file_output(CAP_NETWORK_CONFIG, |
606 | [NTP_CONF, IPTABLES_CONFIG, HOSTS_ALLOW, HOSTS_DENY]) | |
1cad56e1 | 607 | file_output(CAP_NETWORK_CONFIG, [OPENVSWITCH_DEFAULT_SWITCH, |
ccb0ca03 | 608 | OPENVSWITCH_SYSCONFIG_SWITCH]) |
b828c2f5 | 609 | |
0b2c7e69 | 610 | cmd_output(CAP_NETWORK_INFO, [IP, 'addr', 'show']) |
eb6f3089 GS |
611 | cmd_output(CAP_NETWORK_INFO, [ROUTE, '-n']) |
612 | cmd_output(CAP_NETWORK_INFO, [ARP, '-n']) | |
613 | cmd_output(CAP_NETWORK_INFO, [NETSTAT, '-an']) | |
899d3c2d | 614 | for dir in DHCP_LEASE_DIR: |
eb6f3089 | 615 | tree_output(CAP_NETWORK_INFO, dir) |
62182ac9 | 616 | for table in ['filter', 'nat', 'mangle', 'raw', 'security']: |
eb6f3089 | 617 | cmd_output(CAP_NETWORK_INFO, [IPTABLES, '-t', table, '-nL']) |
b828c2f5 EJ |
618 | for p in os.listdir('/sys/class/net/'): |
619 | try: | |
620 | f = open('/sys/class/net/%s/type' % p, 'r') | |
621 | t = f.readline() | |
622 | f.close() | |
74e60d69 | 623 | if os.path.islink('/sys/class/net/%s/device' % p) and int(t) == 1: |
b828c2f5 | 624 | # ARPHRD_ETHER |
eb6f3089 | 625 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-S', p]) |
65e0f34e | 626 | if not p.startswith('vif') and not p.startswith('tap'): |
eb6f3089 GS |
627 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, p]) |
628 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-k', p]) | |
629 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-i', p]) | |
630 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-c', p]) | |
f5a36db1 | 631 | cmd_output(CAP_NETWORK_INFO, [ETHTOOL, '-l', p]) |
74e60d69 | 632 | if int(t) == 1: |
eb6f3089 | 633 | cmd_output(CAP_NETWORK_INFO, |
237414f8 | 634 | [TC, '-s', '-d', 'class', 'show', 'dev', p]) |
b828c2f5 EJ |
635 | except: |
636 | pass | |
eb6f3089 GS |
637 | tree_output(CAP_NETWORK_INFO, PROC_NET_BONDING_DIR) |
638 | tree_output(CAP_NETWORK_INFO, PROC_NET_VLAN_DIR) | |
639 | cmd_output(CAP_NETWORK_INFO, [TC, '-s', 'qdisc']) | |
640 | file_output(CAP_NETWORK_INFO, [PROC_NET_SOFTNET_STAT]) | |
34758022 GS |
641 | |
642 | collect_ovsdb() | |
b828c2f5 | 643 | if os.path.exists(OPENVSWITCH_VSWITCHD_PID): |
2cd2c2ef | 644 | cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'show', '-s']) |
b828c2f5 | 645 | for d in dp_list(): |
c57e02cf WT |
646 | cmd_output(CAP_NETWORK_STATUS, [OVS_DPCTL, 'dump-flows', '-m', |
647 | d.decode()]) | |
b828c2f5 | 648 | |
03fcb5df GS |
649 | cmd_output(CAP_PROCESS_LIST, [PS, 'wwwaxf', '-eo', |
650 | 'pid,tty,stat,time,nice,psr,pcpu,pmem,nwchan,wchan:25,args'], | |
651 | label='process-tree') | |
b828c2f5 EJ |
652 | func_output(CAP_PROCESS_LIST, 'fd_usage', fd_usage) |
653 | ||
03fcb5df | 654 | system_logs = ([VAR_LOG_DIR + x for x in |
659fd90c GS |
655 | ['crit.log', 'kern.log', 'daemon.log', 'user.log', |
656 | 'syslog', 'messages', 'secure', 'debug', 'dmesg', 'boot']]) | |
b1361289 SHL |
657 | for log in system_logs: |
658 | prefix_output(CAP_SYSTEM_LOGS, log, last_mod_time=log_last_mod_time) | |
659 | ||
03fcb5df | 660 | ovs_logs = ([OPENVSWITCH_LOG_DIR + x for x in |
659fd90c | 661 | ['ovs-vswitchd.log', 'ovsdb-server.log', |
2b02d770 | 662 | 'ovs-xapi-sync.log', 'ovs-ctl.log']]) |
b1361289 | 663 | for log in ovs_logs: |
03fcb5df GS |
664 | prefix_output(CAP_OPENVSWITCH_LOGS, log, |
665 | last_mod_time=log_last_mod_time) | |
d0c0b87f | 666 | |
90cef12c | 667 | cmd_output(CAP_SYSTEM_LOGS, [DMESG]) |
b828c2f5 | 668 | |
899d3c2d | 669 | cmd_output(CAP_SYSTEM_SERVICES, [CHKCONFIG, '--list']) |
b828c2f5 | 670 | |
36f0987f | 671 | tree_output(CAP_SYSTEM_LOGS, VAR_LOG_CORE_DIR) |
b828c2f5 | 672 | |
899d3c2d BP |
673 | file_output(CAP_YUM, [YUM_LOG]) |
674 | tree_output(CAP_YUM, YUM_REPOS_DIR) | |
675 | cmd_output(CAP_YUM, [RPM, '-qa']) | |
a004a607 BP |
676 | file_output(CAP_YUM, [APT_SOURCES_LIST]) |
677 | tree_output(CAP_YUM, APT_SOURCES_LIST_D) | |
03fcb5df GS |
678 | cmd_output(CAP_YUM, [DPKG_QUERY, '-W', |
679 | '-f=${Package} ${Version} ${Status}\n'], 'dpkg-packages') | |
b828c2f5 | 680 | |
b592e726 AS |
681 | # Filter out ovs relevant information if --ovs option passed |
682 | # else collect all information | |
683 | filters = set() | |
684 | if only_ovs_info: | |
685 | filters.add('ovs') | |
686 | ovs_info_caps = [CAP_NETWORK_STATUS, CAP_SYSTEM_LOGS, | |
b0d03a15 | 687 | CAP_OPENVSWITCH_LOGS, CAP_NETWORK_CONFIG] |
b592e726 | 688 | ovs_info_list = ['process-tree'] |
c4bc03d8 WT |
689 | # We cannot use items(), since we modify 'data' as we pass through |
690 | for (k, v) in list(data.items()): | |
b592e726 AS |
691 | cap = v['cap'] |
692 | if 'filename' in v: | |
693 | info = k[0] | |
694 | else: | |
695 | info = k | |
696 | if info not in ovs_info_list and cap not in ovs_info_caps: | |
697 | del data[k] | |
698 | ||
699 | if filters: | |
700 | filter = ",".join(filters) | |
701 | else: | |
702 | filter = None | |
703 | ||
b828c2f5 | 704 | try: |
b592e726 | 705 | load_plugins(filter=filter) |
b828c2f5 EJ |
706 | except: |
707 | pass | |
b592e726 | 708 | |
b828c2f5 | 709 | # permit the user to filter out data |
c4bc03d8 WT |
710 | # We cannot use items(), since we modify 'data' as we pass through |
711 | for (k, v) in list(data.items()): | |
03fcb5df GS |
712 | cap = v['cap'] |
713 | if 'filename' in v: | |
714 | key = k[0] | |
715 | else: | |
716 | key = k | |
717 | if not ANSWER_YES_TO_ALL and not yes("Include '%s'? [Y/n]: " % key): | |
718 | del data[k] | |
b828c2f5 EJ |
719 | |
720 | # collect selected data now | |
721 | output_ts('Running commands to collect data') | |
722 | collect_data() | |
723 | ||
724 | subdir = "bug-report-%s" % time.strftime("%Y%m%d%H%M%S") | |
725 | ||
726 | # include inventory | |
03fcb5df | 727 | data['inventory.xml'] = {'cap': None, |
9e6c00bc | 728 | 'output': BytesIOmtime(make_inventory(data, subdir))} |
b828c2f5 EJ |
729 | |
730 | # create archive | |
cfc693d6 SHL |
731 | if output_fd == -1: |
732 | if output_file is None: | |
733 | dirname = BUG_DIR | |
734 | else: | |
735 | dirname = os.path.dirname(output_file) | |
736 | if dirname and not os.path.exists(dirname): | |
737 | try: | |
738 | os.makedirs(dirname) | |
739 | except: | |
740 | pass | |
b828c2f5 EJ |
741 | |
742 | if output_fd == -1: | |
743 | output_ts('Creating output file') | |
744 | ||
745 | if output_type.startswith('tar'): | |
cfc693d6 | 746 | make_tar(subdir, output_type, output_fd, output_file) |
b828c2f5 | 747 | else: |
cfc693d6 | 748 | make_zip(subdir, output_file) |
b828c2f5 | 749 | |
b828c2f5 | 750 | if dbg: |
588d807c | 751 | print("Category sizes (max, actual):\n", file=sys.stderr) |
b828c2f5 | 752 | for c in caps.keys(): |
588d807c RB |
753 | print(" %s (%d, %d)" % (c, caps[c][MAX_SIZE], cap_sizes[c]), |
754 | file=sys.stderr) | |
34758022 GS |
755 | |
756 | cleanup_ovsdb() | |
b828c2f5 EJ |
757 | return 0 |
758 | ||
03fcb5df | 759 | |
b828c2f5 EJ |
760 | def dump_scsi_hosts(cap): |
761 | output = '' | |
593e93e5 AGS |
762 | scsi_list = os.listdir('/sys/class/scsi_host') |
763 | scsi_list.sort() | |
b828c2f5 | 764 | |
593e93e5 | 765 | for h in scsi_list: |
b828c2f5 EJ |
766 | procname = '' |
767 | try: | |
49b0a933 IM |
768 | f = open('/sys/class/scsi_host/%s/proc_name' % h) |
769 | procname = f.readline().strip("\n") | |
770 | f.close() | |
b828c2f5 | 771 | except: |
49b0a933 | 772 | pass |
b828c2f5 EJ |
773 | modelname = None |
774 | try: | |
49b0a933 IM |
775 | f = open('/sys/class/scsi_host/%s/model_name' % h) |
776 | modelname = f.readline().strip("\n") | |
777 | f.close() | |
b828c2f5 | 778 | except: |
49b0a933 | 779 | pass |
b828c2f5 | 780 | |
03fcb5df GS |
781 | output += "%s:\n" % h |
782 | output += " %s%s\n" \ | |
783 | % (procname, modelname and (" -> %s" % modelname) or '') | |
b828c2f5 EJ |
784 | |
785 | return output | |
786 | ||
03fcb5df | 787 | |
b828c2f5 | 788 | def module_info(cap): |
9e6c00bc | 789 | output = BytesIO() |
b828c2f5 EJ |
790 | modules = open(PROC_MODULES, 'r') |
791 | procs = [] | |
792 | ||
793 | for line in modules: | |
794 | module = line.split()[0] | |
03fcb5df GS |
795 | procs.append(ProcOutput([MODINFO, module], |
796 | caps[cap][MAX_TIME], output)) | |
b828c2f5 EJ |
797 | modules.close() |
798 | ||
799 | run_procs([procs]) | |
800 | ||
801 | return output.getvalue() | |
802 | ||
899d3c2d BP |
803 | |
804 | def multipathd_topology(cap): | |
b592e726 | 805 | pipe = Popen([MULTIPATHD, '-k'], bufsize=1, stdin=PIPE, |
899d3c2d BP |
806 | stdout=PIPE, stderr=dev_null) |
807 | stdout, stderr = pipe.communicate('show topology') | |
808 | ||
809 | return stdout | |
810 | ||
03fcb5df | 811 | |
b828c2f5 | 812 | def dp_list(): |
9e6c00bc | 813 | output = BytesIO() |
03fcb5df GS |
814 | procs = [ProcOutput([OVS_DPCTL, 'dump-dps'], |
815 | caps[CAP_NETWORK_STATUS][MAX_TIME], output)] | |
b828c2f5 EJ |
816 | |
817 | run_procs([procs]) | |
818 | ||
819 | if not procs[0].timed_out: | |
820 | return output.getvalue().splitlines() | |
821 | return [] | |
822 | ||
03fcb5df | 823 | |
34758022 GS |
824 | def collect_ovsdb(): |
825 | if not os.path.isfile(OPENVSWITCH_CONF_DB): | |
826 | return | |
827 | ||
03fcb5df | 828 | max_size = 10 * MB |
34758022 GS |
829 | |
830 | try: | |
831 | if os.path.getsize(OPENVSWITCH_CONF_DB) > max_size: | |
832 | if os.path.isfile(OPENVSWITCH_COMPACT_DB): | |
833 | os.unlink(OPENVSWITCH_COMPACT_DB) | |
834 | ||
9e6c00bc | 835 | output = BytesIO() |
34758022 GS |
836 | max_time = 5 |
837 | procs = [ProcOutput(['ovsdb-tool', 'compact', | |
838 | OPENVSWITCH_CONF_DB, OPENVSWITCH_COMPACT_DB], | |
839 | max_time, output)] | |
840 | run_procs([procs]) | |
841 | file_output(CAP_NETWORK_STATUS, [OPENVSWITCH_COMPACT_DB]) | |
842 | else: | |
843 | file_output(CAP_NETWORK_STATUS, [OPENVSWITCH_CONF_DB]) | |
0344512c | 844 | except OSError: |
34758022 GS |
845 | return |
846 | ||
03fcb5df | 847 | |
34758022 GS |
848 | def cleanup_ovsdb(): |
849 | try: | |
850 | if os.path.isfile(OPENVSWITCH_COMPACT_DB): | |
851 | os.unlink(OPENVSWITCH_COMPACT_DB) | |
852 | except: | |
853 | return | |
854 | ||
03fcb5df | 855 | |
b828c2f5 EJ |
856 | def fd_usage(cap): |
857 | output = '' | |
858 | fd_dict = {} | |
859 | for d in [p for p in os.listdir('/proc') if p.isdigit()]: | |
860 | try: | |
03fcb5df | 861 | fh = open('/proc/' + d + '/cmdline') |
b828c2f5 | 862 | name = fh.readline() |
03fcb5df | 863 | num_fds = len(os.listdir(os.path.join('/proc/' + d + '/fd'))) |
b828c2f5 | 864 | if num_fds > 0: |
b7649c73 | 865 | if num_fds not in fd_dict: |
b828c2f5 EJ |
866 | fd_dict[num_fds] = [] |
867 | fd_dict[num_fds].append(name.replace('\0', ' ').strip()) | |
868 | finally: | |
869 | fh.close() | |
870 | keys = fd_dict.keys() | |
871 | keys.sort(lambda a, b: int(b) - int(a)) | |
872 | for k in keys: | |
873 | output += "%s: %s\n" % (k, str(fd_dict[k])) | |
874 | return output | |
875 | ||
03fcb5df | 876 | |
899d3c2d | 877 | def dump_rdac_groups(cap): |
9e6c00bc | 878 | output = BytesIO() |
899d3c2d BP |
879 | procs = [ProcOutput([MPPUTIL, '-a'], caps[cap][MAX_TIME], output)] |
880 | ||
881 | run_procs([procs]) | |
882 | ||
883 | if not procs[0].timed_out: | |
884 | proc_line = 0 | |
885 | for line in output.getvalue().splitlines(): | |
886 | if line.startswith('ID'): | |
887 | proc_line = 2 | |
888 | elif line.startswith('----'): | |
889 | proc_line -= 1 | |
890 | elif proc_line > 0: | |
891 | group, _ = line.split(None, 1) | |
892 | cmd_output(cap, [MPPUTIL, '-g', group]) | |
893 | ||
03fcb5df | 894 | |
b592e726 | 895 | def load_plugins(just_capabilities=False, filter=None): |
b1361289 | 896 | global log_last_mod_time |
03fcb5df | 897 | |
b828c2f5 EJ |
898 | def getText(nodelist): |
899 | rc = "" | |
900 | for node in nodelist: | |
901 | if node.nodeType == node.TEXT_NODE: | |
902 | rc += node.data | |
9e6c00bc | 903 | return rc |
b828c2f5 | 904 | |
b592e726 | 905 | def getBoolAttr(el, attr, default=False): |
b828c2f5 EJ |
906 | ret = default |
907 | val = el.getAttribute(attr).lower() | |
908 | if val in ['true', 'false', 'yes', 'no']: | |
909 | ret = val in ['true', 'yes'] | |
910 | return ret | |
b592e726 | 911 | |
03fcb5df GS |
912 | for dir in [d for d in os.listdir(PLUGIN_DIR) |
913 | if os.path.isdir(os.path.join(PLUGIN_DIR, d))]: | |
b7649c73 | 914 | if dir not in caps: |
b828c2f5 EJ |
915 | if not os.path.exists("%s/%s.xml" % (PLUGIN_DIR, dir)): |
916 | continue | |
917 | xmldoc = parse("%s/%s.xml" % (PLUGIN_DIR, dir)) | |
918 | assert xmldoc.documentElement.tagName == "capability" | |
919 | ||
920 | pii, min_size, max_size, min_time, max_time, mime = \ | |
03fcb5df | 921 | PII_MAYBE, -1, -1, -1, -1, MIME_TEXT |
b828c2f5 | 922 | |
03fcb5df GS |
923 | if xmldoc.documentElement.getAttribute("pii") in \ |
924 | [PII_NO, PII_YES, PII_MAYBE, PII_IF_CUSTOMIZED]: | |
b828c2f5 EJ |
925 | pii = xmldoc.documentElement.getAttribute("pii") |
926 | if xmldoc.documentElement.getAttribute("min_size") != '': | |
99c74883 | 927 | min_size = int( |
03fcb5df | 928 | xmldoc.documentElement.getAttribute("min_size")) |
b828c2f5 | 929 | if xmldoc.documentElement.getAttribute("max_size") != '': |
99c74883 | 930 | max_size = int( |
03fcb5df | 931 | xmldoc.documentElement.getAttribute("max_size")) |
b828c2f5 EJ |
932 | if xmldoc.documentElement.getAttribute("min_time") != '': |
933 | min_time = int(xmldoc.documentElement.getAttribute("min_time")) | |
934 | if xmldoc.documentElement.getAttribute("max_time") != '': | |
935 | max_time = int(xmldoc.documentElement.getAttribute("max_time")) | |
03fcb5df GS |
936 | if xmldoc.documentElement.getAttribute("mime") in \ |
937 | [MIME_DATA, MIME_TEXT]: | |
b828c2f5 EJ |
938 | mime = xmldoc.documentElement.getAttribute("mime") |
939 | checked = getBoolAttr(xmldoc.documentElement, 'checked', True) | |
940 | hidden = getBoolAttr(xmldoc.documentElement, 'hidden', False) | |
941 | ||
03fcb5df GS |
942 | cap(dir, pii, min_size, max_size, min_time, max_time, mime, |
943 | checked, hidden) | |
b828c2f5 EJ |
944 | |
945 | if just_capabilities: | |
946 | continue | |
b592e726 | 947 | |
b828c2f5 EJ |
948 | plugdir = os.path.join(PLUGIN_DIR, dir) |
949 | for file in [f for f in os.listdir(plugdir) if f.endswith('.xml')]: | |
950 | xmldoc = parse(os.path.join(plugdir, file)) | |
951 | assert xmldoc.documentElement.tagName == "collect" | |
952 | ||
953 | for el in xmldoc.documentElement.getElementsByTagName("*"): | |
b592e726 AS |
954 | filters_tmp = el.getAttribute("filters") |
955 | if filters_tmp == '': | |
956 | filters = [] | |
957 | else: | |
958 | filters = filters_tmp.split(',') | |
959 | if not(filter is None or filter in filters): | |
960 | continue | |
b828c2f5 | 961 | if el.tagName == "files": |
42d89d2a | 962 | newest_first = getBoolAttr(el, 'newest_first') |
54e536a6 | 963 | if el.getAttribute("type") == "logs": |
b1361289 SHL |
964 | for fn in getText(el.childNodes).split(): |
965 | prefix_output(dir, fn, newest_first=newest_first, | |
966 | last_mod_time=log_last_mod_time) | |
54e536a6 GS |
967 | else: |
968 | file_output(dir, getText(el.childNodes).split(), | |
969 | newest_first=newest_first) | |
b828c2f5 EJ |
970 | elif el.tagName == "directory": |
971 | pattern = el.getAttribute("pattern") | |
03fcb5df GS |
972 | if pattern == '': |
973 | pattern = None | |
b828c2f5 | 974 | negate = getBoolAttr(el, 'negate') |
42d89d2a | 975 | newest_first = getBoolAttr(el, 'newest_first') |
b1361289 SHL |
976 | if el.getAttribute("type") == "logs": |
977 | tree_output(dir, getText(el.childNodes), | |
978 | pattern and re.compile(pattern) or None, | |
979 | negate=negate, newest_first=newest_first, | |
980 | last_mod_time=log_last_mod_time) | |
981 | else: | |
982 | tree_output(dir, getText(el.childNodes), | |
983 | pattern and re.compile(pattern) or None, | |
984 | negate=negate, newest_first=newest_first) | |
b828c2f5 EJ |
985 | elif el.tagName == "command": |
986 | label = el.getAttribute("label") | |
03fcb5df GS |
987 | if label == '': |
988 | label = None | |
4a39a40a | 989 | binary = getBoolAttr(el, 'binary') |
8979dce3 DPP |
990 | try: |
991 | repeat_count = int(el.getAttribute("repeat")) | |
992 | if repeat_count > 1: | |
993 | cmd_output(dir, | |
994 | DATE + ';' + getText(el.childNodes), | |
995 | label, binary=binary, | |
996 | repeat_count=repeat_count) | |
997 | else: | |
998 | cmd_output(dir, getText(el.childNodes), | |
999 | label, binary=binary, | |
1000 | repeat_count=repeat_count) | |
1001 | except: | |
1002 | cmd_output(dir, getText(el.childNodes), label, | |
1003 | binary=binary) | |
03fcb5df | 1004 | |
b828c2f5 | 1005 | |
cfc693d6 | 1006 | def make_tar(subdir, suffix, output_fd, output_file): |
b828c2f5 EJ |
1007 | global SILENT_MODE, data |
1008 | ||
1009 | mode = 'w' | |
1010 | if suffix == 'tar.bz2': | |
1011 | mode = 'w:bz2' | |
f9c71a15 SHL |
1012 | elif suffix == 'tar.gz': |
1013 | mode = 'w:gz' | |
b828c2f5 EJ |
1014 | |
1015 | if output_fd == -1: | |
cfc693d6 SHL |
1016 | if output_file is None: |
1017 | filename = "%s/%s.%s" % (BUG_DIR, subdir, suffix) | |
1018 | else: | |
1019 | filename = output_file | |
588d807c | 1020 | old_umask = os.umask(0o077) |
b828c2f5 | 1021 | tf = tarfile.open(filename, mode) |
204f61fb | 1022 | os.umask(old_umask) |
b828c2f5 EJ |
1023 | else: |
1024 | tf = tarfile.open(None, 'w', os.fdopen(output_fd, 'a')) | |
1025 | ||
1026 | try: | |
1027 | for (k, v) in data.items(): | |
1028 | try: | |
1029 | tar_filename = os.path.join(subdir, construct_filename(k, v)) | |
1030 | ti = tarfile.TarInfo(tar_filename) | |
1031 | ||
1032 | ti.uname = 'root' | |
1033 | ti.gname = 'root' | |
1034 | ||
b0195f47 | 1035 | if 'output' in v: |
b828c2f5 EJ |
1036 | ti.mtime = v['output'].mtime |
1037 | ti.size = len(v['output'].getvalue()) | |
1038 | v['output'].seek(0) | |
1039 | tf.addfile(ti, v['output']) | |
b0195f47 | 1040 | elif 'filename' in v: |
b828c2f5 EJ |
1041 | s = os.stat(v['filename']) |
1042 | ti.mtime = s.st_mtime | |
1043 | ti.size = s.st_size | |
9e6c00bc | 1044 | tf.addfile(ti, open(v['filename'], 'rb')) |
b828c2f5 EJ |
1045 | except: |
1046 | pass | |
1047 | finally: | |
1048 | tf.close() | |
1049 | ||
1050 | if output_fd == -1: | |
03fcb5df | 1051 | output('Writing tarball %s successful.' % filename) |
b828c2f5 | 1052 | if SILENT_MODE: |
588d807c | 1053 | print(filename) |
b828c2f5 EJ |
1054 | |
1055 | ||
cfc693d6 | 1056 | def make_zip(subdir, output_file): |
b828c2f5 EJ |
1057 | global SILENT_MODE, data |
1058 | ||
cfc693d6 SHL |
1059 | if output_file is None: |
1060 | filename = "%s/%s.zip" % (BUG_DIR, subdir) | |
1061 | else: | |
1062 | filename = output_file | |
588d807c | 1063 | old_umask = os.umask(0o077) |
b828c2f5 | 1064 | zf = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED) |
204f61fb | 1065 | os.umask(old_umask) |
b828c2f5 EJ |
1066 | |
1067 | try: | |
1068 | for (k, v) in data.items(): | |
1069 | try: | |
1070 | dest = os.path.join(subdir, construct_filename(k, v)) | |
1071 | ||
b0195f47 | 1072 | if 'output' in v: |
b828c2f5 EJ |
1073 | zf.writestr(dest, v['output'].getvalue()) |
1074 | else: | |
1075 | if os.stat(v['filename']).st_size < 50: | |
1076 | compress_type = zipfile.ZIP_STORED | |
1077 | else: | |
1078 | compress_type = zipfile.ZIP_DEFLATED | |
1079 | zf.write(v['filename'], dest, compress_type) | |
1080 | except: | |
1081 | pass | |
1082 | finally: | |
1083 | zf.close() | |
b592e726 | 1084 | |
03fcb5df | 1085 | output('Writing archive %s successful.' % filename) |
b828c2f5 | 1086 | if SILENT_MODE: |
588d807c | 1087 | print(filename) |
b828c2f5 EJ |
1088 | |
1089 | ||
1090 | def make_inventory(inventory, subdir): | |
1091 | document = getDOMImplementation().createDocument( | |
1092 | None, INVENTORY_XML_ROOT, None) | |
1093 | ||
1094 | # create summary entry | |
1095 | s = document.createElement(INVENTORY_XML_SUMMARY) | |
1096 | user = os.getenv('SUDO_USER', os.getenv('USER')) | |
1097 | if user: | |
1098 | s.setAttribute('user', user) | |
1099 | s.setAttribute('date', time.strftime('%c')) | |
1100 | s.setAttribute('hostname', platform.node()) | |
1101 | s.setAttribute('uname', ' '.join(platform.uname())) | |
9e6c00bc | 1102 | s.setAttribute('uptime', check_output(UPTIME).decode()) |
b828c2f5 EJ |
1103 | document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(s) |
1104 | ||
99c74883 | 1105 | map(lambda k_v: inventory_entry(document, subdir, k_v[0], k_v[1]), |
b828c2f5 | 1106 | inventory.items()) |
9e6c00bc | 1107 | return document.toprettyxml().encode() |
b828c2f5 | 1108 | |
03fcb5df | 1109 | |
b828c2f5 EJ |
1110 | def inventory_entry(document, subdir, k, v): |
1111 | try: | |
1112 | el = document.createElement(INVENTORY_XML_ELEMENT) | |
1113 | el.setAttribute('capability', v['cap']) | |
03fcb5df GS |
1114 | el.setAttribute('filename', |
1115 | os.path.join(subdir, construct_filename(k, v))) | |
1134de88 | 1116 | el.setAttribute('sha256sum', sha256(v)) |
b828c2f5 EJ |
1117 | document.getElementsByTagName(INVENTORY_XML_ROOT)[0].appendChild(el) |
1118 | except: | |
1119 | pass | |
1120 | ||
1121 | ||
1134de88 BP |
1122 | def sha256(d): |
1123 | m = hashlib.sha256() | |
b0195f47 | 1124 | if 'filename' in d: |
b828c2f5 EJ |
1125 | f = open(d['filename']) |
1126 | data = f.read(1024) | |
1127 | while len(data) > 0: | |
1128 | m.update(data) | |
1129 | data = f.read(1024) | |
1130 | f.close() | |
b0195f47 | 1131 | elif 'output' in d: |
b828c2f5 EJ |
1132 | m.update(d['output'].getvalue()) |
1133 | return m.hexdigest() | |
1134 | ||
1135 | ||
1136 | def construct_filename(k, v): | |
b0195f47 | 1137 | if 'filename' in v: |
b828c2f5 EJ |
1138 | if v['filename'][0] == '/': |
1139 | return v['filename'][1:] | |
1140 | else: | |
1141 | return v['filename'] | |
1142 | s = k.replace(' ', '-') | |
1143 | s = s.replace('--', '-') | |
1144 | s = s.replace('/', '%') | |
1145 | if s.find('.') == -1: | |
1146 | s += '.out' | |
1147 | ||
1148 | return s | |
1149 | ||
03fcb5df | 1150 | |
b828c2f5 EJ |
1151 | def update_capabilities(): |
1152 | pass | |
1153 | ||
03fcb5df | 1154 | |
b828c2f5 EJ |
1155 | def update_cap_size(cap, size): |
1156 | update_cap(cap, MIN_SIZE, size) | |
1157 | update_cap(cap, MAX_SIZE, size) | |
1158 | update_cap(cap, CHECKED, size > 0) | |
1159 | ||
1160 | ||
1161 | def update_cap(cap, k, v): | |
1162 | global caps | |
593e93e5 AGS |
1163 | temp = list(caps[cap]) |
1164 | temp[k] = v | |
1165 | caps[cap] = tuple(temp) | |
b828c2f5 EJ |
1166 | |
1167 | ||
b592e726 | 1168 | def size_of_dir(d, pattern=None, negate=False): |
b828c2f5 EJ |
1169 | if os.path.isdir(d): |
1170 | return size_of_all([os.path.join(d, fn) for fn in os.listdir(d)], | |
1171 | pattern, negate) | |
1172 | else: | |
1173 | return 0 | |
1174 | ||
1175 | ||
b592e726 | 1176 | def size_of_all(files, pattern=None, negate=False): |
b828c2f5 EJ |
1177 | return sum([size_of(f, pattern, negate) for f in files]) |
1178 | ||
1179 | ||
1180 | def matches(f, pattern, negate): | |
1181 | if negate: | |
1182 | return not matches(f, pattern, False) | |
1183 | else: | |
1184 | return pattern is None or pattern.match(f) | |
1185 | ||
1186 | ||
1187 | def size_of(f, pattern, negate): | |
1188 | if os.path.isfile(f) and matches(f, pattern, negate): | |
1189 | return os.stat(f)[6] | |
1190 | else: | |
1191 | return size_of_dir(f, pattern, negate) | |
1192 | ||
1193 | ||
1194 | def print_capabilities(): | |
1195 | document = getDOMImplementation().createDocument( | |
1196 | "ns", CAP_XML_ROOT, None) | |
03fcb5df GS |
1197 | map(lambda key: capability(document, key), |
1198 | [k for k in caps.keys() if not caps[k][HIDDEN]]) | |
588d807c | 1199 | print(document.toprettyxml()) |
b828c2f5 | 1200 | |
03fcb5df | 1201 | |
b828c2f5 EJ |
1202 | def capability(document, key): |
1203 | c = caps[key] | |
1204 | el = document.createElement(CAP_XML_ELEMENT) | |
1205 | el.setAttribute('key', c[KEY]) | |
1206 | el.setAttribute('pii', c[PII]) | |
1207 | el.setAttribute('min-size', str(c[MIN_SIZE])) | |
1208 | el.setAttribute('max-size', str(c[MAX_SIZE])) | |
1209 | el.setAttribute('min-time', str(c[MIN_TIME])) | |
1210 | el.setAttribute('max-time', str(c[MAX_TIME])) | |
1211 | el.setAttribute('content-type', c[MIME]) | |
1212 | el.setAttribute('default-checked', c[CHECKED] and 'yes' or 'no') | |
1213 | document.getElementsByTagName(CAP_XML_ROOT)[0].appendChild(el) | |
1214 | ||
1215 | ||
1216 | def prettyDict(d): | |
1217 | format = '%%-%ds: %%s' % max(map(len, [k for k, _ in d.items()])) | |
1218 | return '\n'.join([format % i for i in d.items()]) + '\n' | |
1219 | ||
1220 | ||
1221 | def yes(prompt): | |
99c74883 | 1222 | yn = input(prompt) |
b828c2f5 EJ |
1223 | |
1224 | return len(yn) == 0 or yn.lower()[0] == 'y' | |
1225 | ||
1226 | ||
1227 | partition_re = re.compile(r'(.*[0-9]+$)|(^xvd)') | |
1228 | ||
03fcb5df | 1229 | |
b828c2f5 EJ |
1230 | def disk_list(): |
1231 | disks = [] | |
1232 | try: | |
1233 | f = open('/proc/partitions') | |
1234 | f.readline() | |
1235 | f.readline() | |
1236 | for line in f.readlines(): | |
1237 | (major, minor, blocks, name) = line.split() | |
1238 | if int(major) < 254 and not partition_re.match(name): | |
1239 | disks.append(name) | |
1240 | f.close() | |
1241 | except: | |
1242 | pass | |
1243 | return disks | |
1244 | ||
1245 | ||
588d807c | 1246 | class ProcOutput(object): |
b828c2f5 EJ |
1247 | debug = False |
1248 | ||
03fcb5df GS |
1249 | def __init__(self, command, max_time, inst=None, filter=None, |
1250 | binary=False): | |
b828c2f5 EJ |
1251 | self.command = command |
1252 | self.max_time = max_time | |
1253 | self.inst = inst | |
1254 | self.running = False | |
1255 | self.status = None | |
1256 | self.timed_out = False | |
1257 | self.failed = False | |
1258 | self.timeout = int(time.time()) + self.max_time | |
1259 | self.filter = filter | |
1260 | self.filter_state = {} | |
4a39a40a SHL |
1261 | if binary: |
1262 | self.bufsize = 1048576 # 1MB buffer | |
1263 | else: | |
1264 | self.bufsize = 1 # line buffered | |
b828c2f5 EJ |
1265 | |
1266 | def __del__(self): | |
1267 | self.terminate() | |
1268 | ||
1269 | def cmdAsStr(self): | |
03fcb5df GS |
1270 | return isinstance(self.command, list) \ |
1271 | and ' '.join(self.command) or self.command | |
b828c2f5 EJ |
1272 | |
1273 | def run(self): | |
1274 | self.timed_out = False | |
1275 | try: | |
1276 | if ProcOutput.debug: | |
1277 | output_ts("Starting '%s'" % self.cmdAsStr()) | |
4a39a40a SHL |
1278 | self.proc = Popen(self.command, bufsize=self.bufsize, |
1279 | stdin=dev_null, stdout=PIPE, stderr=dev_null, | |
1280 | shell=isinstance(self.command, str)) | |
b828c2f5 | 1281 | old = fcntl.fcntl(self.proc.stdout.fileno(), fcntl.F_GETFD) |
03fcb5df GS |
1282 | fcntl.fcntl(self.proc.stdout.fileno(), |
1283 | fcntl.F_SETFD, old | fcntl.FD_CLOEXEC) | |
b828c2f5 EJ |
1284 | self.running = True |
1285 | self.failed = False | |
1286 | except: | |
1287 | output_ts("'%s' failed" % self.cmdAsStr()) | |
1288 | self.running = False | |
1289 | self.failed = True | |
1290 | ||
1291 | def terminate(self): | |
1292 | if self.running: | |
1293 | try: | |
a122b0fa | 1294 | self.proc.stdout.close() |
b828c2f5 EJ |
1295 | os.kill(self.proc.pid, SIGTERM) |
1296 | except: | |
1297 | pass | |
1298 | self.proc = None | |
1299 | self.running = False | |
1300 | self.status = SIGTERM | |
1301 | ||
1302 | def read_line(self): | |
1303 | assert self.running | |
4a39a40a SHL |
1304 | if self.bufsize == 1: |
1305 | line = self.proc.stdout.readline() | |
1306 | else: | |
1307 | line = self.proc.stdout.read(self.bufsize) | |
9e6c00bc | 1308 | if line == b'': |
b828c2f5 | 1309 | # process exited |
a122b0fa | 1310 | self.proc.stdout.close() |
b828c2f5 EJ |
1311 | self.status = self.proc.wait() |
1312 | self.proc = None | |
1313 | self.running = False | |
1314 | else: | |
1315 | if self.filter: | |
1316 | line = self.filter(line, self.filter_state) | |
1317 | if self.inst: | |
1318 | self.inst.write(line) | |
1319 | ||
03fcb5df | 1320 | |
b828c2f5 EJ |
1321 | def run_procs(procs): |
1322 | while True: | |
1323 | pipes = [] | |
1324 | active_procs = [] | |
1325 | ||
1326 | for pp in procs: | |
1327 | for p in pp: | |
1328 | if p.running: | |
1329 | active_procs.append(p) | |
1330 | pipes.append(p.proc.stdout) | |
1331 | break | |
69be0a8d | 1332 | elif p.status is None and not p.failed and not p.timed_out: |
b828c2f5 EJ |
1333 | p.run() |
1334 | if p.running: | |
1335 | active_procs.append(p) | |
1336 | pipes.append(p.proc.stdout) | |
1337 | break | |
1338 | ||
1339 | if len(pipes) == 0: | |
1340 | # all finished | |
1341 | break | |
1342 | ||
1343 | (i, o, x) = select(pipes, [], [], 1.0) | |
1344 | now = int(time.time()) | |
1345 | ||
1346 | # handle process output | |
1347 | for p in active_procs: | |
1348 | if p.proc.stdout in i: | |
1349 | p.read_line() | |
1350 | ||
1351 | # handle timeout | |
1352 | if p.running and now > p.timeout: | |
1353 | output_ts("'%s' timed out" % p.cmdAsStr()) | |
1354 | if p.inst: | |
c57e02cf | 1355 | p.inst.write("\n** timeout **\n".encode()) |
b828c2f5 EJ |
1356 | p.timed_out = True |
1357 | p.terminate() | |
1358 | ||
1359 | ||
1360 | def pidof(name): | |
1361 | pids = [] | |
1362 | ||
1363 | for d in [p for p in os.listdir('/proc') if p.isdigit()]: | |
1364 | try: | |
1365 | if os.path.basename(os.readlink('/proc/%s/exe' % d)) == name: | |
1366 | pids.append(int(d)) | |
1367 | except: | |
1368 | pass | |
1369 | ||
1370 | return pids | |
1371 | ||
1372 | ||
4e0abe6e SHL |
1373 | def check_space(cap, name, size): |
1374 | global free_disk_space | |
1375 | if free_disk_space is not None and size > free_disk_space: | |
1376 | output("Omitting %s, out of disk space (requested: %u, allowed: %u)" % | |
1377 | (name, size, free_disk_space)) | |
1378 | return False | |
1379 | elif unlimited_data or caps[cap][MAX_SIZE] == -1 or \ | |
1380 | cap_sizes[cap] < caps[cap][MAX_SIZE]: | |
1381 | cap_sizes[cap] += size | |
1382 | if free_disk_space is not None: | |
1383 | free_disk_space -= size | |
1384 | return True | |
1385 | else: | |
1386 | output("Omitting %s, size constraint of %s exceeded" % (name, cap)) | |
1387 | return False | |
1388 | ||
1389 | ||
1390 | def get_free_disk_space(path): | |
1391 | path = os.path.abspath(path) | |
1392 | while not os.path.exists(path): | |
1393 | path = os.path.dirname(path) | |
1394 | s = os.statvfs(path) | |
1395 | return s.f_frsize * s.f_bfree | |
1396 | ||
1397 | ||
9e6c00bc TR |
1398 | class BytesIOmtime(BytesIO): |
1399 | def __init__(self, buf=b''): | |
1400 | BytesIO.__init__(self, buf) | |
b828c2f5 EJ |
1401 | self.mtime = time.time() |
1402 | ||
1403 | def write(self, s): | |
9e6c00bc | 1404 | BytesIO.write(self, s) |
b828c2f5 EJ |
1405 | self.mtime = time.time() |
1406 | ||
1407 | ||
1408 | if __name__ == "__main__": | |
1409 | try: | |
1410 | sys.exit(main()) | |
1411 | except KeyboardInterrupt: | |
588d807c | 1412 | print("\nInterrupted.") |
b828c2f5 | 1413 | sys.exit(3) |