1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
#!/usr/bin/env python3
#
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import argparse
import glob
from datetime import datetime
from pathlib import Path
from shutil import rmtree
from socket import gethostname
from sys import exit
from tarfile import open as tar_open
from vyos.defaults import directories
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.file import get_name_from_path
from vyos.remote import upload
# Example: bdbdd9a4807f_tech-support-archive_2026-02-02T13-53-18
ARCHIVE_PATTERN = '_tech-support-archive_'
# Example: drops-debug_2026-02-02T13-53-18
ARCHIVE_TMP_DIR_PATTERN = 'drops-debug_'
DEFAULT_TMP_DIR = '/tmp'
EXCLUDED_ARCHIVE_EXT = ('.iso', '.gz', '.tar', '.zip')
def __rotate_logs(path: str, log_pattern:str):
files_list = glob.glob(f'{path}/{log_pattern}')
if len(files_list) > 5:
oldest_file = min(files_list, key=os.path.getctime)
os.remove(oldest_file)
def __save_show_report_files(reports_dir: Path):
"""
Save result of execution `show tech-support report` command
:param reports_dir: path to the result directory
:type reports_dir: pathlib.Path
"""
vyos_op_scripts_dir = directories['op_mode']
script_path = f'{vyos_op_scripts_dir}/show_techsupport_report.py'
arguments = [
'--launched-from-generate-archive',
'--outdir',
str(reports_dir),
]
output = cmd([script_path] + arguments)
if output.strip():
print(output)
def __generate_archived_files(location_path: str) -> None:
"""
Generate archives of main directories
:param location_path: path to temporary directory
:type location_path: str
"""
# sync/flush journald before archiving /var/log/journal
cmd(['journalctl', '--sync'])
cmd(['journalctl', '--flush'])
def __tar_filter(tarinfo):
# path inside tar, because we set arcname=... below
name = tarinfo.name
basename = os.path.basename(name)
# /var/log: exclude /var/log/messages and /var/log/messages.*
if name.startswith('var/log/messages'):
if basename == 'messages' or basename.startswith('messages.'):
return None
# /tmp, /home: exclude previous tech-support archives and temporary archive directories
if name.startswith(('tmp/', 'home/')):
if ARCHIVE_PATTERN in name or basename.startswith(ARCHIVE_TMP_DIR_PATTERN):
return None
# /home, /opt/vyatta/etc/config, /tmp: exclude general archives
if name.startswith(('home/', 'opt/vyatta/etc/config/', 'tmp/')):
if basename.lower().endswith(EXCLUDED_ARCHIVE_EXT):
return None
return tarinfo
# Dictionary archive_name:directory_to_archive
archive_dict = {
'etc': '/etc',
'home': '/home',
'var-log': '/var/log',
'root': '/root',
'tmp': '/tmp',
'core-dump': '/var/core',
'config': '/opt/vyatta/etc/config',
'run': '/run',
}
for archive_name, path in archive_dict.items():
if not os.path.exists(path):
continue
arcname = str(path).lstrip('/') # e.g. /etc -> 'etc'
archive_file = f'{location_path}/{archive_name}.tar.gz'
with tar_open(name=archive_file, mode='x:gz') as tar_file:
try:
tar_file.add(path, arcname=arcname, filter=__tar_filter)
except (PermissionError, OSError) as e:
print(f'Unable to read `{path}` to archive files:', e)
continue # skip paths we can't read
def __generate_main_archive_file(archive_file: str, tmp_dir_path: str) -> None:
"""
Generate main archive file
:param archive_file: name of archive file
:type archive_file: str
:param tmp_dir_path: path to archive member
:type tmp_dir_path: str
"""
arcname = get_name_from_path(archive_file)
with tar_open(name=archive_file, mode='x:gz') as tar_file:
tar_file.add(tmp_dir_path, arcname=arcname)
def __generate_topology_snapshots(output_dir: Path) -> None:
"""
Generates physical and logical topology PNG files using `lstopo`
:param output_dir: directory where topology PNGs will be stored
"""
physical_topo = output_dir / 'topology.png'
logical_topo = output_dir / 'topology-logical.png'
# Capture physical topology
call(['lstopo', '--output-format', 'png', str(physical_topo)])
# Capture logical topology
call(['lstopo', '--logical', '--output-format', 'png', str(logical_topo)])
def __resolve_main_archive_path(input_path: str, default_archive_name: str) -> Path:
"""
Normalize path for saving a .tar.gz file based on rules:
Rules:
- file -> file.tar.gz
- file.tar -> file.tar.gz
- file.tgz -> file.tgz
- dir/ -> dir/{default_archive_name}
- ../dir/file.tar.gz -> (unchanged)
- file.zip -> file.tar.gz
:param input_path: user's provided path to the archive
:param default_archive_name: name of archive if user didn't provide it
"""
path = Path(input_path)
# Case 1: default temporary directory -> extend by default name of file
if input_path == DEFAULT_TMP_DIR:
return path / default_archive_name
# Case 2: already .tar.gz -> return unchanged
if path.name.endswith(('.tar.gz', '.tgz')):
return path
# Case 3: already .tar -> .tar.gz
if path.name.endswith('.tar'):
return path.with_suffix('.tar.gz')
# Case 4: directory (explicit trailing slash OR existing directory)
if input_path.endswith(('/', '\\')) or path.is_dir():
dir_path = path
return dir_path / default_archive_name
# Default behavior for any other extension
return path.with_suffix('.tar.gz')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('path', nargs='?', default=DEFAULT_TMP_DIR)
args = parser.parse_args()
hostname: str = gethostname()
time_now: str = datetime.now().isoformat(timespec='seconds').replace(':', '-')
default_archive_inner_dir = f'{hostname}{ARCHIVE_PATTERN}{time_now}'
default_archive_name = f'{default_archive_inner_dir}.tar.gz'
is_remote = args.path.startswith(('ftp://', 'scp://'))
if is_remote:
base_tmp_path = DEFAULT_TMP_DIR
archive_dest_path = Path(f'{base_tmp_path}/{default_archive_name}')
else:
# Define destination path to the main archive file based on a rules
archive_dest_path = __resolve_main_archive_path(args.path, default_archive_name)
base_tmp_path = str(archive_dest_path.parent)
default_archive_name = archive_dest_path.name
default_archive_inner_dir = get_name_from_path(archive_dest_path.name)
# Log rotation in tmp directory
if base_tmp_path == DEFAULT_TMP_DIR:
__rotate_logs(base_tmp_path, f'*{ARCHIVE_PATTERN}*')
# Temporary directory creation
tmp_dir: Path = Path(f'{base_tmp_path}/{ARCHIVE_TMP_DIR_PATTERN}{time_now}')
tmp_dir.mkdir(parents=True)
# Directory which contains list of 'tech-support' reports
reports_dir: Path = Path(f'{tmp_dir}/show_tech-support_report')
# Call the topology snapshot function here
__generate_topology_snapshots(tmp_dir)
try:
# Generate files using `show tech-support report` command
__save_show_report_files(reports_dir)
# Generate included archives
__generate_archived_files(tmp_dir)
# Generate main archive
__generate_main_archive_file(archive_dest_path, tmp_dir)
# Upload to remote site if it is scpecified
if is_remote:
upload_uri = args.path
upload(str(archive_dest_path), upload_uri)
except Exception as err:
print(f'Error during generating a debug file: {err}')
else:
print(f'Debug file is generated and located in {archive_dest_path}')
finally:
# Delete temporary directory
if tmp_dir.exists():
rmtree(tmp_dir, ignore_errors=True)
exit()
|