docker_image_availability.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. """Check that required Docker images are available."""
  2. from openshift_checks import OpenShiftCheck
  3. from openshift_checks.mixins import DockerHostMixin
  4. NODE_IMAGE_SUFFIXES = ["haproxy-router", "docker-registry", "deployer", "pod"]
  5. DEPLOYMENT_IMAGE_INFO = {
  6. "origin": {
  7. "namespace": "openshift",
  8. "name": "origin",
  9. "registry_console_image": "cockpit/kubernetes",
  10. },
  11. "openshift-enterprise": {
  12. "namespace": "openshift3",
  13. "name": "ose",
  14. "registry_console_image": "registry.access.redhat.com/openshift3/registry-console",
  15. },
  16. }
  17. class DockerImageAvailability(DockerHostMixin, OpenShiftCheck):
  18. """Check that required Docker images are available.
  19. Determine docker images that an install would require and check that they
  20. are either present in the host's docker index, or available for the host to pull
  21. with known registries as defined in our inventory file (or defaults).
  22. """
  23. name = "docker_image_availability"
  24. tags = ["preflight"]
  25. # we use python-docker-py to check local docker for images, and skopeo
  26. # to look for images available remotely without waiting to pull them.
  27. dependencies = ["python-docker-py", "skopeo"]
  28. def is_active(self):
  29. """Skip hosts with unsupported deployment types."""
  30. deployment_type = self.get_var("openshift_deployment_type")
  31. has_valid_deployment_type = deployment_type in DEPLOYMENT_IMAGE_INFO
  32. return super(DockerImageAvailability, self).is_active() and has_valid_deployment_type
  33. def run(self):
  34. msg, failed, changed = self.ensure_dependencies()
  35. if failed:
  36. return {
  37. "failed": True,
  38. "changed": changed,
  39. "msg": "Some dependencies are required in order to check Docker image availability.\n" + msg
  40. }
  41. required_images = self.required_images()
  42. missing_images = set(required_images) - set(self.local_images(required_images))
  43. # exit early if all images were found locally
  44. if not missing_images:
  45. return {"changed": changed}
  46. registries = self.known_docker_registries()
  47. if not registries:
  48. return {"failed": True, "msg": "Unable to retrieve any docker registries.", "changed": changed}
  49. available_images = self.available_images(missing_images, registries)
  50. unavailable_images = set(missing_images) - set(available_images)
  51. if unavailable_images:
  52. return {
  53. "failed": True,
  54. "msg": (
  55. "One or more required Docker images are not available:\n {}\n"
  56. "Configured registries: {}"
  57. ).format(",\n ".join(sorted(unavailable_images)), ", ".join(registries)),
  58. "changed": changed,
  59. }
  60. return {"changed": changed}
  61. def required_images(self):
  62. """
  63. Determine which images we expect to need for this host.
  64. Returns: a set of required images like 'openshift/origin:v3.6'
  65. The thorny issue of determining the image names from the variables is under consideration
  66. via https://github.com/openshift/openshift-ansible/issues/4415
  67. For now we operate as follows:
  68. * For containerized components (master, node, ...) we look at the deployment type and
  69. use openshift/origin or openshift3/ose as the base for those component images. The
  70. version is openshift_image_tag as determined by the openshift_version role.
  71. * For OpenShift-managed infrastructure (router, registry...) we use oreg_url if
  72. it is defined; otherwise we again use the base that depends on the deployment type.
  73. Registry is not included in constructed images. It may be in oreg_url or etcd image.
  74. """
  75. required = set()
  76. deployment_type = self.get_var("openshift_deployment_type")
  77. host_groups = self.get_var("group_names")
  78. # containerized etcd may not have openshift_image_tag, see bz 1466622
  79. image_tag = self.get_var("openshift_image_tag", default="latest")
  80. image_info = DEPLOYMENT_IMAGE_INFO[deployment_type]
  81. if not image_info:
  82. return required
  83. # template for images that run on top of OpenShift
  84. image_url = "{}/{}-{}:{}".format(image_info["namespace"], image_info["name"], "${component}", "${version}")
  85. image_url = self.get_var("oreg_url", default="") or image_url
  86. if 'nodes' in host_groups:
  87. for suffix in NODE_IMAGE_SUFFIXES:
  88. required.add(image_url.replace("${component}", suffix).replace("${version}", image_tag))
  89. # The registry-console is for some reason not prefixed with ose- like the other components.
  90. # Nor is it versioned the same, so just look for latest.
  91. # Also a completely different name is used for Origin.
  92. required.add(image_info["registry_console_image"])
  93. # images for containerized components
  94. if self.get_var("openshift", "common", "is_containerized"):
  95. components = set()
  96. if 'nodes' in host_groups:
  97. components.update(["node", "openvswitch"])
  98. if 'masters' in host_groups: # name is "origin" or "ose"
  99. components.add(image_info["name"])
  100. for component in components:
  101. required.add("{}/{}:{}".format(image_info["namespace"], component, image_tag))
  102. if 'etcd' in host_groups: # special case, note it is the same for origin/enterprise
  103. required.add("registry.access.redhat.com/rhel7/etcd") # and no image tag
  104. return required
  105. def local_images(self, images):
  106. """Filter a list of images and return those available locally."""
  107. return [
  108. image for image in images
  109. if self.is_image_local(image)
  110. ]
  111. def is_image_local(self, image):
  112. """Check if image is already in local docker index."""
  113. result = self.execute_module("docker_image_facts", {"name": image})
  114. if result.get("failed", False):
  115. return False
  116. return bool(result.get("images", []))
  117. def known_docker_registries(self):
  118. """Build a list of docker registries available according to inventory vars."""
  119. docker_facts = self.get_var("openshift", "docker")
  120. regs = set(docker_facts["additional_registries"])
  121. deployment_type = self.get_var("openshift_deployment_type")
  122. if deployment_type == "origin":
  123. regs.update(["docker.io"])
  124. elif "enterprise" in deployment_type:
  125. regs.update(["registry.access.redhat.com"])
  126. return list(regs)
  127. def available_images(self, images, default_registries):
  128. """Search remotely for images. Returns: list of images found."""
  129. return [
  130. image for image in images
  131. if self.is_available_skopeo_image(image, default_registries)
  132. ]
  133. def is_available_skopeo_image(self, image, default_registries):
  134. """Use Skopeo to determine if required image exists in known registry(s)."""
  135. registries = default_registries
  136. # if image already includes a registry, only use that
  137. if image.count("/") > 1:
  138. registry, image = image.split("/", 1)
  139. registries = [registry]
  140. for registry in registries:
  141. args = {"_raw_params": "skopeo inspect --tls-verify=false docker://{}/{}".format(registry, image)}
  142. result = self.execute_module("command", args)
  143. if result.get("rc", 0) == 0 and not result.get("failed"):
  144. return True
  145. return False