pyreposync 0.0.12__tar.gz → 0.2.0__tar.gz

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,9 @@
1
+ .idea
2
+ *__pycache__*
3
+ pyreposync.egg-info
4
+ .eggs
5
+ _data
6
+ deb_mirror
7
+ dist
8
+ venv
9
+ *xml
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyreposync
3
+ Version: 0.2.0
4
+ Summary: Orbit Api
5
+ Project-URL: Source, https://github.com/schlitzered/pyreposync
6
+ Author-email: "Stephan.Schultchen" <sschultchen@gmail.com>
7
+ License: The MIT License (MIT)
8
+
9
+ Copyright (c) 2015 Stephan Schultchen
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in
19
+ all copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
+ THE SOFTWARE.
28
+ License-File: LICENSE.txt
29
+ Classifier: Programming Language :: Python
30
+ Requires-Python: >=3.9
31
+ Requires-Dist: requests
@@ -0,0 +1,15 @@
1
+ [main]
2
+ destination = /home/schlitzer/PycharmProjects/pyreposync/_data
3
+ downloaders = 10
4
+ log = /home/schlitzer/PycharmProjects/pyreposync/_data/pyreposync.log
5
+ logretention = 7
6
+ loglevel = INFO
7
+ #proxy = http://proxy.example.com:3128
8
+
9
+ [AlmaLinux/9/AppStream/x86_64:rpm]
10
+ baseurl = https://mirror.23m.com/almalinux/9/AppStream/x86_64/os/
11
+ tags = EXTERNAL,OS:AlmaLinux9,BASE,X86_64
12
+
13
+ [AlmaLinux/9/BaseOS/x86_64:rpm]
14
+ baseurl = https://mirror.23m.com/almalinux/9/BaseOS/x86_64/os/
15
+ tags = EXTERNAL,OS:AlmaLinux9,BASE,X86_64
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-requirements-txt"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyreposync"
7
+ version = "0.2.0"
8
+ requires-python = ">=3.9"
9
+ authors = [
10
+ {name = "Stephan.Schultchen", email = "sschultchen@gmail.com"},
11
+ ]
12
+ description = "Orbit Api"
13
+ dynamic = ["dependencies"]
14
+ license = {file = "LICENSE.txt"}
15
+ keywords = []
16
+ classifiers = [
17
+ "Programming Language :: Python"
18
+ ]
19
+
20
+ [project.scripts]
21
+ pyreposync = "pyreposync:main"
22
+
23
+ [project.urls]
24
+ Source = "https://github.com/schlitzered/pyreposync"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["pyreposync"]
28
+
29
+ [tool.hatch.metadata.hooks.requirements_txt]
30
+ files = ["requirements.txt"]
31
+
@@ -0,0 +1,511 @@
1
+ import argparse
2
+ import collections
3
+ import configparser
4
+ import datetime
5
+ import logging
6
+ import sys
7
+ import threading
8
+ import time
9
+
10
+ from pyreposync.downloader import Downloader
11
+ from pyreposync.exceptions import OSRepoSyncException, OSRepoSyncHashError
12
+ from pyreposync.sync_rpm import SyncRPM
13
+ from pyreposync.sync_deb import SyncDeb
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(description="OS Repo Sync Tool")
18
+
19
+ parser.add_argument(
20
+ "--cfg",
21
+ dest="cfg",
22
+ action="store",
23
+ default="/etc/pyreposync/reposync.ini",
24
+ help="Full path to configuration",
25
+ )
26
+
27
+ parser.add_argument(
28
+ "--repo",
29
+ dest="repo",
30
+ action="store",
31
+ default=None,
32
+ help="""
33
+ execute command on this repository, if not set,
34
+ command applies to all configured repositories.
35
+
36
+ This command is mutually exclusive with --tags
37
+ """,
38
+ )
39
+
40
+ parser.add_argument(
41
+ "--tags",
42
+ dest="tags",
43
+ action="store",
44
+ default=None,
45
+ help="""
46
+ Comma separated list of repo tags the command applies to.
47
+ putting a '!' in front of a tag negates it.
48
+ At least one not negated tag has to be match.
49
+
50
+ This command is mutually exclusive with --repo
51
+ """,
52
+ )
53
+
54
+ subparsers = parser.add_subparsers(
55
+ help="commands",
56
+ dest="method",
57
+ )
58
+ subparsers.required = True
59
+
60
+ migrate_parser = subparsers.add_parser(
61
+ "migrate",
62
+ help="migrate to new sync format",
63
+ )
64
+ migrate_parser.set_defaults(
65
+ method="migrate",
66
+ )
67
+
68
+ snap_cleanup_parser = subparsers.add_parser(
69
+ "snap_cleanup",
70
+ help="remove all unnamed snapshots and unreferenced rpms.",
71
+ )
72
+ snap_cleanup_parser.set_defaults(
73
+ method="snap_cleanup",
74
+ )
75
+
76
+ snap_list_parser = subparsers.add_parser(
77
+ "snap_list",
78
+ help="list snapshots",
79
+ )
80
+ snap_list_parser.set_defaults(
81
+ method="snap_list",
82
+ )
83
+
84
+ snap_name_parser = subparsers.add_parser(
85
+ "snap_name",
86
+ help="give timed snapshot a name",
87
+ )
88
+ snap_name_parser.set_defaults(
89
+ method="snap_name",
90
+ )
91
+ snap_name_parser.add_argument(
92
+ "--timestamp",
93
+ dest="timestamp",
94
+ action="store",
95
+ required=True,
96
+ default=None,
97
+ help="source timestampm might also be a named snapshot or latest",
98
+ )
99
+ snap_name_parser.add_argument(
100
+ "--name",
101
+ dest="snapname",
102
+ action="store",
103
+ required=True,
104
+ default=None,
105
+ help="name to be created",
106
+ )
107
+
108
+ snap_unname_parser = subparsers.add_parser(
109
+ "snap_unname",
110
+ help="remove name from timed snapshot",
111
+ )
112
+ snap_unname_parser.set_defaults(
113
+ method="snap_unname",
114
+ )
115
+ snap_unname_parser.add_argument(
116
+ "--name",
117
+ dest="snapname",
118
+ action="store",
119
+ required=True,
120
+ help="name to be removed",
121
+ )
122
+
123
+ snap_parser = subparsers.add_parser(
124
+ "snap",
125
+ help="create new snapshots",
126
+ )
127
+ snap_parser.set_defaults(
128
+ method="snap",
129
+ )
130
+
131
+ sync_parser = subparsers.add_parser(
132
+ "sync",
133
+ help="sync all repos",
134
+ )
135
+ sync_parser.set_defaults(
136
+ method="sync",
137
+ )
138
+
139
+ validate_parser = subparsers.add_parser(
140
+ "validate",
141
+ help="re validate package downloads",
142
+ )
143
+ validate_parser.set_defaults(
144
+ method="validate",
145
+ )
146
+
147
+ parsed_args = parser.parse_args()
148
+ try:
149
+ snapname = parsed_args.snapname
150
+ except AttributeError:
151
+ snapname = None
152
+ try:
153
+ timestamp = parsed_args.timestamp
154
+ except AttributeError:
155
+ timestamp = None
156
+
157
+ osreposync = PyRepoSync(
158
+ cfg=parsed_args.cfg,
159
+ method=parsed_args.method,
160
+ snapname=snapname,
161
+ repo=parsed_args.repo,
162
+ tags=parsed_args.tags,
163
+ timestamp=timestamp,
164
+ )
165
+ osreposync.work()
166
+
167
+
168
+ class PyRepoSync:
169
+ def __init__(self, cfg, snapname, method, repo, tags, timestamp):
170
+ self._config_file = cfg
171
+ self._config = configparser.ConfigParser()
172
+ self._config_dict = None
173
+ self._method = method
174
+ self._snapname = snapname
175
+ self._repo = repo
176
+ self._tags = None
177
+ self._timestamp = timestamp
178
+ self.tags = tags
179
+ self.log = logging.getLogger("application")
180
+ self.config.read_file(open(self._config_file))
181
+ self._config_dict = self._cfg_to_dict(self.config)
182
+ self._logging()
183
+ if self._tags and self._repo:
184
+ self.log.fatal("both tags & repo have been specified, choose one")
185
+
186
+ @property
187
+ def method(self):
188
+ return self._method
189
+
190
+ @property
191
+ def snapname(self):
192
+ return self._snapname
193
+
194
+ @property
195
+ def repo(self):
196
+ return self._repo
197
+
198
+ @property
199
+ def tags(self):
200
+ return self._tags
201
+
202
+ @tags.setter
203
+ def tags(self, tags):
204
+ if tags:
205
+ self._tags = tags.split(",")
206
+
207
+ @property
208
+ def timestamp(self):
209
+ return self._timestamp
210
+
211
+ def _logging(self):
212
+ logfmt = logging.Formatter(
213
+ "%(asctime)sUTC - %(levelname)s - %(threadName)s - %(message)s"
214
+ )
215
+ logfmt.converter = time.gmtime
216
+ aap_level = self.config.get("main", "loglevel")
217
+ handler = logging.StreamHandler()
218
+
219
+ handler.setFormatter(logfmt)
220
+ self.log.addHandler(handler)
221
+ self.log.setLevel(aap_level)
222
+ self.log.debug("logger is up")
223
+
224
+ @staticmethod
225
+ def _cfg_to_dict(config):
226
+ result = {}
227
+ for section in config.sections():
228
+ result[section] = {}
229
+ for option in config.options(section):
230
+ try:
231
+ result[section][option] = config.getint(section, option)
232
+ continue
233
+ except ValueError:
234
+ pass
235
+ try:
236
+ result[section][option] = config.getfloat(section, option)
237
+ continue
238
+ except ValueError:
239
+ pass
240
+ try:
241
+ result[section][option] = config.getboolean(section, option)
242
+ continue
243
+ except ValueError:
244
+ pass
245
+ try:
246
+ result[section][option] = config.get(section, option)
247
+ continue
248
+ except ValueError:
249
+ pass
250
+ return result
251
+
252
+ @property
253
+ def config(self):
254
+ return self._config
255
+
256
+ @property
257
+ def config_dict(self):
258
+ return self._config_dict
259
+
260
+ def get_job(self, date, section):
261
+ self.log.info(f"section name: {section}")
262
+ if section.endswith(":rpm"):
263
+ return SyncRPM(
264
+ base_url=self.config.get(section, "baseurl"),
265
+ destination=self.config.get("main", "destination"),
266
+ reponame=section[:-4],
267
+ date=date,
268
+ treeinfo=self.config.get(section, "treeinfo", fallback=".treeinfo"),
269
+ proxy=self.config.get("main", "proxy", fallback=None),
270
+ client_cert=self.config.get(section, "sslclientcert", fallback=None),
271
+ client_key=self.config.get(section, "sslclientkey", fallback=None),
272
+ ca_cert=self.config.get(section, "sslcacert", fallback=None),
273
+ )
274
+ elif section.endswith(":deb"):
275
+ return SyncDeb(
276
+ base_url=self.config.get(section, "baseurl"),
277
+ destination=self.config.get("main", "destination"),
278
+ reponame=section[:-4],
279
+ date=date,
280
+ proxy=self.config.get(section, "proxy", fallback=None),
281
+ client_cert=self.config.get(section, "sslclientcert", fallback=None),
282
+ client_key=self.config.get(section, "sslclientkey", fallback=None),
283
+ ca_cert=self.config.get(section, "sslcacert", fallback=None),
284
+ suites=self.config.get(section, "suites").split(),
285
+ components=self.config.get(section, "components").split(),
286
+ binary_archs=self.config.get(section, "binary_archs").split(),
287
+ )
288
+
289
+ def get_sections(self):
290
+ sections = set()
291
+ for section in self.config:
292
+ if section.endswith(":rpm") or section.endswith(":deb"):
293
+ if self.repo and section != self.repo:
294
+ continue
295
+ if self._tags:
296
+ if not self.validate_tags(section):
297
+ continue
298
+ sections.add(section)
299
+
300
+ return sections
301
+
302
+ def work(self):
303
+ self.log.info("starting up")
304
+ date = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
305
+ queue = collections.deque()
306
+ for section in self.get_sections():
307
+ queue.append(self.get_job(date=date, section=section))
308
+ workers = set()
309
+ if self.method == "sync":
310
+ num_worker = self.config.getint("main", "downloaders", fallback=1)
311
+ else:
312
+ num_worker = 1
313
+ for _ in range(num_worker):
314
+ workers.add(
315
+ RepoSyncThread(
316
+ queue=queue,
317
+ action=self.method,
318
+ snapname=self.snapname,
319
+ timestamp=self.timestamp,
320
+ )
321
+ )
322
+
323
+ for worker in workers:
324
+ worker.start()
325
+ return_code = 0
326
+ for worker in workers:
327
+ worker.join()
328
+ if worker.status != 0:
329
+ return_code = 1
330
+ sys.exit(return_code)
331
+
332
+ def validate_tags(self, section):
333
+ try:
334
+ section_tags = self.config.get(section, "tags").split(",")
335
+ except Exception as err:
336
+ return False
337
+ for tag in self.tags:
338
+ if tag.startswith("!"):
339
+ if tag[1:] in section_tags:
340
+ return False
341
+ else:
342
+ if tag not in section_tags:
343
+ return False
344
+ self.log.info(f"section {section} has matching tags")
345
+ return True
346
+
347
+
348
+ class RepoSyncThread(threading.Thread):
349
+ def __init__(self, queue, action, snapname, timestamp):
350
+ super().__init__()
351
+ self._action = action
352
+ self._snapname = snapname
353
+ self._queue = queue
354
+ self._status = 0
355
+ self._timestamp = timestamp
356
+ self.daemon = True
357
+ self.log = logging.getLogger("application")
358
+
359
+ @property
360
+ def action(self):
361
+ return self._action
362
+
363
+ @property
364
+ def snapname(self):
365
+ return self._snapname
366
+
367
+ @property
368
+ def queue(self):
369
+ return self._queue
370
+
371
+ @property
372
+ def status(self):
373
+ return self._status
374
+
375
+ @status.setter
376
+ def status(self, value):
377
+ self._status = value
378
+
379
+ @property
380
+ def timestamp(self):
381
+ return self._timestamp
382
+
383
+ def do_migrate(self, job):
384
+ try:
385
+ self.name = job.reponame
386
+ self.log.info(f"{self.action} start repo {job.reponame}")
387
+ job.migrate()
388
+ self.log.info(f"{self.action} done repo {job.reponame}")
389
+ except OSRepoSyncException:
390
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
391
+ self.status = 1
392
+
393
+ def do_sync(self, job):
394
+ try:
395
+ self.name = job.reponame
396
+ self.log.info(f"{self.action} start repo {job.reponame}")
397
+ job.sync()
398
+ self.log.info(f"{self.action} done repo {job.reponame}")
399
+ except OSRepoSyncException:
400
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
401
+ self.status = 1
402
+
403
+ def do_snap(self, job):
404
+ try:
405
+ self.name = job.reponame
406
+ self.log.info(f"{self.action} start repo {job.reponame}")
407
+ job.snap()
408
+ self.log.info(f"{self.action} done repo {job.reponame}")
409
+ except OSRepoSyncException:
410
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
411
+ self.status = 1
412
+
413
+ def do_snap_cleanup(self, job):
414
+ try:
415
+ self.name = job.reponame
416
+ self.log.info(f"{self.action} start repo {job.reponame}")
417
+ job.snap_cleanup()
418
+ self.log.info(f"{self.action} done repo {job.reponame}")
419
+ except OSRepoSyncException:
420
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
421
+ self.status = 1
422
+
423
+ def do_snap_list(self, job):
424
+ try:
425
+ self.name = job.reponame
426
+ referenced_timestamps = job.snap_list_get_referenced_timestamps()
427
+ self.log.info(f"Repository: {job.reponame}")
428
+ self.log.info("The following timestamp snapshots exist:")
429
+ for timestamp in job.snap_list_timestamp_snapshots():
430
+ self.log.info(
431
+ f"{timestamp} -> {referenced_timestamps.get(timestamp, [])}"
432
+ )
433
+ self.log.info("The following named snapshots exist:")
434
+ base = f"{job.destination}/snap/{job.reponame}/"
435
+ for named in job.snap_list_named_snapshots():
436
+ timestamp = job.snap_list_named_snapshot_target(f"{base}/named/{named}")
437
+ self.log.info(f"named/{named} -> {timestamp}")
438
+ latest = f"{base}/latest"
439
+ self.log.info(f"latest -> {job.snap_list_named_snapshot_target(latest)}")
440
+
441
+ except OSRepoSyncException:
442
+ self.status = 1
443
+
444
+ def do_snap_name(self, job):
445
+ try:
446
+ self.name = job.reponame
447
+ self.log.info(f"{self.action} start repo {job.reponame}")
448
+ job.snap_name(self.timestamp, self.snapname)
449
+ self.log.info(f"{self.action} done repo {job.reponame}")
450
+ except OSRepoSyncException:
451
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
452
+ self.status = 1
453
+
454
+ def do_snap_unname(self, job):
455
+ try:
456
+ self.name = job.reponame
457
+ self.log.info(f"{self.action} start repo {job.reponame}")
458
+ job.snap_unname(self.snapname)
459
+ self.log.info(f"{self.action} done repo {job.reponame}")
460
+ except OSRepoSyncException:
461
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
462
+ self.status = 1
463
+
464
+ def do_validate(self, job):
465
+ _downloader = Downloader()
466
+ packages = dict()
467
+ try:
468
+ self.log.info(f"{self.action} start repo {job.reponame}")
469
+ packages.update(job.revalidate())
470
+ except OSRepoSyncException:
471
+ self.log.fatal(f"could not {self.action} repo {job.reponame}")
472
+ self.status = 1
473
+ for destination, hash_info in packages.items():
474
+ try:
475
+ self.log.info(f"validating: {destination}")
476
+ _downloader.check_hash(
477
+ destination=destination,
478
+ checksum=hash_info["hash_sum"],
479
+ hash_type=hash_info["hash_algo"],
480
+ )
481
+ except OSRepoSyncHashError:
482
+ self.log.error(f"hash mismatch for: {destination}")
483
+ except FileNotFoundError:
484
+ self.log.error(f"file not found: {destination}")
485
+
486
+ def run(self):
487
+ while True:
488
+ try:
489
+ job = self.queue.pop()
490
+ if self.action == "migrate":
491
+ self.do_migrate(job)
492
+ elif self.action == "sync":
493
+ self.do_sync(job)
494
+ elif self.action == "snap_cleanup":
495
+ self.do_snap_cleanup(job)
496
+ elif self.action == "snap_list":
497
+ self.do_snap_list(job)
498
+ elif self.action == "snap_name":
499
+ self.do_snap_name(job)
500
+ elif self.action == "snap_unname":
501
+ self.do_snap_unname(job)
502
+ elif self.action == "snap":
503
+ self.do_snap(job)
504
+ elif self.action == "validate":
505
+ self.do_validate(job)
506
+ except IndexError:
507
+ break
508
+
509
+
510
+ if __name__ == "__main__":
511
+ main()