2 # from __future__ import print_function
4 __copyright__ = "(C) 2021 Guido Draheim"
5 __contact__ = "https://github.com/gdraheim/docker-mirror-packages-repo"
6 __license__ = "CC0 Creative Commons Zero (Public Domain)"
7 __version__ = "1.6.3007"
9 from collections import OrderedDict, namedtuple
19 if sys.version[0] != '2':
23 logg = logging.getLogger("mirror")
29 LEAP = "opensuse/leap"
31 OPENSUSE_VERSIONS = {"42.2": SUSE, "42.3": SUSE, "15.0": LEAP, "15.1": LEAP, "15.2": LEAP, "15.3": LEAP}
32 UBUNTU_LTS = {"16": "16.04", "18": "18.04", "20": "20.04"}
33 UBUNTU_VERSIONS = {"12.04": "precise", "14.04": "trusty", "16.04": "xenial", "17.10": "artful",
34 "18.04": "bionic", "18.10": "cosmic", "19.04": "disco", "19.10": "eoan",
35 "20.04": "focal", "20.10": "groovy"}
36 CENTOS_VERSIONS = {"7.0": "7.0.1406", "7.1": "7.1.1503", "7.2": "7.2.1511", "7.3": "7.3.1611",
37 "7.4": "7.4.1708", "7.5": "7.5.1804", "7.6": "7.6.1810", "7.7": "7.7.1908",
38 "7.8": "7.8.2003", "7.9": "7.9.2009",
39 "8.0": "8.0.1905", "8.1": "8.1.1911", "8.2": "8.2.2004", "8.3": "8.3.2011"}
42 if text is None: return None
45 if isinstance(text, bytes):
46 encoded = sys.getdefaultencoding()
47 if encoded in ["ascii"]:
50 return text.decode(encoded)
52 return text.decode("latin-1")
54 def output3(cmd, shell=True, debug=True):
55 if isinstance(cmd, basestring):
56 if debug: logg.debug("run: %s", cmd)
58 if debug: logg.debug("run: %s", " ".join(["'%s'" % item for item in cmd]))
59 run = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
60 out, err = run.communicate()
61 return decodes_(out), decodes_(err), run.returncode
67 def onlyversion(image):
69 return image.split(":")[-1]
73 def __init__(self, cname, image, hosts):
74 self.cname = cname # name of running container
75 self.image = image # image used to start the container
76 self.hosts = hosts # domain names for the container
78 class DockerMirrorPackagesRepo:
79 def __init__(self, image=None):
81 def host_system_image(self):
82 """ returns the docker image name which corresponds to the
83 operating system distribution of the host system. This
84 image name is the key for the other mirror functions. """
85 distro, version = self.detect_etc_image("/etc")
86 logg.info(":%s:%s host system image detected", distro, version)
87 if distro and version:
88 return "%s:%s" % (distro, version)
90 def detect_etc_image(self, etc):
91 distro, version = "", ""
92 os_release = os.path.join(etc, "os-release")
93 if os.path.exists(os_release):
94 # rhel:7.4 # VERSION="7.4 (Maipo)" ID="rhel" VERSION_ID="7.4"
95 # centos:7.3 # VERSION="7 (Core)" ID="centos" VERSION_ID="7"
96 # centos:7.4 # VERSION="7 (Core)" ID="centos" VERSION_ID="7"
97 # centos:7.7.1908 # VERSION="7 (Core)" ID="centos" VERSION_ID="7"
98 # opensuse:42.3 # VERSION="42.3" ID=opensuse VERSION_ID="42.3"
99 # opensuse/leap:15.0 # VERSION="15.0" ID="opensuse-leap" VERSION_ID="15.0"
100 # ubuntu:16.04 # VERSION="16.04.3 LTS (Xenial Xerus)" ID=ubuntu VERSION_ID="16.04"
101 # ubuntu:18.04 # VERSION="18.04.1 LTS (Bionic Beaver)" ID=ubuntu VERSION_ID="18.04"
102 for line in open(os_release):
104 m = re.match('^([_\\w]+)=([^"].*).*', line.strip())
106 key, value = m.group(1), m.group(2)
107 m = re.match('^([_\\w]+)="([^"]*)".*', line.strip())
109 key, value = m.group(1), m.group(2)
110 # logg.debug("%s => '%s' '%s'", line.strip(), key, value)
112 distro = value.replace("-", "/")
113 if key in ["VERSION_ID"]:
115 redhat_release = os.path.join(etc, "redhat-release")
116 if os.path.exists(redhat_release):
117 for line in open(redhat_release):
118 m = re.search("release (\\d+[.]\\d+).*", line)
122 centos_release = os.path.join(etc, "centos-release")
123 if os.path.exists(centos_release):
124 # CentOS Linux release 7.5.1804 (Core)
125 for line in open(centos_release):
126 m = re.search("release (\\d+[.]\\d+).*", line)
130 return distro, version
131 def detect_base_image(self, image):
132 """ returns the docker image name which corresponds to the
133 operating system distribution of the image provided. This
134 image name is the key for the other mirror functions. """
136 distro, version = "", ""
137 cname = "docker_mirror_detect." + os.path.basename(image).replace(":", ".")
138 cmd = "{docker} rm -f {cname}"
139 out, err, end = output3(cmd.format(**locals()))
140 cmd = "{docker} create --name={cname} {image}"
141 out, err, end = output3(cmd.format(**locals()))
143 logg.info("%s --name %s : %s", image, cname, err.strip())
144 tempdir = tempfile.mkdtemp("docker_mirror_detect")
146 distro, version = self.detect_base_image_from(cname, tempdir)
147 logg.info(":%s:%s base image detected", distro, version)
148 if distro and version:
149 return "%s:%s" % (distro, version)
151 shutil.rmtree(tempdir)
152 cmd = "{docker} rm {cname}"
153 out, err, end = output3(cmd.format(**locals()))
155 def detect_base_image_from(self, cname, tempdir):
158 cmd = "{docker} cp {cname}:/usr/lib/os-release {tempdir}/os-release"
159 out, err, end = output3(cmd.format(**locals()), debug=debug)
161 logg.debug("get: /usr/lib/os-release copied")
163 logg.debug("get: /usr/lib/os-release: %s", err.strip().replace(cname, "{cname}"))
164 cmd = "{docker} cp {cname}:/etc/os-release {tempdir}/os-release"
165 out, err, end = output3(cmd.format(**locals()), debug=debug)
167 logg.debug("get: /etc/os-release copied")
169 logg.debug("get: /etc/os-release: %s", err.strip().replace(cname, "{cname}"))
170 cmd = "{docker} cp {cname}:/etc/redhat-release {tempdir}/redhat-release"
171 out, err, end = output3(cmd.format(**locals()), debug=debug)
173 logg.debug("get: /etc/redhat-release copied")
175 logg.debug("get: /etc/redhat-release: %s", err.strip().replace(cname, "{cname}"))
176 cmd = "{docker} cp {cname}:/etc/centos-release {tempdir}/centos-release"
177 out, err, end = output3(cmd.format(**locals()), debug=debug)
179 logg.debug("get: /etc/centos-release copied")
181 logg.debug("get: /etc/centos-release: %s", err.strip().replace(cname, "{cname}"))
182 return self.detect_etc_image(tempdir)
183 def get_docker_latest_image(self, image):
184 """ converts a shorthand version into the version string used on an image name. """
185 if image.startswith("centos:"):
186 return self.get_centos_latest(image)
187 if image.startswith("opensuse/leap:"):
188 return self.get_opensuse_latest(image)
189 if image.startswith("opensuse:"):
190 return self.get_opensuse_latest(image)
191 if image.startswith("ubuntu:"):
192 return self.get_ubuntu_latest(image)
194 def get_docker_latest_version(self, image):
195 """ converts a shorthand version into the version string used on an image name. """
196 if image.startswith("centos:"):
197 version = image[len("centos:"):]
198 return self.get_centos_latest_version(version)
199 if image.startswith("opensuse/leap:"):
200 version = image[len("opensuse/leap:"):]
201 return self.get_opensuse_latest_version(version)
202 if image.startswith("opensuse:"):
203 version = image[len("opensuse:"):]
204 return self.get_opensuse_latest_version(version)
205 if image.startswith("ubuntu:"):
206 version = image[len("ubuntu:"):]
207 return self.get_ubuntu_latest_version(version)
209 def get_docker_mirror(self, image):
210 """ attach local centos-repo / opensuse-repo to docker-start enviroment.
211 Effectivly when it is required to 'docker start centos:x.y' then do
212 'docker start centos-repo:x.y' before and extend the original to
213 'docker start --add-host mirror...:centos-repo centos:x.y'. """
214 if image.startswith("centos:"):
215 return self.get_centos_docker_mirror(image)
216 if image.startswith("opensuse/leap:"):
217 return self.get_opensuse_docker_mirror(image)
218 if image.startswith("opensuse:"):
219 return self.get_opensuse_docker_mirror(image)
220 if image.startswith("ubuntu:"):
221 return self.get_ubuntu_docker_mirror(image)
223 def get_docker_mirrors(self, image):
224 """ attach local centos-repo / opensuse-repo to docker-start enviroment.
225 Effectivly when it is required to 'docker start centos:x.y' then do
226 'docker start centos-repo:x.y' before and extend the original to
227 'docker start --add-host mirror...:centos-repo centos:x.y'. """
229 if image.startswith("centos:"):
230 mirrors = self.get_centos_docker_mirrors(image)
232 if "centos" in image:
233 mirrors += self.get_epel_docker_mirrors(image)
234 if image.startswith("opensuse/leap:"):
235 mirrors = self.get_opensuse_docker_mirrors(image)
236 if image.startswith("opensuse:"):
237 mirrors = self.get_opensuse_docker_mirrors(image)
238 if image.startswith("ubuntu:"):
239 mirrors = self.get_ubuntu_docker_mirrors(image)
240 logg.info(" %s -> %s", image, " ".join([mirror.cname for mirror in mirrors]))
242 def get_ubuntu_latest(self, image, default=None):
243 if image.startswith("ubuntu:"):
245 version = image[len("ubuntu:"):]
246 latest = self.get_ubuntu_latest_version(version)
248 return "{distro}:{latest}".format(**locals())
249 if default is not None:
252 def get_ubuntu_latest_version(self, version):
253 """ allows to use 'ubuntu:18' or 'ubuntu:bionic' """
255 if ver in ["latest"]:
259 for release in UBUNTU_VERSIONS:
260 codename = UBUNTU_VERSIONS[release]
261 if len(ver) >= 3 and codename.startswith(ver):
262 logg.debug("release (%s) %s", release, codename)
265 elif release.startswith(ver):
266 logg.debug("release %s (%s)", release, codename)
271 return ver or version
272 def get_ubuntu_docker_mirror(self, image):
273 """ detects a local ubuntu mirror or starts a local
274 docker container with a ubunut repo mirror. It
275 will return the extra_hosts setting to start
276 other docker containers"""
277 rmi = "localhost:5000/mirror-packages"
279 if UNIVERSE: rep = "ubuntu-repo/universe"
280 ver = self.get_ubuntu_latest_version(onlyversion(image))
281 return self.docker_mirror(rmi, rep, ver, "archive.ubuntu.com", "security.ubuntu.com")
282 def get_ubuntu_docker_mirrors(self, image):
283 main = self.get_ubuntu_docker_mirror(image)
285 def get_centos_latest(self, image, default=None):
286 if image.startswith("centos:"):
288 version = image[len("centos:"):]
289 latest = self.get_centos_latest_version(version)
291 return "{distro}:{latest}".format(**locals())
292 if default is not None:
295 def get_centos_latest_version(self, version):
296 """ allows to use 'centos:7' or 'centos:7.9' making 'centos:7.9.2009' """
298 if ver in ["latest"]:
302 for release in CENTOS_VERSIONS:
303 if release.startswith(ver):
304 fullrelease = CENTOS_VERSIONS[release]
305 logg.debug("release %s (%s)", release, fullrelease)
306 if latest < fullrelease:
310 if ver in CENTOS_VERSIONS:
311 ver = CENTOS_VERSIONS[ver]
312 return ver or version
313 def get_centos_docker_mirror(self, image):
314 """ detects a local centos mirror or starts a local
315 docker container with a centos repo mirror. It
316 will return the setting for extrahosts"""
317 rmi = "localhost:5000/mirror-packages"
319 ver = self.get_centos_latest_version(onlyversion(image))
320 return self.docker_mirror(rmi, rep, ver, "mirrorlist.centos.org")
321 def get_centos_docker_mirrors(self, image):
322 main = self.get_centos_docker_mirror(image)
324 def get_opensuse_latest(self, image, default=None):
325 if image.startswith("opensuse/leap:"):
326 distro = "opensuse/leap"
327 version = image[len("opensuse/leap:"):]
328 latest = self.get_opensuse_latest_version(version)
330 if latest in OPENSUSE_VERSIONS:
331 distro = OPENSUSE_VERSIONS[latest]
332 return "{distro}:{latest}".format(**locals())
333 if image.startswith("opensuse:"):
335 version = image[len("opensuse:"):]
336 latest = self.get_opensuse_latest_version(version)
338 if latest in OPENSUSE_VERSIONS:
339 distro = OPENSUSE_VERSIONS[latest]
340 return "{distro}:{latest}".format(**locals())
341 if default is not None:
344 def get_opensuse_latest_version(self, version):
345 """ allows to use 'opensuse:42' making 'opensuse:42.3' """
347 if ver in ["latest"]:
351 for release in OPENSUSE_VERSIONS:
352 if release.startswith(ver):
353 logg.debug("release %s", release)
354 # opensuse:42.0 was before opensuse/leap:15.0
355 release42 = release.replace("42.", "14.")
356 latest42 = latest.replace("42.", "14.")
357 if latest42 < release42:
360 return ver or version
361 def get_opensuse_docker_mirror(self, image):
362 """ detects a local opensuse mirror or starts a local
363 docker container with a centos repo mirror. It
364 will return the extra_hosts setting to start
365 other docker containers"""
366 rmi = "localhost:5000/mirror-packages"
367 rep = "opensuse-repo"
368 ver = self.get_opensuse_latest_version(onlyversion(image))
369 return self.docker_mirror(rmi, rep, ver, "download.opensuse.org")
370 def get_opensuse_docker_mirrors(self, image):
371 main = self.get_opensuse_docker_mirror(image)
373 def docker_mirror(self, rmi, rep, ver, *hosts):
374 req = rep.replace("/", "-")
375 image = "{rmi}/{rep}:{ver}".format(**locals())
376 cname = "{req}-{ver}".format(**locals())
377 return DockerMirror(cname, image, list(hosts))
379 def get_extra_mirrors(self, image):
381 if image.startswith("centos:"):
382 version = image[len("centos:"):]
383 mirrors = self.get_epel_docker_mirrors(version)
385 def get_epel_docker_mirrors(self, image):
386 main = self.get_epel_docker_mirror(image)
388 def get_epel_docker_mirror(self, image):
389 """ detects a local epel mirror or starts a local
390 docker container with a epel repo mirror. It
391 will return the setting for extrahosts"""
393 rmi = "localhost:5000/mirror-packages"
395 ver = onlyversion(image)
396 version = self.get_centos_latest_version(ver)
397 # cut the yymm date part from the centos release
398 released = version.split(".")[-1]
401 # and then check for actual images around
402 cmd = docker + " images --format '{{.Repository}}:{{.Tag}}'"
403 out, err, end = output3(cmd)
405 logg.error("docker images [%s]\n\t", end, cmd)
406 for line in out.split("\n"):
407 if "/epel-repo:" not in line:
409 tagline = re.sub(".*/epel-repo:", "", line)
410 tagname = re.sub(" .*", "", tagline)
411 created = tagname.split(".")[-1]
412 accepts = tagname.startswith(major(version))
413 logg.debug(": %s (%s) (%s) %s:%s", line.strip(), created, released, major(version), accepts and "x" or "ignore")
414 if created >= released and accepts:
415 if not later or later > tagname:
417 elif created < released and accepts:
418 if not before or before < tagname:
424 return self.docker_mirror(rmi, rep, ver, "mirrors.fedoraproject.org")
426 def ip_container(self, name):
428 cmd = "{docker} inspect {name}"
429 out, err, rc = output3(cmd.format(**locals()))
431 logg.info("%s : %s", cmd, err)
432 logg.debug("no address for %s", name)
434 values = json.loads(out)
435 if not values or "NetworkSettings" not in values[0]:
436 logg.critical(" docker inspect %s => %s ", name, values)
437 addr = values[0]["NetworkSettings"]["IPAddress"]
438 assert isinstance(addr, basestring)
439 logg.debug("address %s for %s", addr, name)
441 def start_containers(self, image):
442 mirrors = self.get_docker_mirrors(image)
444 for mirror in mirrors:
445 addr = self.start_container(mirror.image, mirror.cname)
446 done[mirror.cname] = addr
448 def start_container(self, image, container):
450 cmd = "{docker} inspect {image}"
451 out, err, ok = output3(cmd.format(**locals()))
452 image_found = json.loads(out)
454 logg.info("image not found: %s", image)
456 cmd = "{docker} inspect {container}"
457 out, err, rc = output3(cmd.format(**locals()))
458 container_found = json.loads(out)
459 if not rc and container_found:
460 container_status = container_found[0]["State"]["Status"]
461 logg.info("::: %s -> %s", container, container_status)
462 latest_image_id = image_found[0]["Id"]
463 container_image_id = container_found[0]["Image"]
464 if latest_image_id != container_image_id or container_status not in ["running"]:
465 cmd = "{docker} rm --force {container}"
466 out, err, rc = output3(cmd.format(**locals()))
468 logg.debug("%s : %s", cmd, err)
470 if not container_found:
471 cmd = "{docker} run --rm=true --detach --name {container} {image}"
472 out, err, rc = output3(cmd.format(**locals()))
474 logg.error("%s : %s", cmd, err)
475 addr = self.ip_container(container)
476 logg.info("%s : %s", container, addr)
478 def stop_containers(self, image):
479 mirrors = self.get_docker_mirrors(image)
481 for mirror in mirrors:
482 info = self.stop_container(mirror.image, mirror.cname)
483 done[mirror.cname] = info
485 def stop_container(self, image, container):
487 cmd = "{docker} inspect {container}"
488 out, err, rc = output3(cmd.format(**locals()))
489 container_found = json.loads(out)
490 if not rc and container_found:
491 cmd = "{docker} rm --force {container}"
492 out, err, ok = output3(cmd.format(**locals()))
493 status = container_found[0].get("State", {})
494 started = status.get("StartedAt", "(was not started)")
495 assert isinstance(started, basestring)
497 return "(did not exist)"
498 def info_containers(self, image):
499 mirrors = self.get_docker_mirrors(image)
501 for mirror in mirrors:
502 info = self.info_container(mirror.image, mirror.cname)
503 done[mirror.cname] = info
505 def info_container(self, image, container):
506 addr = self.ip_container(container)
508 def get_containers(self, image):
509 mirrors = self.get_docker_mirrors(image)
511 for mirror in mirrors:
512 done.append(mirror.cname)
514 def inspect_containers(self, image):
515 mirrors = self.get_docker_mirrors(image)
518 for mirror in mirrors:
519 addr = self.ip_container(mirror.cname)
520 done[mirror.cname] = addr
523 def add_hosts(self, image, done={}):
524 mirrors = self.get_docker_mirrors(image)
526 for mirror in mirrors:
528 logg.info("name = %s (%s)", name, done)
532 for host in mirror.hosts:
533 args += ["--add-host", "%s:%s" % (host, addr)]
536 return """helper to start/stop mirror-container with the packages-repo
537 help this help screen
538 image|detect the image name matching the local system
539 facts [image] the json data used to start or stop the containers
540 start [image] starts the container(s) with the mirror-packages-repo
541 stop [image] stops the containers(s) with the mirror-packages-repo
542 addhosts [image] shows the --add-hosts string for the client container
544 def detect(self, image=None):
545 if not image and self._image:
547 if not image or image in ["host", "system"]:
548 return self.host_system_image()
549 latest = self.get_docker_latest_image(image)
553 # actually create a container and look into it
554 return self.detect_base_image(image)
555 def epel(self, image=None):
556 image = self.detect(image)
557 mirrors = self.get_extra_mirrors(image)
558 for mirror in mirrors:
561 def repo(self, image=None):
562 image = self.detect(image)
563 mirrors = self.get_docker_mirrors(image)
564 for mirror in mirrors:
567 host = mirror.hosts[0]
568 return "--add-host={host}:({refer})".format(**locals())
572 def repos(self, image=None):
573 image = self.detect(image)
574 mirrors = self.get_docker_mirrors(image)
576 for mirror in mirrors:
578 if shown: shown += " "
579 for host in mirror.hosts:
581 shown += "--add-host={host}:({refer})".format(**locals())
583 shown += mirror.image + "\n"
585 def facts(self, image=None):
586 image = self.detect(image)
587 mirrors = self.get_docker_mirrors(image)
589 for mirror in mirrors:
590 data[mirror.cname] = {"image": mirror.image, "name": mirror.cname,
591 "hosts": mirror.hosts}
592 return json.dumps(data, indent=2)
593 def starts(self, image=None):
594 image = self.detect(image)
595 done = self.start_containers(image)
597 return " ".join(self.add_hosts(image, done))
599 return json.dumps(done, indent=2)
600 def stops(self, image=None):
601 image = self.detect(image)
602 done = self.stop_containers(image)
604 names = sorted(done.keys())
605 return " ".join(names)
607 return json.dumps(done, indent=2)
608 def infos(self, image=None):
609 image = self.detect(image)
610 done = self.info_containers(image)
612 return " ".join(self.add_hosts(image, done))
614 return json.dumps(done, indent=2)
615 def containers(self, image=None):
616 image = self.detect(image)
617 done = self.get_containers(image)
619 return " ".join(done)
621 return json.dumps(done, indent=2)
622 def inspects(self, image=None):
623 image = self.detect(image)
624 done = self.inspect_containers(image)
626 return " ".join(self.add_hosts(image, done))
628 return json.dumps(done, indent=2)
630 if __name__ == "__main__":
631 from argparse import ArgumentParser
632 _o = ArgumentParser(description="""starts local containers representing mirrors of package repo repositories
633 which are required by a container type. Subsequent 'docker run' can use the '--add-hosts' from this
634 helper script to divert 'pkg install' calls to a local docker container as the real source.""")
635 _o.add_argument("-v", "--verbose", action="count", default=0, help="more logging")
636 _o.add_argument("-a", "--add-hosts", "--add-host", action="store_true", default=ADDHOSTS,
637 help="show addhost options for 'docker run' [%(default)s]")
638 _o.add_argument("--epel", action="store_true", default=ADDEPEL,
639 help="addhosts for epel as well [%(default)s]")
640 _o.add_argument("--universe", action="store_true", default=UNIVERSE,
641 help="addhosts using universe variant [%(default)s]")
642 commands = ["help", "detect", "image", "repo", "info", "facts", "start", "stop"]
643 _o.add_argument("command", nargs="?", default="detect", help="|".join(commands))
644 _o.add_argument("image", nargs="?", default=None, help="defaults to image name of the local host system")
645 opt = _o.parse_args()
646 logging.basicConfig(level=max(0, logging.WARNING - opt.verbose * 10))
647 ADDHOSTS = opt.add_hosts
648 ADDEPEL = opt.epel # centos epel-repo
649 UNIVERSE = opt.universe # ubuntu universe repo
651 repo = DockerMirrorPackagesRepo()
652 if opt.command in ["?", "help"]:
654 elif opt.command in ["detect", "image"]:
655 print(repo.detect(opt.image))
656 elif opt.command in ["repo", "from"]:
657 print(repo.repo(opt.image))
658 elif opt.command in ["repos", "for"]:
659 print(repo.repos(opt.image))
660 elif opt.command in ["latest"]:
661 print(repo.get_docker_latest_version(opt.image))
662 elif opt.command in ["epel"]:
663 print(repo.epel(opt.image))
664 elif opt.command in ["facts"]:
665 print(repo.facts(opt.image))
666 elif opt.command in ["start", "starts"]:
667 print(repo.starts(opt.image))
668 elif opt.command in ["stop", "stops"]:
669 print(repo.stops(opt.image))
670 elif opt.command in ["show", "shows", "info", "infos"]:
671 print(repo.infos(opt.image))
672 elif opt.command in ["addhost", "add-host", "addhosts", "add-hosts"]:
674 print(repo.infos(opt.image))
675 elif opt.command in ["inspect"]:
676 print(repo.inspects(opt.image))
677 elif opt.command in ["containers"]:
678 print(repo.containers(opt.image))
680 print("unknown command", opt.command)