-
-
Notifications
You must be signed in to change notification settings - Fork 101
Expand file tree
/
Copy pathutils.py
More file actions
334 lines (292 loc) · 11.6 KB
/
Copy pathutils.py
File metadata and controls
334 lines (292 loc) · 11.6 KB
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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# Copyright 2012 VPAC, http://www.vpac.org
# Copyright 2013-2025 Marcus Furlong <furlongm@gmail.com>
#
# This file is part of Patchman.
#
# Patchman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 only.
#
# Patchman 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 Patchman. If not, see <http://www.gnu.org/licenses/>
import re
from io import BytesIO
from defusedxml import ElementTree
from django.db import IntegrityError
from django.db.models import Q
from tenacity import RetryError
from packages.models import Package
from packages.utils import (
convert_package_to_packagestring, convert_packagestring_to_package,
)
from patchman.signals import pbar_start, pbar_update
from util import (
Checksum, extract, fetch_content, get_checksum, get_setting_of_type,
get_url, response_is_valid,
)
from util.logging import (
debug_message, error_message, info_message, warning_message,
)
def get_or_create_repo(r_name, r_arch, r_type, r_id=None):
""" Get or create a Repository object and returns the object.
Returns None if it cannot get or create the object.
"""
from repos.models import Repository
try:
repository, c = Repository.objects.get_or_create(name=r_name, arch=r_arch, repotype=r_type)
except IntegrityError:
repository = Repository.objects.get(name=r_name, arch=r_arch, repotype=r_type)
if repository:
if r_id:
repository.repo_id = r_id
repository.save()
return repository
def update_mirror_packages(mirror, packages):
""" Updates the packages contained on a mirror, and
removes obsolete packages.
"""
from repos.models import MirrorPackage # noqa
old = set()
mirror_packages = mirror.packages.all()
plen = mirror_packages.count()
pbar_start.send(sender=None, ptext=f'Fetching {plen} existing Packages', plen=plen)
for i, package in enumerate(mirror_packages):
pbar_update.send(sender=None, index=i + 1)
strpackage = convert_package_to_packagestring(package)
old.add(strpackage)
removals = old.difference(packages)
rlen = len(removals)
pbar_start.send(sender=None, ptext=f'Removing {rlen} obsolete Packages', plen=rlen)
for i, strpackage in enumerate(removals):
pbar_update.send(sender=None, index=i + 1)
package = convert_packagestring_to_package(strpackage)
MirrorPackage.objects.filter(mirror=mirror, package=package).delete()
new = packages.difference(old)
nlen = len(new)
pbar_start.send(sender=None, ptext=f'Adding {nlen} new Packages', plen=nlen)
for i, strpackage in enumerate(new):
pbar_update.send(sender=None, index=i + 1)
try:
package = convert_packagestring_to_package(strpackage)
mirror_package, c = MirrorPackage.objects.get_or_create(mirror=mirror, package=package)
except Package.MultipleObjectsReturned:
error_message(text=f'Duplicate Package found in {mirror}: {strpackage}')
def find_mirror_url(stored_mirror_url, formats):
""" Find the actual URL of the mirror by trying predefined paths
"""
for fmt in formats:
mirror_url = stored_mirror_url
for f in formats:
if mirror_url.endswith(f):
mirror_url = mirror_url[:-len(f)]
mirror_url = f"{mirror_url.rstrip('/')}/{fmt}"
debug_message(text=f'Checking for Mirror at {mirror_url}')
try:
res = get_url(mirror_url)
except RetryError:
continue
if res is not None and res.ok:
return res
def is_metalink(url):
""" Checks if a given url is a metalink url
"""
return 'metalink?' in url.lower()
def get_metalink_urls(url):
""" Parses a metalink and returns a list of mirrors
"""
try:
res = get_url(url)
except RetryError:
return
if not response_is_valid(res):
return
if not res.headers.get('content-type') == 'application/metalink+xml':
return
metalink_urls = []
data = fetch_content(res, 'Fetching metalink data')
extracted = extract(data, url)
ns = 'http://www.metalinker.org/'
try:
tree = ElementTree.parse(BytesIO(extracted))
root = tree.getroot()
for child in root:
if child.tag == f'{{{ns}}}files':
for grandchild in child:
if grandchild.tag == f'{{{ns}}}file':
for greatgrandchild in grandchild:
if greatgrandchild.tag == f'{{{ns}}}resources':
for greatgreatgrandchild in greatgrandchild:
if greatgreatgrandchild.tag == f'{{{ns}}}url':
if greatgreatgrandchild.attrib.get('protocol') in ['https', 'http']:
metalink_urls.append(greatgreatgrandchild.text)
except ElementTree.ParseError as e:
error_message(text=f'Error parsing metalink {url}: {e}')
return metalink_urls
def get_mirrorlist_urls(url):
""" Checks if a given url returns a mirrorlist by checking if it contains
a list of urls. Returns a list of mirrors if it is a mirrorlist.
"""
try:
res = get_url(url)
except RetryError:
return
if response_is_valid(res):
try:
data = fetch_content(res, 'Fetching Repo data to check for mirrorlist')
if data is None:
return
mirror_urls = re.findall(r'^http[s]*://.*$|^ftp://.*$', data.decode('utf-8'), re.MULTILINE)
if mirror_urls:
debug_message(text=f'Found mirrorlist: {url}')
return mirror_urls
else:
debug_message(text=f'Not a mirrorlist: {url}')
except Exception as e:
error_message(text=f'Error attempting to parse a mirrorlist: {e} {url}')
def add_mirrors_from_urls(repo, mirror_urls):
""" Creates mirrors from a list of mirror urls
"""
max_mirrors = get_max_mirrors()
for mirror_url in mirror_urls:
mirror_url = mirror_url.replace('$ARCH', repo.arch.name)
mirror_url = mirror_url.replace('$basearch', repo.arch.name)
mirror_url = mirror_url.rstrip('/')
q = Q(mirrorlist=False, refresh=True, enabled=True)
existing = repo.mirror_set.filter(q).count()
if existing >= max_mirrors:
text = f'{existing} Mirrors already exist (max={max_mirrors}), not adding more'
warning_message(text=text)
break
from repos.models import Mirror
# FIXME: maybe we should store the mirrorlist url with full path to repomd.xml?
# that is what metalink urls return now
m, c = Mirror.objects.get_or_create(repo=repo, url=mirror_url.rstrip('/').replace('repodata/repomd.xml', ''))
if c:
text = f'Added Mirror - {mirror_url}'
info_message(text=text)
def check_for_mirrorlists(repo):
""" Check if any of the mirrors are actually mirrorlists.
Creates MAX_MIRRORS mirrors from list if so.
"""
for mirror in repo.mirror_set.all():
if not mirror.url.startswith(('http://', 'https://')):
warning_message(text=f'Skipping non-http(s) mirror URL: {mirror.url}')
continue
mirror_urls = get_mirrorlist_urls(mirror.url)
if mirror_urls:
mirror.mirrorlist = True
mirror.last_access_ok = True
mirror.save()
info_message(text=f'Found mirrorlist - {mirror.url}')
add_mirrors_from_urls(repo, mirror_urls)
def check_for_metalinks(repo):
""" Checks a set of mirrors for metalinks and creates
MAX_MIRRORS mirrors if so.
"""
for mirror in repo.mirror_set.all():
if is_metalink(mirror.url):
mirror_urls = get_metalink_urls(mirror.url)
else:
continue
if mirror_urls:
mirror.mirrorlist = True
mirror.last_access_ok = True
mirror.save()
info_message(text=f'Found metalink - {mirror.url}')
add_mirrors_from_urls(repo, mirror_urls)
def fetch_mirror_data(mirror, url, text, checksum=None, checksum_type=None, metadata_type=None):
if not url:
mirror.fail()
return
try:
res = get_url(url)
except RetryError:
mirror.fail()
return
if not response_is_valid(res):
mirror.fail()
return
mirror.last_access_ok = True
mirror.save()
data = fetch_content(res, text)
if not data:
return
if checksum and checksum_type and metadata_type:
computed_checksum = get_checksum(data, Checksum[checksum_type])
if not mirror_checksum_is_valid(computed_checksum, checksum, mirror, metadata_type):
mirror.fail()
return
return data
def mirror_checksum_is_valid(computed, provided, mirror, metadata_type):
""" Compares the computed checksum and the provided checksum.
Returns True if both match.
"""
if not computed or computed != provided:
text = f'Checksum failed for mirror {mirror.id}, not refreshing {metadata_type} metadata'
error_message(text=text)
text = f'Found checksum: {computed}\nExpected checksum: {provided}'
error_message(text=text)
mirror.last_access_ok = False
mirror.fail()
return False
else:
return True
def find_best_repo(package, hostrepos):
""" Given a package and a set of HostRepos, determine the best
repo. Returns the best repo.
"""
best_repo = None
package_repos = hostrepos.filter(repo__mirror__packages=package).select_related('repo').distinct()
package_repos = list(package_repos)
if package_repos:
best_repo = package_repos[0]
if len(package_repos) > 1:
for hostrepo in package_repos:
if hostrepo.repo.security:
best_repo = hostrepo
else:
if hostrepo.priority > best_repo.priority:
best_repo = hostrepo
return best_repo
def get_max_mirrors():
""" Find the max number of mirrors for refresh
"""
max_mirrors = get_setting_of_type(
setting_name='MAX_MIRRORS',
setting_type=int,
default=3,
)
return max_mirrors
def clean_repos():
""" Remove repositories that contain no mirrors
"""
from repos.models import Repository
repos = Repository.objects.filter(mirror__isnull=True)
rlen = repos.count()
if rlen == 0:
info_message(text='No Repositories with zero Mirrors found.')
else:
info_message(text=f'Removing {rlen} empty Repositories.')
repos.delete()
def remove_mirror_trailing_slashes():
""" Remove trailing slashes from mirrors, delete duplicates
"""
from repos.models import Mirror
mirrors = Mirror.objects.filter(url__endswith='/')
mlen = mirrors.count()
if mlen == 0:
info_message(text='No Mirrors with trailing slashes found.')
else:
info_message(text=f'Removing trailing slashes from {mlen} Mirrors.')
for mirror in mirrors:
mirror.url = mirror.url.rstrip('/')
try:
mirror.save()
except IntegrityError:
warning_message(text=f'Deleting duplicate Mirror {mirror.id}: {mirror.url}')
mirror.delete()