1
0
mirror of https://github.com/projectatomic/atomic.git synced 2026-02-06 03:45:28 +01:00
Files
atomic/Atomic/syscontainers.py
Giuseppe Scrivano f596f1205a syscontainers: rename some functions
they are part of the class SystemContainers, do not repeat
system_container in the name.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>

Closes: #683
Approved by: rhatdan
2016-10-06 11:07:34 +00:00

1189 lines
47 KiB
Python

import os
import sys
import json
from . import util
import tempfile
import tarfile
from string import Template
import calendar
import shutil
import stat
import subprocess
import time
from .client import AtomicDocker
from ctypes import cdll, CDLL
try:
import gi
try:
gi.require_version('OSTree', '1.0')
from gi.repository import Gio, GLib, OSTree # pylint: disable=no-name-in-module
OSTREE_PRESENT = True
except ValueError:
OSTREE_PRESENT = False
except ImportError:
OSTREE_PRESENT = False
try:
from subprocess import DEVNULL # pylint: disable=no-name-in-module
except ImportError:
DEVNULL = open(os.devnull, 'wb')
HOME = os.path.expanduser("~")
ATOMIC_LIBEXEC = os.environ.get('ATOMIC_LIBEXEC', '/usr/libexec/atomic')
ATOMIC_VAR = '/var/lib/containers/atomic'
ATOMIC_VAR_USER = "%s/.containers/atomic" % HOME
OSTREE_OCIIMAGE_PREFIX = "ociimage/"
SYSTEMD_UNIT_FILES_DEST = "/etc/systemd/system"
SYSTEMD_UNIT_FILES_DEST_USER = "%s/.config/systemd/user" % HOME
SYSTEMD_TMPFILES_DEST = "/etc/tmpfiles.d"
SYSTEMD_TMPFILES_DEST_USER = "%s/.containers/tmpfiles" % HOME
SYSTEMD_UNIT_FILE_DEFAULT_TEMPLATE = """
[Unit]
Description=$NAME
[Service]
ExecStart=$EXEC_START
ExecStop=$EXEC_STOP
Restart=on-crash
WorkingDirectory=$DESTDIR
[Install]
WantedBy=multi-user.target
"""
class SystemContainers(object):
def __init__(self):
self.atomic_config = util.get_atomic_config()
self.backend = None
self.user = util.is_user_mode()
self.args = None
self.setvalues = None
self.display = False
def get_atomic_config_item(self, config_item):
return util.get_atomic_config_item(config_item, atomic_config=self.atomic_config)
def _do_syncfs(self, rootfs, rootfs_fd):
# Fallback to sync --file-system if loading it from libc fails.
try:
cdll.LoadLibrary("libc.so.6")
libc = CDLL("libc.so.6")
if libc.syncfs(rootfs_fd) == 0:
return
except (NameError, AttributeError, OSError):
pass
util.check_call(["sync", "--file-system", rootfs], stdin=DEVNULL,
stdout=DEVNULL,
stderr=DEVNULL)
def _checkout_layer(self, repo, rootfs_fd, rootfs, rev):
# ostree 2016.8 has a glib introspection safe API for checkout, use it
# when available.
if hasattr(repo, "checkout_at"):
options = OSTree.RepoCheckoutAtOptions() # pylint: disable=no-member
options.overwrite_mode = OSTree.RepoCheckoutOverwriteMode.UNION_FILES
options.process_whiteouts = True
options.disable_fsync = True
if self.user:
options.mode = OSTree.RepoCheckoutMode.USER
repo.checkout_at(options, rootfs_fd, rootfs, rev)
else:
if self.user:
user = ["--user-mode"]
else:
user = []
util.check_call(["ostree", "--repo=%s" % self.get_ostree_repo_location(),
"checkout",
"--union"] +
user +
["--whiteouts",
"--fsync=no",
rev,
rootfs],
stdin=DEVNULL,
stdout=DEVNULL,
stderr=DEVNULL)
def set_args(self, args):
self.args = args
try:
self.backend = args.backend
except (NameError, AttributeError):
self.backend = None
if not self.backend:
self.backend = self.get_atomic_config_item(["default_storage"]) or "ostree"
try:
self.display = self.args.display
except (NameError, AttributeError):
pass
try:
self.setvalues = args.setvalues
except (NameError, AttributeError):
pass
def _pull_image_to_ostree(self, repo, image, upgrade):
if not repo:
raise ValueError("Cannot find a configured OSTree repo")
if image.startswith("ostree:"):
self._check_system_ostree_image(repo, image, upgrade)
elif self.args.image.startswith("docker:"):
self._pull_docker_image(repo, image.replace("docker:", ""))
elif self.args.image.startswith("dockertar:"):
self._pull_docker_tar(repo, image.replace("dockertar:", ""))
else: # Assume "oci:"
self._check_system_oci_image(repo, image, upgrade)
def pull_image(self):
self._pull_image_to_ostree(self._get_ostree_repo(), self.args.image, True)
def install_user_container(self, image, name):
try:
util.check_call([util.BWRAP_OCI_PATH, "--version"], stdout=DEVNULL)
except util.FileNotFound:
raise ValueError("Cannot install the container: bwrap-oci is needed to run user containers")
if not "--user" in str(util.check_output(["systemctl", "--help"], stdin=DEVNULL, stderr=DEVNULL)):
raise ValueError("Cannot install the container: systemctl does not support --user")
# Same entrypoint
return self.install(image, name)
def install(self, image, name):
repo = self._get_ostree_repo()
if not repo:
raise ValueError("Cannot find a configured OSTree repo")
if self.args.system and self.user:
raise ValueError("Only root can use --system")
if not self.user:
try:
util.check_call([util.RUNC_PATH, "--version"], stdout=DEVNULL)
except util.FileNotFound:
raise ValueError("Cannot install the container: runc is needed to run system containers")
self._pull_image_to_ostree(repo, image, False)
if self.get_checkout(name):
util.write_out("%s already present" % (name))
return
return self._checkout(repo, name, image, 0, False, remote=self.args.remote)
def _check_oci_configuration_file(self, conf_path, remote=None):
with open(conf_path, 'r') as conf:
configuration = json.loads(conf.read())
if not 'root' in configuration or \
not 'readonly' in configuration['root'] or \
not configuration['root']['readonly']:
raise ValueError("Invalid configuration file. Only readonly images are supported")
if configuration['root']['path'] != 'rootfs' and not remote:
raise ValueError("Invalid configuration file. Path must be 'rootfs'")
missing_source_paths = []
# Ensure that the source path specified in bind/rbind exists
if "mounts" in configuration:
for mount in configuration["mounts"]:
if not "type" in mount:
continue
if "source" in mount and "bind" in mount["type"]:
source = mount["source"]
if not os.path.exists(source):
missing_source_paths.append(source)
return missing_source_paths
def _generate_default_oci_configuration(self, destination):
args = [util.RUNC_PATH, 'spec']
util.subp(args, cwd=destination)
conf_path = os.path.join(destination, "config.json")
with open(conf_path, 'r') as conf:
configuration = json.loads(conf.read())
configuration['root']['readonly'] = True
configuration['root']['path'] = 'rootfs'
configuration['process']['terminal'] = False
configuration['process']['args'] = ['run.sh']
with open(conf_path, 'w') as conf:
conf.write(json.dumps(configuration, indent=4))
def _generate_systemd_startstop_directives(self, name):
if self.user:
return ["%s '%s'" % (util.BWRAP_OCI_PATH, name), ""]
version = str(util.check_output([util.RUNC_PATH, "--version"], stderr=DEVNULL))
if "version 0" in version:
runc_commands = ["start", "kill"]
else:
runc_commands = ["run", "kill"]
return ["%s %s '%s'" % (util.RUNC_PATH, command, name) for command in runc_commands]
def _get_systemd_destination_files(self, name):
if self.user:
unitfileout = os.path.join(SYSTEMD_UNIT_FILES_DEST_USER, "%s.service" % name)
tmpfilesout = os.path.join(SYSTEMD_TMPFILES_DEST_USER, "%s.conf" % name)
else:
unitfileout = os.path.join(SYSTEMD_UNIT_FILES_DEST, "%s.service" % name)
tmpfilesout = os.path.join(SYSTEMD_TMPFILES_DEST, "%s.conf" % name)
return unitfileout, tmpfilesout
def _resolve_remote_path(self, remote_path):
if not remote_path:
return None
real_path = os.path.realpath(remote_path)
if not os.path.exists(real_path):
raise ValueError("The container's rootfs is set to remote, but the remote rootfs does not exist")
return real_path
def _checkout(self, repo, name, img, deployment, upgrade, values=None, destination=None, extract_only=False, remote=None):
destination = destination or "%s/%s.%d" % (self._get_system_checkout_path(), name, deployment)
unitfileout, tmpfilesout = self._get_systemd_destination_files(name)
if not upgrade:
for f in [unitfileout, tmpfilesout]:
if os.path.exists(f):
raise ValueError("The file %s already exists." % f)
try:
return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote)
except (ValueError, OSError) as e:
try:
if not extract_only:
shutil.rmtree(destination)
except OSError:
pass
try:
if not extract_only and not upgrade:
shutil.rmtree(unitfileout)
except OSError:
pass
try:
if not extract_only and not upgrade:
shutil.rmtree(tmpfilesout)
except OSError:
pass
raise e
# Accept both name and version Id, and return the ostree rev
def _resolve_image(self, repo, img):
imagebranch = SystemContainers._get_ostree_image_branch(img)
rev = repo.resolve_rev(imagebranch, True)[1]
if rev:
return imagebranch, rev
# if we could not find an image with the specified name, check if it is the prefix
# of an ID, and allow it only for tagged images.
if not str.isalnum(str(img)):
return None, None
tagged_images = [i for i in self.get_system_images(get_all=True, repo=repo) if i['RepoTags']]
matches = [i for i in tagged_images if i['Id'].startswith(img)]
if len(matches) == 1:
# only one image, use it
i = matches[0]
imagebranch = "%s%s" % (OSTREE_OCIIMAGE_PREFIX, SystemContainers._encode_to_ostree_ref(i['RepoTags'][0]))
return imagebranch, i['OSTree-rev']
elif len(matches) > 1:
# more than one match, error out
raise ValueError("more images matching prefix `%s`" % img)
return None, None
def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote):
if not values:
values = {}
remote_path = self._resolve_remote_path(remote)
_, rev = self._resolve_image(repo, img)
if rev is None:
raise ValueError("Image %s not found" % img)
if remote_path:
remote_rootfs = os.path.join(remote_path, "rootfs")
if os.path.exists(remote_rootfs):
util.write_out("The remote rootfs for this container is set to be %s" % remote_rootfs)
elif os.path.exists(os.path.join(remote, "usr")): # Assume that the user directly gave the location of the rootfs
remote_rootfs = remote
remote_path = os.path.dirname(remote_path) # Use the parent directory as the "container location"
else:
raise ValueError("--remote was specified but the given location does not contain a rootfs")
exports = os.path.join(remote_path, "rootfs/exports")
else:
exports = os.path.join(destination, "rootfs/exports")
unitfile = os.path.join(exports, "service.template")
tmpfiles = os.path.join(exports, "tmpfiles.template")
util.write_out("Extracting to %s" % destination)
# upgrade will not restart the service if it was not already running
was_service_active = self._is_service_active(name)
if self.display:
return
if self.user:
rootfs = os.path.join(destination, "rootfs")
elif extract_only:
rootfs = destination
elif remote_path:
rootfs = os.path.join(remote_path, "rootfs")
else:
# Under Atomic, get the real deployment location. It is needed to create the hard links.
try:
sysroot = OSTree.Sysroot()
sysroot.load()
osname = sysroot.get_booted_deployment().get_osname()
destination = os.path.join("/ostree/deploy/", osname, os.path.relpath(destination, "/"))
destination = os.path.realpath(destination)
except: #pylint: disable=bare-except
pass
rootfs = os.path.join(destination, "rootfs")
if os.path.exists(destination):
shutil.rmtree(destination)
if remote_path:
os.makedirs(destination)
else:
os.makedirs(rootfs)
manifest = self._image_manifest(repo, rev)
if not remote_path:
rootfs_fd = None
try:
rootfs_fd = os.open(rootfs, os.O_DIRECTORY)
if manifest is None:
self._checkout_layer(repo, rootfs_fd, rootfs, rev)
else:
layers = SystemContainers.get_layers_from_manifest(json.loads(manifest))
for layer in layers:
rev_layer = repo.resolve_rev("%s%s" % (OSTREE_OCIIMAGE_PREFIX, layer.replace("sha256:", "")), True)[1]
if not rev_layer:
raise ValueError("Layer not found: %s. Please pull again the image" % layer.replace("sha256:", ""))
self._checkout_layer(repo, rootfs_fd, rootfs, rev_layer)
self._do_syncfs(rootfs, rootfs_fd)
finally:
if rootfs_fd:
os.close(rootfs_fd)
if extract_only:
return
if self.user:
values["RUN_DIRECTORY"] = os.environ.get("XDG_RUNTIME_DIR", "/run/user/%s" % (os.getuid()))
values["STATE_DIRECTORY"] = "%s/.data" % HOME
else:
values["RUN_DIRECTORY"] = "/run"
values["STATE_DIRECTORY"] = "/var/lib"
# When installing a new system container, set values in this order:
#
# 1) What comes from manifest.json, if present, as default value.
# 2) What the user sets explictly as --set
# 3) Values for DESTDIR and NAME
manifest_file = os.path.join(exports, "manifest.json")
if os.path.exists(manifest_file):
with open(manifest_file, "r") as f:
manifest = json.loads(f.read())
if "defaultValues" in manifest:
for key, val in manifest["defaultValues"].items():
if key not in values:
values[key] = val
if self.args.setvalues is not None:
for i in self.args.setvalues:
split = i.find("=")
if split < 0:
raise ValueError("Invalid value '%s'. Expected form NAME=VALUE" % i)
key, val = i[:split], i[split+1:]
values[key] = val
values["DESTDIR"] = destination
values["NAME"] = name
values["EXEC_START"], values["EXEC_STOP"] = self._generate_systemd_startstop_directives(name)
values["HOST_UID"] = os.getuid()
values["HOST_GID"] = os.getgid()
def _write_template(inputfilename, data, values, destination):
try:
os.makedirs(os.path.dirname(destination))
except OSError:
pass
with open(destination, "w") as outfile:
template = Template(data)
result = template.safe_substitute(values)
missing = {"".join(x) for x in template.pattern.findall(data) if "".join(x) not in values} # pylint: disable=no-member
if len(missing):
raise ValueError("The template file '%s' still contains unreplaced values for: %s" % \
(inputfilename, ", ".join(missing)))
outfile.write(result)
src = os.path.join(exports, "config.json")
destination_path = os.path.join(destination, "config.json")
if os.path.exists(src):
shutil.copyfile(src, destination_path)
elif os.path.exists(src + ".template"):
with open(src + ".template", 'r') as infile:
_write_template(src + ".template", infile.read(), values, destination_path)
else:
self._generate_default_oci_configuration(destination)
if remote_path:
with open(destination_path, 'r') as config_file:
config = json.loads(config_file.read())
config['root']['path'] = remote_rootfs
with open(destination_path, 'w') as config_file:
config_file.write(json.dumps(config, indent=4))
# When upgrading, stop the service and remove previously installed
# tmpfiles, before restarting the service.
if upgrade:
if was_service_active:
self._systemctl_command("stop", name)
if os.path.exists(tmpfilesout):
try:
self._systemd_tmpfiles("--remove", tmpfilesout)
except subprocess.CalledProcessError:
pass
missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path)
image_manifest = self._image_manifest(repo, rev)
image_id = rev
if image_manifest:
image_manifest = json.loads(image_manifest)
if 'Digest' in image_manifest:
image_id = image_manifest['Digest'].replace("sha256:", "")
with open(os.path.join(destination, "info"), 'w') as info_file:
info = {"image" : img,
"revision" : image_id,
"ostree-commit": rev,
'created' : calendar.timegm(time.gmtime()),
"values" : values,
"remote" : remote}
info_file.write(json.dumps(info, indent=4))
if os.path.exists(unitfile):
with open(unitfile, 'r') as infile:
systemd_template = infile.read()
else:
systemd_template = SYSTEMD_UNIT_FILE_DEFAULT_TEMPLATE
if os.path.exists(tmpfiles):
with open(tmpfiles, 'r') as infile:
tmpfiles_template = infile.read()
else:
tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths, values["STATE_DIRECTORY"])
_write_template(unitfile, systemd_template, values, unitfileout)
shutil.copyfile(unitfileout, os.path.join(destination, "%s.service" % name))
if (tmpfiles_template):
_write_template(unitfile, tmpfiles_template, values, tmpfilesout)
shutil.copyfile(unitfileout, os.path.join(destination, "tmpfiles-%s.conf" % name))
sym = "%s/%s" % (self._get_system_checkout_path(), name)
if os.path.exists(sym):
os.unlink(sym)
os.symlink(destination, sym)
self._systemctl_command("daemon-reload")
if (tmpfiles_template):
self._systemd_tmpfiles("--create", tmpfilesout)
if not upgrade:
self._systemctl_command("enable", name)
elif was_service_active:
self._systemctl_command("start", name)
def _get_system_checkout_path(self):
if os.environ.get("ATOMIC_OSTREE_CHECKOUT_PATH"):
return os.environ.get("ATOMIC_OSTREE_CHECKOUT_PATH")
if self.get_atomic_config_item(["checkout_path"]):
return self.get_atomic_config_item(["checkout_path"])
if self.user:
return ATOMIC_VAR_USER
else:
return ATOMIC_VAR
def get_ostree_repo_location(self):
if self.user:
return "%s/.containers/repo" % HOME
else:
return os.environ.get("ATOMIC_OSTREE_REPO") or \
self.get_atomic_config_item(["ostree_repository"]) or \
"/ostree/repo"
def _get_ostree_repo(self):
if not OSTREE_PRESENT:
return None
repo_location = self.get_ostree_repo_location()
repo = OSTree.Repo.new(Gio.File.new_for_path(repo_location))
# If the repository doesn't exist at the specified location, create it
if not os.path.exists(os.path.join(repo_location, "config")):
os.makedirs(repo_location)
if self.user:
repo.create(OSTree.RepoMode.BARE_USER)
else:
repo.create(OSTree.RepoMode.BARE)
repo.open(None)
return repo
def version(self, image):
image_inspect = self.inspect_system_image(image)
if image_inspect:
return image_inspect['ImageId']
return None
def update(self, name):
repo = self._get_ostree_repo()
if not repo:
raise ValueError("Cannot find a configured OSTree repo")
path = os.path.join(self._get_system_checkout_path(), name)
with open(os.path.join(path, "info"), 'r') as info_file:
info = json.loads(info_file.read())
self.args.remote = info['remote']
if self.args.remote:
util.write_out("Updating a container with a remote rootfs. Only changes to config will be applied.")
next_deployment = 0
if os.path.realpath(path).endswith(".0"):
next_deployment = 1
else:
next_deployment = 0
with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file:
info = json.loads(info_file.read())
image = info["image"]
values = info["values"]
revision = info["revision"] if "revision" in info else None
if revision and self.args.setvalues is None:
image_inspect = self.inspect_system_image(image)
if image_inspect:
if image_inspect['ImageId'] == revision:
# Nothing to do
util.write_out("Latest version already installed.")
return
if os.path.exists("%s/%s.%d" % (self._get_system_checkout_path(), name, next_deployment)):
shutil.rmtree("%s/%s.%d" % (self._get_system_checkout_path(), name, next_deployment))
self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote)
def get_container_runtime_info(self, container):
if self._is_service_active(container):
return {'status' : "running"}
elif self._is_service_failed(container):
return {'status' : "failed"}
else:
# The container is newly created or stopped, and can be started with 'systemctl start'
return {'status' : "inactive"}
def get_containers(self):
checkouts = self._get_system_checkout_path()
if not os.path.exists(checkouts):
return []
ret = []
for x in os.listdir(checkouts):
fullpath = os.path.join(checkouts, x)
if not os.path.islink(fullpath):
continue
with open(os.path.join(fullpath, "info"), "r") as info_file:
info = json.load(info_file)
revision = info["revision"] if "revision" in info else ""
created = info["created"] if "created" in info else ""
image = info["image"] if "image" in info else ""
with open(os.path.join(fullpath, "config.json"), "r") as config_file:
config = json.load(config_file)
command = u' '.join(config["process"]["args"])
container = {'Image' : image, 'ImageID' : revision, 'Id' : x, 'Created' : created, 'Names' : [x],
'Command' : command, 'Type' : 'system'}
ret.append(container)
return ret
def delete_image(self, image):
repo = self._get_ostree_repo()
if not repo:
return
imagebranch, commit_rev = self._resolve_image(repo, image)
if not commit_rev:
return
ref = OSTree.parse_refspec(imagebranch)
repo.set_ref_immediate(ref[1], ref[2], None)
def inspect_system_image(self, image):
repo = self._get_ostree_repo()
if not repo:
return None
return self._inspect_system_branch(repo, image)
def _inspect_system_branch(self, repo, imagebranch):
if imagebranch.startswith(OSTREE_OCIIMAGE_PREFIX):
commit_rev = repo.resolve_rev(imagebranch, False)[1]
else:
_, commit_rev = self._resolve_image(repo, imagebranch)
if commit_rev is None:
raise ValueError("Image %s not found" % imagebranch)
commit = repo.load_commit(commit_rev)[1]
branch_id = SystemContainers._decode_from_ostree_ref(imagebranch.replace(OSTREE_OCIIMAGE_PREFIX, ""))
tag = ":".join(branch_id.rsplit(':', 1))
timestamp = OSTree.commit_get_timestamp(commit)
labels = {}
manifest = self._image_manifest(repo, commit_rev)
if len(branch_id) == 64:
image_id = branch_id
tag = "<none>"
else:
image_id = commit_rev
if manifest:
manifest = json.loads(manifest)
if 'Labels' in manifest:
labels = manifest['Labels']
if 'Digest' in manifest:
image_id = manifest['Digest'].replace("sha256:", "")
if self.user:
image_type = "user"
else:
image_type = "system"
return {'Id' : image_id, 'ImageId' : image_id, 'RepoTags' : [tag], 'Names' : [], 'Created': timestamp,
'ImageType' : image_type, 'Labels' : labels, 'OSTree-rev' : commit_rev}
def get_system_images(self, get_all=False, repo=None):
if repo is None:
repo = self._get_ostree_repo()
if repo is None:
return []
revs = [x for x in repo.list_refs()[1] if x.startswith(OSTREE_OCIIMAGE_PREFIX) \
and (get_all or len(x) != len(OSTREE_OCIIMAGE_PREFIX) + 64)]
return [self._inspect_system_branch(repo, x) for x in revs]
def _is_service_active(self, name):
try:
return self._systemctl_command("is-active", name, quiet=True).replace("\n", "") == "active"
except subprocess.CalledProcessError:
return False
def _is_service_failed(self, name):
try:
is_failed = self._systemctl_command("is-failed", name, quiet=True).replace("\n", "")
except subprocess.CalledProcessError as e:
is_failed = e.output
if is_failed.replace("\n", "") != "inactive":
return True
if is_failed == "failed":
return True
elif is_failed == "active":
return False
else:
# in case of "inactive", could be a stopped container or failed process
try:
status = self._systemctl_command("status", name, quiet=True)
except subprocess.CalledProcessError as e:
status = e.output
if 'FAILURE' in status:
return True
else:
return False
def start_service(self, name):
try:
self._systemctl_command("start", name)
except subprocess.CalledProcessError as e:
raise ValueError(e.output)
def stop_service(self, name):
try:
self._systemctl_command("stop", name)
except subprocess.CalledProcessError as e:
raise ValueError(e.output)
def _systemd_tmpfiles(self, command, name):
cmd = ["systemd-tmpfiles"] + [command, name]
util.write_out(" ".join(cmd))
if not self.display:
util.check_call(cmd)
def _systemctl_command(self, command, name=None, quiet=False):
cmd = ["systemctl"]
if self.user:
cmd.append("--user")
cmd.append(command)
if name:
cmd.append(name)
if not quiet:
util.write_out(" ".join(cmd))
if not self.display:
return util.check_output(cmd, stderr=DEVNULL)
return None
def get_checkout(self, name):
path = "%s/%s" % (self._get_system_checkout_path(), name)
if os.path.exists(path):
return path
else:
return None
def uninstall(self, name):
unitfileout, tmpfilesout = self._get_systemd_destination_files(name)
try:
self._systemctl_command("stop", name)
except subprocess.CalledProcessError:
pass
try:
self._systemctl_command("disable", name)
except subprocess.CalledProcessError:
pass
if os.path.exists(tmpfilesout):
try:
self._systemd_tmpfiles("--remove", tmpfilesout)
except subprocess.CalledProcessError:
pass
os.unlink(tmpfilesout)
if os.path.lexists("%s/%s" % (self._get_system_checkout_path(), name)):
os.unlink("%s/%s" % (self._get_system_checkout_path(), name))
for deploy in ["0", "1"]:
if os.path.exists("%s/%s.%s" % (self._get_system_checkout_path(), name, deploy)):
shutil.rmtree("%s/%s.%s" % (self._get_system_checkout_path(), name, deploy))
if os.path.exists(unitfileout):
os.unlink(unitfileout)
def prune_ostree_images(self):
repo = self._get_ostree_repo()
if not repo:
return
refs = {}
app_refs = []
for i in repo.list_refs()[1]:
if i.startswith(OSTREE_OCIIMAGE_PREFIX):
if len(i) == len(OSTREE_OCIIMAGE_PREFIX) + 64:
refs[i] = False
else:
invalid_encoding = False
for c in i.replace(OSTREE_OCIIMAGE_PREFIX, ""):
if not str.isalnum(str(c)) and c not in '.-_':
invalid_encoding = True
break
if invalid_encoding:
refs[i] = False
else:
app_refs.append(i)
def visit(rev):
manifest = self._image_manifest(repo, repo.resolve_rev(rev, True)[1])
if not manifest:
return
for layer in SystemContainers.get_layers_from_manifest(json.loads(manifest)):
refs[OSTREE_OCIIMAGE_PREFIX + layer.replace("sha256:", "")] = True
for app in app_refs:
visit(app)
for k, v in refs.items():
if not v:
ref = OSTree.parse_refspec(k)
util.write_out("Deleting %s" % k)
repo.set_ref_immediate(ref[1], ref[2], None)
@staticmethod
def get_default_system_name(image):
image = image.replace("oci:", "").replace("docker:", "")
_, image, tag = SystemContainers._parse_imagename(image)
name = image.split("/")[-1]
if tag != "latest":
name = "%s-%s" % (name, tag)
return name
@staticmethod
def _parse_imagename(imagename):
sep = imagename.find("/")
reg, image = imagename[:sep], imagename[sep + 1:]
if '.' not in reg:
# if the registry doesn't look like a domain, consider it as the
# image prefix
reg = ""
image = imagename
sep = image.find(":")
if sep > 0:
return reg, image[:sep], image[sep + 1:]
else:
return reg, image, "latest"
def _convert_to_skopeo(self, image):
insecure = "http:" in image
for i in ["oci:", "http:", "https:"]:
image = image.replace(i, "")
with AtomicDocker() as client:
fqn_image = util.find_remote_image(client, image) or image
if insecure:
return ["--insecure"], "docker://" + fqn_image
else:
return [], "docker://" + fqn_image
def _skopeo_get_manifest(self, image):
args, img = self._convert_to_skopeo(image)
return util.skopeo_inspect(img, args)
def _skopeo_get_layers(self, image, layers):
args, img = self._convert_to_skopeo(image)
return util.skopeo_layers(img, args, layers)
def _image_manifest(self, repo, rev):
return SystemContainers._get_commit_metadata(repo, rev, "docker.manifest")
def get_manifest(self, image, remote=False):
repo = self._get_ostree_repo()
if not repo:
return None
if remote:
return self._skopeo_get_manifest(image)
imagebranch = SystemContainers._get_ostree_image_branch(image)
commit_rev = repo.resolve_rev(imagebranch, True)
if not commit_rev[1]:
return None
return self._image_manifest(repo, commit_rev[1])
@staticmethod
def get_layers_from_manifest(manifest):
if isinstance(manifest, str):
manifest = json.loads(manifest)
fs_layers = manifest.get("fsLayers")
if fs_layers:
layers = list(i["blobSum"] for i in fs_layers)
layers.reverse()
else:
layers = manifest.get("Layers")
return layers
@staticmethod
def _import_layers_into_ostree(repo, imagebranch, manifest, layers):
repo.prepare_transaction()
for layer, tar in layers.items():
mtree = OSTree.MutableTree()
def filter_func(*args):
info = args[2]
if info.get_file_type() == Gio.FileType.DIRECTORY:
info.set_attribute_uint32("unix::mode", info.get_attribute_uint32("unix::mode") | stat.S_IWUSR)
return OSTree.RepoCommitFilterResult.ALLOW
modifier = OSTree.RepoCommitModifier.new(0, filter_func, None)
metav = GLib.Variant("a{sv}", {'docker.layer': GLib.Variant('s', layer)})
imported = False
try:
repo.write_archive_to_mtree(Gio.File.new_for_path(tar), mtree, modifier, True)
root = repo.write_mtree(mtree)[1]
csum = repo.write_commit(None, "", None, metav, root)[1]
imported = True
except GLib.GError as e: #pylint: disable=catching-non-exception
# libarchive which is used internally by OSTree to import a tarball doesn't support correctly
# files with xattrs. If that happens, extract the tarball and import the directory.
if e.domain != "g-io-error-quark": # pylint: disable=no-member
raise e #pylint: disable=raising-non-exception
if not imported:
try:
temp_dir = tempfile.mkdtemp()
with tarfile.open(tar, 'r') as t:
t.extractall(temp_dir)
repo.write_directory_to_mtree(Gio.File.new_for_path(temp_dir), mtree, modifier)
root = repo.write_mtree(mtree)[1]
csum = repo.write_commit(None, "", None, metav, root)[1]
finally:
shutil.rmtree(temp_dir)
root = repo.write_mtree(mtree)[1]
csum = repo.write_commit(None, "", None, metav, root)[1]
repo.transaction_set_ref(None, "%s%s" % (OSTREE_OCIIMAGE_PREFIX, layer), csum)
# create a $OSTREE_OCIIMAGE_PREFIX$image-$tag branch
if not isinstance(manifest, str):
manifest = json.dumps(manifest)
metadata = GLib.Variant("a{sv}", {'docker.manifest': GLib.Variant('s', manifest)})
mtree = OSTree.MutableTree()
file_info = Gio.FileInfo()
file_info.set_attribute_uint32("unix::uid", 0)
file_info.set_attribute_uint32("unix::gid", 0)
file_info.set_attribute_uint32("unix::mode", 0o755 | stat.S_IFDIR)
dirmeta = OSTree.create_directory_metadata(file_info, None)
csum_dirmeta = repo.write_metadata(OSTree.ObjectType.DIR_META, None, dirmeta)[1]
mtree.set_metadata_checksum(OSTree.checksum_from_bytes(csum_dirmeta))
root = repo.write_mtree(mtree)[1]
csum = repo.write_commit(None, "", None, metadata, root)[1]
repo.transaction_set_ref(None, imagebranch, csum)
repo.commit_transaction(None)
def _pull_docker_image(self, repo, image):
with tempfile.NamedTemporaryFile(mode="w") as temptar:
util.check_call(["docker", "save", "-o", temptar.name, image])
return self._pull_docker_tar(repo, temptar.name)
def _pull_docker_tar(self, repo, image):
temp_dir = tempfile.mkdtemp()
try:
with tarfile.open(image, 'r') as t:
t.extractall(temp_dir)
manifest_file = os.path.join(temp_dir, "manifest.json")
if os.path.exists(manifest_file):
manifest = ""
with open(manifest_file, 'r') as mfile:
manifest = mfile.read()
for m in json.loads(manifest):
if "Config" in m:
config_file = os.path.join(temp_dir, m["Config"])
with open(config_file, 'r') as config:
config = json.loads(config.read())
labels = config['config']['Labels']
imagename = m["RepoTags"][0]
imagebranch = "%s%s" % (OSTREE_OCIIMAGE_PREFIX, SystemContainers._encode_to_ostree_ref(imagename))
input_layers = m["Layers"]
self._pull_dockertar_layers(repo, imagebranch, temp_dir, input_layers, labels=labels)
else:
repositories = ""
repositories_file = os.path.join(temp_dir, "repositories")
with open(repositories_file, 'r') as rfile:
repositories = rfile.read()
imagename = list(json.loads(repositories).keys())[0]
imagebranch = "%s%s" % (OSTREE_OCIIMAGE_PREFIX, SystemContainers._encode_to_ostree_ref(imagename))
input_layers = []
for name in os.listdir(temp_dir):
if name == "repositories":
continue
input_layers.append(name + "/layer.tar")
self._pull_dockertar_layers(repo, imagebranch, temp_dir, input_layers)
finally:
shutil.rmtree(temp_dir)
def _check_system_ostree_image(self, repo, img, upgrade):
imagebranch = img.replace("ostree:", "")
current_rev = repo.resolve_rev(imagebranch, True)
if not upgrade and current_rev[1]:
return False
remote, branch = imagebranch.split(":")
return repo.pull(remote, [branch], 0, None)
def _check_system_oci_image(self, repo, img, upgrade):
imagebranch = "%s%s" % (OSTREE_OCIIMAGE_PREFIX, SystemContainers._encode_to_ostree_ref(img))
current_rev = repo.resolve_rev(imagebranch, True)
if not upgrade and current_rev[1]:
return False
manifest = self._skopeo_get_manifest(img)
layers = SystemContainers.get_layers_from_manifest(manifest)
missing_layers = []
for i in layers:
layer = i.replace("sha256:", "")
has_layer = repo.resolve_rev("%s%s" % (OSTREE_OCIIMAGE_PREFIX, layer), True)[1]
if not has_layer:
missing_layers.append(layer)
util.write_out("Pulling layer %s" % layer)
layers_dir = None
try:
layers_to_import = {}
if len(missing_layers):
layers_dir = self._skopeo_get_layers(img, missing_layers)
for root, _, files in os.walk(layers_dir):
for f in files:
if f.endswith(".tar"):
layer_file = os.path.join(root, f)
layer = f.replace(".tar", "")
if not repo.resolve_rev("%s%s" % (OSTREE_OCIIMAGE_PREFIX, layer), True)[1]:
layers_to_import[layer] = layer_file
SystemContainers._import_layers_into_ostree(repo, imagebranch, manifest, layers_to_import)
finally:
if layers_dir:
shutil.rmtree(layers_dir)
return True
@staticmethod
def _generate_tmpfiles_data(missing_bind_paths, state_directory):
def _generate_line(x, state):
return "%s %s 0700 %i %i - -\n" % (state, x, os.getuid(), os.getgid())
lines = []
for x in missing_bind_paths:
if os.path.commonprefix([x, state_directory]) == state_directory:
lines.append(_generate_line(x, "d"))
else:
lines.append(_generate_line(x, "D"))
lines.append(_generate_line(x, "R"))
return "".join(lines)
@staticmethod
def _get_commit_metadata(repo, rev, key):
commit = repo.load_commit(rev)[1]
metadata = commit.get_child_value(0)
if key not in metadata.keys():
return None
return metadata[key]
def extract(self, img, destination):
repo = self._get_ostree_repo()
if not repo:
return False
return self._checkout(repo, img, img, 0, False, destination=destination, extract_only=True)
@staticmethod
def _encode_to_ostree_ref(name):
def convert(x):
return (x if str.isalnum(str(x)) or x in '.-' else "_%02X" % ord(x))
if name.startswith("oci:"):
name = name[len("oci:"):]
registry, image, tag = SystemContainers._parse_imagename(name)
if registry:
fullname = "%s/%s:%s" % (registry, image, tag)
else:
fullname = "%s:%s" % (image, tag)
ret = "".join([convert(i) for i in fullname])
return ret
@staticmethod
def _decode_from_ostree_ref(name):
try:
l = []
i = 0
while i < len(name):
if name[i] == '_':
l.append(str(chr(int(name[i+1:i+3], 16))))
i = i + 3
else:
l.append(name[i])
i = i + 1
return "".join(l)
except ValueError:
return name
@staticmethod
def _get_ostree_image_branch(img):
if "ostree:" in img:
imagebranch = img.replace("ostree:", "")
else: # assume "oci:" image
imagebranch = "%s%s" % (OSTREE_OCIIMAGE_PREFIX, SystemContainers._encode_to_ostree_ref(img.replace("sha256:", "")))
return imagebranch
def has_image(self, img):
repo = self._get_ostree_repo()
if not repo:
return False
_, rev = self._resolve_image(repo, img)
return True if rev else False
def _pull_dockertar_layers(self, repo, imagebranch, temp_dir, input_layers, labels=None):
layers = {}
next_layer = {}
top_layer = None
for i in input_layers:
layer = i.replace("/layer.tar", "")
layers[layer] = os.path.join(temp_dir, i)
with open(os.path.join(temp_dir, layer, "json"), 'r') as f:
json_layer = json.loads(f.read())
parent = json_layer.get("parent")
if not parent:
top_layer = layer
next_layer[parent] = layer
layers_map = {}
enc = sys.getdefaultencoding()
for k, v in layers.items():
out = util.check_output([ATOMIC_LIBEXEC + '/dockertar-sha256-helper', v],
stderr=DEVNULL)
layers_map[k] = out.decode(enc).replace("\n", "")
layers_ordered = []
it = top_layer
while it:
layers_ordered.append(layers_map[it])
it = next_layer.get(it)
manifest = json.dumps({"Layers" : layers_ordered, "Labels" : labels})
layers_to_import = {}
for k, v in layers.items():
layers_to_import[layers_map[k]] = v
SystemContainers._import_layers_into_ostree(repo, imagebranch, manifest, layers_to_import)
def validate_layer(self, layer):
ret = []
layer = layer.replace("sha256:", "")
repo = self._get_ostree_repo()
if not repo:
return ret
def validate_ostree_file(csum):
_, inputfile, file_info, xattrs = repo.load_file(csum)
# images are imported from layer tarballs, without any xattr. Don't use xattr to compute
# the OSTree object checksum.
xattrs = GLib.Variant("a(ayay)", [])
_, checksum_v = OSTree.checksum_file_from_input(file_info, xattrs, inputfile, OSTree.ObjectType.FILE)
return OSTree.checksum_from_bytes(checksum_v)
def traverse(it):
while True:
res = it.next() # pylint: disable=next-method-called
if res == OSTree.RepoCommitIterResult.DIR:
dir_checksum = it.get_dir().out_content_checksum
dir_it = OSTree.RepoCommitTraverseIter()
dirtree = repo.load_variant(OSTree.ObjectType.DIR_TREE, dir_checksum)
dir_it.init_dirtree(repo, dirtree[1], OSTree.RepoCommitTraverseFlags.REPO_COMMIT_TRAVERSE_FLAG_NONE)
traverse(dir_it)
elif res == OSTree.RepoCommitIterResult.FILE:
new_checksum = validate_ostree_file(it.get_file().out_checksum)
if new_checksum != it.get_file().out_checksum:
ret.append({"name" : it.get_file().out_name,
"old-checksum" : it.get_file().out_checksum,
"new-checksum" : new_checksum})
elif res == OSTree.RepoCommitIterResult.ERROR:
raise ValueError("Internal error while validating the layer")
elif res == OSTree.RepoCommitIterResult.END:
break
current_rev = repo.resolve_rev("%s%s" % (OSTREE_OCIIMAGE_PREFIX, layer), False)[1]
it = OSTree.RepoCommitTraverseIter()
it.init_commit(repo, repo.load_commit(current_rev)[1], OSTree.RepoCommitTraverseFlags.REPO_COMMIT_TRAVERSE_FLAG_NONE)
traverse(it)
return ret