From 26b454ce22d3e8e6c0ec33bb4f190eee65a8b0c7 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 23 Nov 2016 12:09:11 +0100 Subject: [PATCH 01/18] syscontainers: supported preinstalled containers In addition to containers installed under /var/lib, support preinstalled containers under /usr/lib which are not managed by atomic (update, remove). Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 102 +++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index b42245fd..e6720afb 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -35,12 +35,15 @@ ATOMIC_LIBEXEC = os.environ.get('ATOMIC_LIBEXEC', '/usr/libexec/atomic') ATOMIC_VAR = '/var/lib/containers/atomic' +ATOMIC_USR = '/usr/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_FILES_DEST_PREFIX = "%s/usr/lib/systemd/system" +SYSTEMD_TMPFILES_DEST_PREFIX = "%s/usr/lib/tmpfiles.d" SYSTEMD_UNIT_FILE_DEFAULT_TEMPLATE = """ [Unit] Description=$NAME @@ -199,7 +202,7 @@ def install(self, image, name): return self._checkout(repo, name, image, 0, False, values=values, remote=self.args.remote) - def _check_oci_configuration_file(self, conf_path, remote=None): + def _check_oci_configuration_file(self, conf_path, remote=None, include_all=False): with open(conf_path, 'r') as conf: try: configuration = json.loads(conf.read()) @@ -223,7 +226,7 @@ def _check_oci_configuration_file(self, conf_path, remote=None): continue if "source" in mount and "bind" in mount["type"]: source = mount["source"] - if not os.path.exists(source): + if include_all or not os.path.exists(source): missing_source_paths.append(source) return missing_source_paths @@ -262,13 +265,17 @@ def _generate_systemd_startstop_directives(self, name): 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): + def _get_systemd_destination_files(self, name, prefix=None): 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) + if prefix: + unitfileout = os.path.join(SYSTEMD_UNIT_FILES_DEST_PREFIX % prefix, "%s.service" % name) + tmpfilesout = os.path.join(SYSTEMD_TMPFILES_DEST_PREFIX % prefix, "%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): @@ -280,9 +287,9 @@ def _resolve_remote_path(self, remote_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): + def _checkout(self, repo, name, img, deployment, upgrade, values=None, destination=None, extract_only=False, remote=None, prefix=None): destination = destination or "%s/%s.%d" % (self._get_system_checkout_path(), name, deployment) - unitfileout, tmpfilesout = self._get_systemd_destination_files(name) + unitfileout, tmpfilesout = self._get_systemd_destination_files(name, prefix) if not upgrade: for f in [unitfileout, tmpfilesout]: @@ -290,7 +297,7 @@ def _checkout(self, repo, name, img, deployment, upgrade, values=None, destinati raise ValueError("The file %s already exists." % f) try: - return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote) + return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix) except (ValueError, OSError) as e: try: if not extract_only and not upgrade: @@ -352,7 +359,7 @@ def get_image(i): return [get_image(i) for i in matches] - def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote): + def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None): if not values: values = {} @@ -469,7 +476,11 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou if "UUID" not in values: values["UUID"] = str(uuid.uuid4()) - values["DESTDIR"] = destination + if prefix: + values["DESTDIR"] = os.path.join("/", os.path.relpath(destination, prefix)) + else: + values["DESTDIR"] = destination + values["NAME"] = name values["EXEC_START"], values["EXEC_STOP"] = self._generate_systemd_startstop_directives(name) values["HOST_UID"] = os.getuid() @@ -524,7 +535,7 @@ def _write_template(inputfilename, data, values, destination): except subprocess.CalledProcessError: pass - missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path) + missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) image_manifest = self._image_manifest(repo, rev) image_id = rev @@ -554,24 +565,28 @@ def _write_template(inputfilename, data, values, destination): 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)) + shutil.copyfile(unitfileout, os.path.join(prefix, destination, "%s.service" % name)) if (tmpfiles_template): _write_template(unitfile, tmpfiles_template, values, tmpfilesout) - shutil.copyfile(tmpfilesout, os.path.join(destination, "tmpfiles-%s.conf" % name)) + shutil.copyfile(tmpfilesout, os.path.join(prefix, 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) + if not prefix: + 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) + 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) + if not upgrade: + self._systemctl_command("enable", name) + elif was_service_active: + self._systemctl_command("start", name) + + def _get_preinstalled_containers_path(self): + return ATOMIC_USR def _get_system_checkout_path(self): if os.environ.get("ATOMIC_OSTREE_CHECKOUT_PATH"): @@ -616,6 +631,9 @@ def version(self, image): return None def update_container(self, name, setvalues=None, rebase=None): + if self._is_preinstalled_container(name): + raise ValueError("Cannot update a preinstalled container") + repo = self._get_ostree_repo() if not repo: raise ValueError("Cannot find a configured OSTree repo") @@ -716,9 +734,8 @@ def get_container_runtime_info(self, container): # The container is newly created or stopped, and can be started with 'systemctl start' return {'status' : "inactive"} - def get_containers(self, containers=None): - checkouts = self._get_system_checkout_path() - if not os.path.exists(checkouts): + def _get_containers_at(self, checkouts, are_preinstalled, containers=None): + if not checkouts or not os.path.exists(checkouts): return [] ret = [] if containers is None: @@ -727,7 +744,9 @@ def get_containers(self, containers=None): if x[0] == ".": continue fullpath = os.path.join(checkouts, x) - if not os.path.islink(fullpath): + if not os.path.exists(fullpath): + continue + if fullpath.endswith(".0") or fullpath.endswith(".1"): continue with open(os.path.join(fullpath, "info"), "r") as info_file: @@ -742,10 +761,15 @@ def get_containers(self, containers=None): runtime = "bwrap-oci" if self.user else "runc" container = {'Image' : image, 'ImageID' : revision, 'Id' : x, 'Created' : created, 'Names' : [x], - 'Command' : command, 'Type' : 'system', 'Runtime' : runtime} + 'Command' : command, 'Type' : 'system', 'Runtime' : runtime, "Preinstalled" : are_preinstalled} ret.append(container) return ret + def get_containers(self, containers=None): + checkouts = self._get_system_checkout_path() + preinstalled = self._get_preinstalled_containers_path() + return self._get_containers_at(checkouts, False, containers) + self._get_containers_at(preinstalled, True, containers) + def get_template_variables(self, image): repo = self._get_ostree_repo() imgs = self._resolve_image(repo, image) @@ -944,12 +968,26 @@ def get_checkout(self, name): path = "%s/%s" % (self._get_system_checkout_path(), name) if os.path.exists(path): return path - else: - return None + + path = "%s/%s" % (self._get_preinstalled_containers_path(), name) + if os.path.exists(path): + return path + + return None + + def _is_preinstalled_container(self, name): + path = "%s/%s" % (self._get_system_checkout_path(), name) + if os.path.exists(path): + return False + + path = "%s/%s" % (self._get_preinstalled_containers_path(), name) + return os.path.exists(path) def uninstall(self, name): - unitfileout, tmpfilesout = self._get_systemd_destination_files(name) + if self._is_preinstalled_container(name): + raise ValueError("Cannot uninstall a preinstalled container") + unitfileout, tmpfilesout = self._get_systemd_destination_files(name) try: self._systemctl_command("stop", name) except subprocess.CalledProcessError: From b0c0ad1b69dbf830f58a1b532e5666e02fc748bf Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 23 Nov 2016 12:11:52 +0100 Subject: [PATCH 02/18] syscontainers: add generation of an rpm Add a hidden (for now) option ---generate-rpm to "install --system" to generate an rpm file instead of installing the container. The rpm can be installed on the system and the container won't be managed by atomic, update and uninstall are disabled. Differently from containers handled by atomic that installs the container under /var/lib/containers/atomic, the RPM version installs its files under /usr/lib/containers and can be integrated in an atomic image as a normal rpm. Signed-off-by: Giuseppe Scrivano --- Atomic/install.py | 3 + Atomic/syscontainers.py | 119 ++++++++++++++++++++++++++++++++++------ 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/Atomic/install.py b/Atomic/install.py index e1831fb7..8cd8b13b 100644 --- a/Atomic/install.py +++ b/Atomic/install.py @@ -62,6 +62,9 @@ def cli(subparser): system_xor_user.add_argument("--system", dest="system", action='store_true', default=False, help=_('install a system container')) + installp.add_argument("---generate-rpm", dest="generate_rpm", + action='store_true', default=False, + help=_('generate an rpm instead of installing the container')) installp.add_argument("--rootfs", dest="remote", help=_("choose an existing exploded container/image to use " "its rootfs as a remote, read-only rootfs for the " diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index e6720afb..586264fe 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -44,6 +44,7 @@ SYSTEMD_TMPFILES_DEST_USER = "%s/.containers/tmpfiles" % HOME SYSTEMD_UNIT_FILES_DEST_PREFIX = "%s/usr/lib/systemd/system" SYSTEMD_TMPFILES_DEST_PREFIX = "%s/usr/lib/tmpfiles.d" +RPM_NAME_PREFIX = "atomic-container" SYSTEMD_UNIT_FILE_DEFAULT_TEMPLATE = """ [Unit] Description=$NAME @@ -188,6 +189,11 @@ def install(self, image, name): if self.args.system and self.user: raise ValueError("Only root can use --system") + if self.args.generate_rpm: + if not self.args.system: + raise ValueError("Only --system can generate rpms") + return self.generate_rpm(repo, name, image) + image = self._pull_image_to_ostree(repo, image, False) if self.get_checkout(name): @@ -476,11 +482,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou if "UUID" not in values: values["UUID"] = str(uuid.uuid4()) - if prefix: - values["DESTDIR"] = os.path.join("/", os.path.relpath(destination, prefix)) - else: - values["DESTDIR"] = destination - + values["DESTDIR"] = os.path.join("/", os.path.relpath(destination, prefix)) if prefix else destination values["NAME"] = name values["EXEC_START"], values["EXEC_STOP"] = self._generate_systemd_startstop_directives(name) values["HOST_UID"] = os.getuid() @@ -570,20 +572,22 @@ def _write_template(inputfilename, data, values, destination): _write_template(unitfile, tmpfiles_template, values, tmpfilesout) shutil.copyfile(tmpfilesout, os.path.join(prefix, destination, "tmpfiles-%s.conf" % name)) - if not prefix: - sym = "%s/%s" % (self._get_system_checkout_path(), name) - if os.path.exists(sym): - os.unlink(sym) - os.symlink(destination, sym) + if prefix: + return + + 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) + 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) + if not upgrade: + self._systemctl_command("enable", name) + elif was_service_active: + self._systemctl_command("start", name) def _get_preinstalled_containers_path(self): return ATOMIC_USR @@ -904,7 +908,8 @@ def get_system_images(self, get_all=False, repo=None): def _is_service_active(self, name): try: - return self._systemctl_command("is-active", name, quiet=True).replace("\n", "") == "active" + ret = self._systemctl_command("is-active", name, quiet=True) + return ret and ret.replace("\n", "") == "active" except subprocess.CalledProcessError: return False @@ -1442,3 +1447,81 @@ def get_out_checksum(obj): return obj.out_checksum if hasattr(obj, 'out_checksum it.init_commit(repo, repo.load_commit(current_rev)[1], OSTree.RepoCommitTraverseFlags.REPO_COMMIT_TRAVERSE_FLAG_NONE) traverse(it) return ret + + def generate_rpm(self, repo, name, image): + image_inspect = self.inspect_system_image(image) + temp_dir = tempfile.mkdtemp() + rpm_content = os.path.join(temp_dir, "rpmroot") + rootfs = os.path.join(rpm_content, "usr/lib/containers/atomic", name) + os.makedirs(rootfs) + try: + spec_file = os.path.join(temp_dir, "container.spec") + self._checkout(repo, name, image, 0, False, destination=rootfs, prefix=rpm_content) + + if self.display: + return + + labels = {k.lower() : v for k, v in image_inspect.get('Labels', {})} + summary = labels.get('summary', name) + version = labels.get("version", "1.0") + release = labels.get("release", "1.0") + license_ = labels.get("license", "GPLv2") + url = labels.get("url") + source0 = labels.get("source0") + requires = labels.get("requires") + provides = labels.get("provides") + conflicts = labels.get("conflicts") + description = labels.get("description") + + self._generate_spec_file(spec_file, rpm_content, name, summary, license_, version=version, release=release, + url=url, source0=source0, requires=requires, provides=provides, conflicts=conflicts, + description=description) + + cwd = os.getcwd() + cmd = ["rpmbuild", "--noclean", "-bb", spec_file, + "--define", "_sourcedir %s" % temp_dir, + "--define", "_specdir %s" % temp_dir, + "--define", "_builddir %s" % temp_dir, + "--define", "_srcrpmdir %s" % cwd, + "--define", "_rpmdir %s" % cwd, + "--build-in-place", + "--buildroot=%s" % rpm_content] + util.write_out(" ".join(cmd)) + if not self.display: + util.check_call(cmd) + return False + finally: + shutil.rmtree(temp_dir) + + def _generate_spec_file(self, out, destdir, name, summary, license_, version="1.0", release="1", url=None, + source0=None, requires=None, conflicts=None, provides=None, description=None): + spec = "%global __requires_exclude_from ^.*$\n" + spec = spec + "%global __provides_exclude_from ^.*$\n" + + fields = {"Name" : "%s-%s" % (RPM_NAME_PREFIX, name), "Version" : version, "Release" : release, "Summary" : summary, + "License" : license_, "URL" : url, "Source0" : source0, "Requires" : requires, + "Provides" : provides, "Conflicts" : conflicts} + for k, v in fields.items(): + if v is not None: + spec = spec + "%s:\t%s\n" % (k, v) + + spec = spec + "\n%description\n" + if description: + spec = spec + "%s\n" % description + + spec = spec + "\n%files\n" + for root, _, files in os.walk(os.path.join(destdir, "etc")): + rel_path = os.path.relpath(root, destdir) + for f in files: + spec += "%config \"%s\"\n" % os.path.join("/", rel_path, f) + + spec += "/usr/lib/containers/atomic/%s\n" % name + for root, _, files in os.walk(os.path.join(destdir, "usr/lib/systemd/system")): + for f in files: + spec = spec + "/usr/lib/systemd/system/%s\n" % f + for root, _, files in os.walk(os.path.join(destdir, "usr/lib/tmpfiles.d")): + for f in files: + spec = spec + "/usr/lib/tmpfiles.d/%s\n" % f + + with open(out, "w") as f: + f.write(spec) From b53737fd4dda8d8abba06627b729b8da9bc3f70a Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 25 Nov 2016 12:04:22 +0100 Subject: [PATCH 03/18] syscontainers: allow to copy arbitrary files to the host Copy the /exports/hostfs/ to the host file system. These files are tracked into the "info" file for a deployment. Change update/rollback/uninstall to honor the new configuration. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 102 ++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 586264fe..83d96b1c 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -293,7 +293,7 @@ def _resolve_remote_path(self, remote_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, prefix=None): + def _checkout(self, repo, name, img, deployment, upgrade, values=None, destination=None, extract_only=False, remote=None, prefix=None, installed_files=None): destination = destination or "%s/%s.%d" % (self._get_system_checkout_path(), name, deployment) unitfileout, tmpfilesout = self._get_systemd_destination_files(name, prefix) @@ -303,7 +303,7 @@ def _checkout(self, repo, name, img, deployment, upgrade, values=None, destinati raise ValueError("The file %s already exists." % f) try: - return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix) + return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix, installed_files=installed_files) except (ValueError, OSError) as e: try: if not extract_only and not upgrade: @@ -365,7 +365,23 @@ def get_image(i): return [get_image(i) for i in matches] - def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None): + @staticmethod + 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) + try: + result = template.substitute(values) + except KeyError as e: + os.unlink(destination) + raise ValueError("The template file '%s' still contains an unreplaced value for: '%s'" % \ + (inputfilename, str(e))) + outfile.write(result) + + def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None, installed_files=None): if not values: values = {} @@ -488,28 +504,13 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou 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) - try: - result = template.substitute(values) - except KeyError as e: - os.unlink(destination) - raise ValueError("The template file '%s' still contains an unreplaced value for: '%s'" % \ - (inputfilename, str(e))) - 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) + SystemContainers._write_template(src + ".template", infile.read(), values, destination_path) else: self._generate_default_oci_configuration(destination) @@ -537,6 +538,8 @@ def _write_template(inputfilename, data, values, destination): except subprocess.CalledProcessError: pass + new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix) + missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) image_manifest = self._image_manifest(repo, rev) @@ -551,8 +554,10 @@ def _write_template(inputfilename, data, values, destination): "ostree-commit": rev, 'created' : calendar.timegm(time.gmtime()), "values" : values, + "installed-files": new_installed_files, "remote" : remote} info_file.write(json.dumps(info, indent=4)) + info_file.write("\n") if os.path.exists(unitfile): with open(unitfile, 'r') as infile: @@ -566,10 +571,10 @@ def _write_template(inputfilename, data, values, destination): else: tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths, values["STATE_DIRECTORY"]) - _write_template(unitfile, systemd_template, values, unitfileout) + self._write_template(unitfile, systemd_template, values, unitfileout) shutil.copyfile(unitfileout, os.path.join(prefix, destination, "%s.service" % name)) if (tmpfiles_template): - _write_template(unitfile, tmpfiles_template, values, tmpfilesout) + self._write_template(unitfile, tmpfiles_template, values, tmpfilesout) shutil.copyfile(tmpfilesout, os.path.join(prefix, destination, "tmpfiles-%s.conf" % name)) if prefix: @@ -634,6 +639,35 @@ def version(self, image): return [image_inspect] return None + @staticmethod + def _rm_add_files_to_host(old_installed_files, exports, prefix="/"): + # if any file was installed on the host delete it + if old_installed_files: + for i in old_installed_files: + try: + os.remove(i) + except OSError: + pass + + if not exports: + return [] + + # if there is a directory hostfs/ under exports, copy these files to the host file system. + hostfs = os.path.join(exports, "hostfs") + new_installed_files = [] + if os.path.exists(hostfs): + for root, _, files in os.walk(hostfs): + rel_root_path = os.path.relpath(root, hostfs) + if not os.path.exists(os.path.join(prefix, rel_root_path)): + os.makedirs(os.path.join(prefix, rel_root_path)) + for f in files: + path = os.path.join(prefix, rel_root_path, f) + shutil.copy2(os.path.join(root, f), path) + new_installed_files.append(os.path.join("/", rel_root_path, f)) + new_installed_files.sort() # just for an aesthetic reason in the info file output + + return new_installed_files + def update_container(self, name, setvalues=None, rebase=None): if self._is_preinstalled_container(name): raise ValueError("Cannot update a preinstalled container") @@ -659,6 +693,7 @@ def update_container(self, name, setvalues=None, rebase=None): image = rebase or info["image"] values = info["values"] revision = info["revision"] if "revision" in info else None + installed_files = info["installed-files"] if "installed-files" in info else None # Check if the image id or the configuration for the container has # changed before upgrading it. @@ -684,7 +719,7 @@ def update_container(self, name, setvalues=None, rebase=None): util.write_out("Latest version already installed.") return - self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote) + self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote, installed_files=installed_files) def rollback(self, name): path = os.path.join(self._get_system_checkout_path(), name) @@ -692,6 +727,11 @@ def rollback(self, name): if not os.path.exists(destination): raise ValueError("Error: Cannot find a previous deployment to rollback located at %s" % destination) + installed_files = None + with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: + info = json.loads(info_file.read()) + installed_files = info["installed-files"] if "installed-files" in info else None + was_service_active = self._is_service_active(name) unitfileout, tmpfilesout = self._get_systemd_destination_files(name) unitfile = os.path.join(destination, "%s.service" % name) @@ -719,6 +759,8 @@ def rollback(self, name): if (os.path.exists(tmpfiles)): shutil.copyfile(tmpfiles, tmpfilesout) + self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports")) + os.unlink(path) os.symlink(destination, path) self._systemctl_command("daemon-reload") @@ -1009,11 +1051,19 @@ def uninstall(self, name): 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)) + checkout = self._get_system_checkout_path() + installed_files = None + with open(os.path.join(checkout, name, "info"), 'r') as info_file: + info = json.loads(info_file.read()) + installed_files = info["installed-files"] if "installed-files" in info else None + if installed_files: + self._rm_add_files_to_host(installed_files, None) + + if os.path.lexists("%s/%s" % (checkout, name)): + os.unlink("%s/%s" % (checkout, 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("%s/%s.%s" % (checkout, name, deploy)): + shutil.rmtree("%s/%s.%s" % (checkout, name, deploy)) if os.path.exists(unitfileout): os.unlink(unitfileout) From 1715b9526aa6f174bebc0b1086795b2f3d8f5876 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 25 Nov 2016 12:41:26 +0100 Subject: [PATCH 04/18] syscontainers: rpm generation handles host files When generating an rpm, ha the files copied to the file system in the rpm itself. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 83d96b1c..6831189d 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -538,7 +538,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou except subprocess.CalledProcessError: pass - new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix) + new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values) missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) @@ -1511,6 +1511,11 @@ def generate_rpm(self, repo, name, image): if self.display: return + installed_files = None + with open(os.path.join(rootfs, "info"), "r") as info_file: + info = json.loads(info_file.read()) + installed_files = info["installed-files"] if "installed-files" in info else None + labels = {k.lower() : v for k, v in image_inspect.get('Labels', {})} summary = labels.get('summary', name) version = labels.get("version", "1.0") @@ -1525,7 +1530,7 @@ def generate_rpm(self, repo, name, image): self._generate_spec_file(spec_file, rpm_content, name, summary, license_, version=version, release=release, url=url, source0=source0, requires=requires, provides=provides, conflicts=conflicts, - description=description) + description=description, installed_files=installed_files) cwd = os.getcwd() cmd = ["rpmbuild", "--noclean", "-bb", spec_file, @@ -1544,7 +1549,7 @@ def generate_rpm(self, repo, name, image): shutil.rmtree(temp_dir) def _generate_spec_file(self, out, destdir, name, summary, license_, version="1.0", release="1", url=None, - source0=None, requires=None, conflicts=None, provides=None, description=None): + source0=None, requires=None, conflicts=None, provides=None, description=None, installed_files=None): spec = "%global __requires_exclude_from ^.*$\n" spec = spec + "%global __provides_exclude_from ^.*$\n" @@ -1572,6 +1577,9 @@ def _generate_spec_file(self, out, destdir, name, summary, license_, version="1. for root, _, files in os.walk(os.path.join(destdir, "usr/lib/tmpfiles.d")): for f in files: spec = spec + "/usr/lib/tmpfiles.d/%s\n" % f + if installed_files: + for i in installed_files: + spec = spec + "%s\n" % i with open(out, "w") as f: f.write(spec) From 25b18afe29b6a4e4058f87b93d5692621352111a Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 25 Nov 2016 13:03:47 +0100 Subject: [PATCH 05/18] syscontainers: allow copied files to the host to be templates It can be specified in the manifest.json file as: "installedFilesTemplate" : [ "/usr/local/bin/hello-template.sh" ] Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 55 ++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 6831189d..fdd162b3 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -485,6 +485,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou # 2) What the user sets explictly as --set # 3) Values for DESTDIR and NAME manifest_file = os.path.join(exports, "manifest.json") + installed_files_template = [] if os.path.exists(manifest_file): with open(manifest_file, "r") as f: try: @@ -495,6 +496,8 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou for key, val in manifest["defaultValues"].items(): if key not in values: values[key] = val + if "installedFilesTemplate" in manifest: + installed_files_template = manifest["installedFilesTemplate"] if "UUID" not in values: values["UUID"] = str(uuid.uuid4()) @@ -548,16 +551,22 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou image_manifest = json.loads(image_manifest) image_id = SystemContainers._get_image_id_from_manifest(image_manifest) or image_id - 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, - "installed-files": new_installed_files, - "remote" : remote} - info_file.write(json.dumps(info, indent=4)) - info_file.write("\n") + try: + 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, + "installed-files": new_installed_files, + "installed-files-template": installedFilesTemplate, + "remote" : remote} + info_file.write(json.dumps(info, indent=4)) + info_file.write("\n") + except (NameError, AttributeError, OSError) as e: + for i in new_installed_files: + os.remove(os.path.join(prefix or "/", os.path.relpath(i, "/"))) + raise e if os.path.exists(unitfile): with open(unitfile, 'r') as infile: @@ -640,7 +649,7 @@ def version(self, image): return None @staticmethod - def _rm_add_files_to_host(old_installed_files, exports, prefix="/"): + def _rm_add_files_to_host(old_installed_files, exports, prefix="/", files_template=None, values=None): # if any file was installed on the host delete it if old_installed_files: for i in old_installed_files: @@ -652,6 +661,8 @@ def _rm_add_files_to_host(old_installed_files, exports, prefix="/"): if not exports: return [] + templates_set = set(files_template or []) + # if there is a directory hostfs/ under exports, copy these files to the host file system. hostfs = os.path.join(exports, "hostfs") new_installed_files = [] @@ -661,9 +672,22 @@ def _rm_add_files_to_host(old_installed_files, exports, prefix="/"): if not os.path.exists(os.path.join(prefix, rel_root_path)): os.makedirs(os.path.join(prefix, rel_root_path)) for f in files: - path = os.path.join(prefix, rel_root_path, f) - shutil.copy2(os.path.join(root, f), path) - new_installed_files.append(os.path.join("/", rel_root_path, f)) + src_file = os.path.join(root, f) + dest_path = os.path.join(prefix, rel_root_path, f) + rel_dest_path = os.path.join("/", rel_root_path, f) + if os.path.exists(dest_path): + for i in new_installed_files: + os.remove(new_installed_files) + raise ValueError("File %s already exists." % dest_path) + + if rel_dest_path in templates_set: + with open(src_file, 'r') as src_file_obj: + data = src_file_obj.read() + SystemContainers._write_template(src_file, data, values or {}, dest_path) + shutil.copystat(src_file, dest_path) + else: + shutil.copy2(src_file, dest_path) + new_installed_files.append(rel_dest_path) new_installed_files.sort() # just for an aesthetic reason in the info file output return new_installed_files @@ -731,6 +755,7 @@ def rollback(self, name): with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: info = json.loads(info_file.read()) installed_files = info["installed-files"] if "installed-files" in info else None + installed_files_template = info["installed-files-template"] if "installed-files-template" in info else None was_service_active = self._is_service_active(name) unitfileout, tmpfilesout = self._get_systemd_destination_files(name) @@ -759,7 +784,7 @@ def rollback(self, name): if (os.path.exists(tmpfiles)): shutil.copyfile(tmpfiles, tmpfilesout) - self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports")) + self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports"), files_template=installed_files_template) os.unlink(path) os.symlink(destination, path) From 7112585ec0c51df44bacd3d0f5aff24148875870 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Fri, 25 Nov 2016 13:41:07 +0100 Subject: [PATCH 06/18] syscontainers: support no-container-service mode with the rpm generation, it can be useful to copy files to the host without the overhead of maintaining a dummy service. Containers that do not use a service, can specify it in the manifest file as: "noContainerService": true it also implies that the rootfs is deleted once all the files are copied to the system. Signed-off-by: Giuseppe Scrivano --- Atomic/containers.py | 4 +-- Atomic/syscontainers.py | 62 ++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/Atomic/containers.py b/Atomic/containers.py index f2d93c72..aaa59365 100644 --- a/Atomic/containers.py +++ b/Atomic/containers.py @@ -151,7 +151,7 @@ def ps_tty(self): max_container_id = 12 if self.args.truncate else max([len(x.id) for x in container_objects]) max_image_name = 20 if self.args.truncate else max([len(x.image_name) for x in container_objects]) max_command = 20 if self.args.truncate else max([len(x.command) for x in container_objects]) - col_out = "{0:2} {1:%s} {2:%s} {3:%s} {4:16} {5:9} {6:10} {7:10}" % (max_container_id, max_image_name, max_command) + col_out = "{0:2} {1:%s} {2:%s} {3:%s} {4:16} {5:10} {6:10} {7:10}" % (max_container_id, max_image_name, max_command) if self.args.heading: util.write_out(col_out.format(" ", "CONTAINER ID", @@ -173,7 +173,7 @@ def ps_tty(self): con_obj.image_name[0:max_image_name], con_obj.command[0:max_command], con_obj.created[0:16], - con_obj.state[0:9], + con_obj.state[0:10], con_obj.backend.backend[0:10], con_obj.runtime[0:10])) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index fdd162b3..5c3f0e12 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -486,6 +486,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou # 3) Values for DESTDIR and NAME manifest_file = os.path.join(exports, "manifest.json") installed_files_template = [] + has_container_service = True if os.path.exists(manifest_file): with open(manifest_file, "r") as f: try: @@ -498,6 +499,8 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou values[key] = val if "installedFilesTemplate" in manifest: installed_files_template = manifest["installedFilesTemplate"] + if "noContainerService" in manifest and manifest["noContainerService"]: + has_container_service = False if "UUID" not in values: values["UUID"] = str(uuid.uuid4()) @@ -532,7 +535,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou # When upgrading, stop the service and remove previously installed # tmpfiles, before restarting the service. - if upgrade: + if has_container_service and upgrade: if was_service_active: self._systemctl_command("stop", name) if os.path.exists(tmpfilesout): @@ -558,6 +561,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou "ostree-commit": rev, 'created' : calendar.timegm(time.gmtime()), "values" : values, + "has-container-service" : has_container_service, "installed-files": new_installed_files, "installed-files-template": installedFilesTemplate, "remote" : remote} @@ -580,12 +584,26 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou else: tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths, values["STATE_DIRECTORY"]) - self._write_template(unitfile, systemd_template, values, unitfileout) - shutil.copyfile(unitfileout, os.path.join(prefix, destination, "%s.service" % name)) + if has_container_service: + self._write_template(unitfile, systemd_template, values, unitfileout) + shutil.copyfile(unitfileout, os.path.join(prefix, destination, "%s.service" % name)) if (tmpfiles_template): self._write_template(unitfile, tmpfiles_template, values, tmpfilesout) shutil.copyfile(tmpfilesout, os.path.join(prefix, destination, "tmpfiles-%s.conf" % name)) + if not prefix: + sym = "%s/%s" % (self._get_system_checkout_path(), name) + if os.path.exists(sym): + os.unlink(sym) + os.symlink(destination, sym) + + # if there is no container service, delete the checked out files. At this point files copied to the host + # are already handled. + if not has_container_service: + if not remote_path: + shutil.rmtree(os.path.join(destination, "rootfs")) + return + if prefix: return @@ -756,8 +774,9 @@ def rollback(self, name): info = json.loads(info_file.read()) installed_files = info["installed-files"] if "installed-files" in info else None installed_files_template = info["installed-files-template"] if "installed-files-template" in info else None + has_container_service = info["has-container-service"] if "has-container-service" in info else True - was_service_active = self._is_service_active(name) + was_service_active = has_container_service and self._is_service_active(name) unitfileout, tmpfilesout = self._get_systemd_destination_files(name) unitfile = os.path.join(destination, "%s.service" % name) tmpfiles = os.path.join(destination, "tmpfiles-%s.conf" % name) @@ -767,6 +786,7 @@ def rollback(self, name): "The previous checkout at %s may be corrupted." % destination) util.write_out("Rolling back container {} to the checkout at {}".format(name, destination)) + if was_service_active: self._systemctl_command("stop", name) @@ -788,7 +808,8 @@ def rollback(self, name): os.unlink(path) os.symlink(destination, path) - self._systemctl_command("daemon-reload") + if has_container_service: + self._systemctl_command("daemon-reload") if (os.path.exists(tmpfiles)): self._systemd_tmpfiles("--create", tmpfilesout) @@ -797,6 +818,16 @@ def rollback(self, name): def get_container_runtime_info(self, container): + info_path = os.path.join(self._get_system_checkout_path(), container, "info") + if not os.path.exists(info_path): + info_path = os.path.join(self._get_preinstalled_containers_path(), container, "info") + + with open(info_path, "r") as info_file: + info = json.loads(info_file.read()) + has_container_service = info["has-container-service"] if "has-container-service" in info else True + + if not has_container_service: + return {'status' : "no service"} if self._is_service_active(container): return {'status' : "running"} elif self._is_service_failed(container): @@ -1059,15 +1090,20 @@ def uninstall(self, name): if self._is_preinstalled_container(name): raise ValueError("Cannot uninstall a preinstalled container") + with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: + info = json.loads(info_file.read()) + has_container_service = info["has-container-service"] if "has-container-service" in info else True + 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 has_container_service: + 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: From 7ee09a4890a2f2d3dc2eb692fe667f57e48d3e24 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 28 Nov 2016 12:54:32 +0100 Subject: [PATCH 07/18] syscontainers: allow to specify the copied file name allow to rename a copied file to host using the variable substitution. In the manifest.json file it is permitted to specify the destination file name of a file copied to the host as: "renameFiles" : { "/usr/local/bin/hello-template.sh" : "/usr/local/bin/hello-template-$NAME.sh" }, Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 5c3f0e12..211f3cca 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -487,6 +487,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou manifest_file = os.path.join(exports, "manifest.json") installed_files_template = [] has_container_service = True + rename_files = {} if os.path.exists(manifest_file): with open(manifest_file, "r") as f: try: @@ -499,6 +500,8 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou values[key] = val if "installedFilesTemplate" in manifest: installed_files_template = manifest["installedFilesTemplate"] + if "renameFiles" in manifest: + rename_files = manifest["renameFiles"] if "noContainerService" in manifest and manifest["noContainerService"]: has_container_service = False @@ -544,7 +547,18 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou except subprocess.CalledProcessError: pass - new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values) + # rename_files may contain variables that need to be replaced. + if rename_files: + for k, v in rename_files.items(): + template = Template(v) + try: + new_v = template.substitute(values) + except KeyError as e: + raise ValueError("The template file 'manifest.json' still contains an unreplaced value for: '%s'" % \ + (str(e))) + rename_files[k] = new_v + + new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values, rename_files=rename_files) missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) @@ -563,7 +577,8 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou "values" : values, "has-container-service" : has_container_service, "installed-files": new_installed_files, - "installed-files-template": installedFilesTemplate, + "installed-files-template": installed_files_template, + "rename-installed-files" : rename_files, "remote" : remote} info_file.write(json.dumps(info, indent=4)) info_file.write("\n") @@ -667,7 +682,7 @@ def version(self, image): return None @staticmethod - def _rm_add_files_to_host(old_installed_files, exports, prefix="/", files_template=None, values=None): + def _rm_add_files_to_host(old_installed_files, exports, prefix="/", files_template=None, values=None, rename_files=None): # if any file was installed on the host delete it if old_installed_files: for i in old_installed_files: @@ -693,6 +708,12 @@ def _rm_add_files_to_host(old_installed_files, exports, prefix="/", files_templa src_file = os.path.join(root, f) dest_path = os.path.join(prefix, rel_root_path, f) rel_dest_path = os.path.join("/", rel_root_path, f) + + # If rename_files is set, rename the destination file + if rename_files and rel_dest_path in rename_files: + rel_dest_path = rename_files.get(rel_dest_path) + dest_path = os.path.join(prefix or "/", os.path.relpath(rel_dest_path, "/")) + if os.path.exists(dest_path): for i in new_installed_files: os.remove(new_installed_files) @@ -705,6 +726,7 @@ def _rm_add_files_to_host(old_installed_files, exports, prefix="/", files_templa shutil.copystat(src_file, dest_path) else: shutil.copy2(src_file, dest_path) + new_installed_files.append(rel_dest_path) new_installed_files.sort() # just for an aesthetic reason in the info file output @@ -770,11 +792,13 @@ def rollback(self, name): raise ValueError("Error: Cannot find a previous deployment to rollback located at %s" % destination) installed_files = None + rename_files = None with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: info = json.loads(info_file.read()) installed_files = info["installed-files"] if "installed-files" in info else None installed_files_template = info["installed-files-template"] if "installed-files-template" in info else None has_container_service = info["has-container-service"] if "has-container-service" in info else True + rename_files = info["rename-installed-files"] if "rename-installed-files" in info else None was_service_active = has_container_service and self._is_service_active(name) unitfileout, tmpfilesout = self._get_systemd_destination_files(name) @@ -804,7 +828,7 @@ def rollback(self, name): if (os.path.exists(tmpfiles)): shutil.copyfile(tmpfiles, tmpfilesout) - self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports"), files_template=installed_files_template) + self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports"), files_template=installed_files_template, rename_files=rename_files) os.unlink(path) os.symlink(destination, path) From f0fc0718dce69bd65466b6f077698ffa2e699d97 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 13 Dec 2016 13:47:15 +0100 Subject: [PATCH 08/18] syscontainers: accept rpm.spec or rpm.spec.template under /exports Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 65 +++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 211f3cca..de1896ae 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -367,22 +367,27 @@ def get_image(i): @staticmethod 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) + + if destination: try: - result = template.substitute(values) - except KeyError as e: - os.unlink(destination) - raise ValueError("The template file '%s' still contains an unreplaced value for: '%s'" % \ - (inputfilename, str(e))) - outfile.write(result) + os.makedirs(os.path.dirname(destination)) + except OSError: + pass + + template = Template(data) + try: + result = template.substitute(values) + except KeyError as e: + raise ValueError("The template file '%s' still contains an unreplaced value for: '%s'" % \ + (inputfilename, str(e))) + + if destination is not None: + with open(destination, "w") as outfile: + outfile.write(result) + return result def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None, installed_files=None): - if not values: + if values is None: values = {} remote_path = self._resolve_remote_path(remote) @@ -568,6 +573,14 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou image_manifest = json.loads(image_manifest) image_id = SystemContainers._get_image_id_from_manifest(image_manifest) or image_id + # If rpm.spec or rpm.spec.template exist, copy them to the checkout directory, processing the .template version. + if os.path.exists(os.path.join(exports, "rpm.spec.template")): + with open(os.path.join(exports, "rpm.spec.template"), "r") as f: + spec_content = f.read() + SystemContainers._write_template("rpm.spec.template", spec_content, values, os.path.join(destination, "rpm.spec")) + elif os.path.exists(os.path.join(rootfs, "rpm.spec")): + shutil.copyfile(os.path.join(rootfs, "rpm.spec"), os.path.join(destination, "rpm.spec")) + try: with open(os.path.join(destination, "info"), 'w') as info_file: info = {"image" : img, @@ -600,10 +613,10 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths, values["STATE_DIRECTORY"]) if has_container_service: - self._write_template(unitfile, systemd_template, values, unitfileout) + SystemContainers._write_template(unitfile, systemd_template, values, unitfileout) shutil.copyfile(unitfileout, os.path.join(prefix, destination, "%s.service" % name)) if (tmpfiles_template): - self._write_template(unitfile, tmpfiles_template, values, tmpfilesout) + SystemContainers._write_template(unitfile, tmpfiles_template, values, tmpfilesout) shutil.copyfile(tmpfilesout, os.path.join(prefix, destination, "tmpfiles-%s.conf" % name)) if not prefix: @@ -1613,9 +1626,17 @@ def generate_rpm(self, repo, name, image): conflicts = labels.get("conflicts") description = labels.get("description") - self._generate_spec_file(spec_file, rpm_content, name, summary, license_, version=version, release=release, - url=url, source0=source0, requires=requires, provides=provides, conflicts=conflicts, - description=description, installed_files=installed_files) + if os.path.exists(os.path.join(rootfs, "rpm.spec")): + with open(os.path.join(rootfs, "rpm.spec"), "r") as f: + spec_content = f.read() + else: + spec_content = self._generate_spec_file(rpm_content, name, summary, license_, version=version, + release=release, url=url, source0=source0, requires=requires, + provides=provides, conflicts=conflicts, description=description, + installed_files=installed_files) + + with open(spec_file, "w") as f: + f.write(spec_content) cwd = os.getcwd() cmd = ["rpmbuild", "--noclean", "-bb", spec_file, @@ -1633,8 +1654,9 @@ def generate_rpm(self, repo, name, image): finally: shutil.rmtree(temp_dir) - def _generate_spec_file(self, out, destdir, name, summary, license_, version="1.0", release="1", url=None, - source0=None, requires=None, conflicts=None, provides=None, description=None, installed_files=None): + def _generate_spec_file(self, destdir, name, summary, license_, version="1.0", release="1", url=None, + source0=None, requires=None, conflicts=None, provides=None, description=None, + installed_files=None): spec = "%global __requires_exclude_from ^.*$\n" spec = spec + "%global __provides_exclude_from ^.*$\n" @@ -1666,5 +1688,4 @@ def _generate_spec_file(self, out, destdir, name, summary, license_, version="1. for i in installed_files: spec = spec + "%s\n" % i - with open(out, "w") as f: - f.write(spec) + return spec From d0d64dd796c1505f27d8e82a1f6fed9f06c0ac64 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 6 Mar 2017 12:46:17 +0100 Subject: [PATCH 09/18] install: add --system-package Drop the hidden option ---generate-rpm in favor of --system-package. The new option controls how a system container is installed to the host: --generate-rpm=build build the rpm file without installing it. --generate-rpm=yes build the rpm and install it, the rpm is deleted. --generate-rpm=no do not attempt to build and install an rpm file. --generate-rpm=auto install the rpm only if a .spec file is defined in the image. This is the default. Signed-off-by: Giuseppe Scrivano --- Atomic/install.py | 5 ++- Atomic/syscontainers.py | 67 +++++++++++++++++++++++++++++++++------- bash/atomic | 1 + docs/atomic-install.1.md | 13 ++++++++ 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/Atomic/install.py b/Atomic/install.py index 8cd8b13b..855f1370 100644 --- a/Atomic/install.py +++ b/Atomic/install.py @@ -62,9 +62,8 @@ def cli(subparser): system_xor_user.add_argument("--system", dest="system", action='store_true', default=False, help=_('install a system container')) - installp.add_argument("---generate-rpm", dest="generate_rpm", - action='store_true', default=False, - help=_('generate an rpm instead of installing the container')) + installp.add_argument("--system-package", dest="system_package", default="auto", + help=_('control how to install the package. It accepts `auto`, `yes`, `no`, `build`')) installp.add_argument("--rootfs", dest="remote", help=_("choose an existing exploded container/image to use " "its rootfs as a remote, read-only rootfs for the " diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index de1896ae..7fed6dd6 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -181,6 +181,14 @@ def install_user_container(self, image, name): # Same entrypoint return self.install(image, name) + def _install_rpm(self, rpm_file): + if os.path.exists("/run/ostree-booted"): + raise ValueError("This doesn't work on Atomic Host yet") + elif os.path.exists("/usr/bin/dnf"): + util.check_call(["dnf", "install", "-y", rpm_file]) + else: + util.check_call(["yum", "install", "-y", rpm_file]) + def install(self, image, name): repo = self._get_ostree_repo() if not repo: @@ -189,17 +197,42 @@ def install(self, image, name): if self.args.system and self.user: raise ValueError("Only root can use --system") - if self.args.generate_rpm: - if not self.args.system: - raise ValueError("Only --system can generate rpms") - return self.generate_rpm(repo, name, image) - - image = self._pull_image_to_ostree(repo, image, False) + accepted_system_package_values = ['auto', 'build', 'no', 'yes'] + if self.args.system_package not in accepted_system_package_values: + raise ValueError("Invalid --system-package mode. Accepted values: '%s'" % "', '".join(accepted_system_package_values)) if self.get_checkout(name): util.write_out("%s already present" % (name)) return + image = self._pull_image_to_ostree(repo, image, False) + + if self.args.system_package in ['build', 'yes', 'auto']: + if not self.args.system: + raise ValueError("Only --system can generate rpms") + + rpm_file = None + tmp_dir = self.generate_rpm(repo, self.args.system_package == 'auto', name, image) + if tmp_dir: + try: + for root, _, files in os.walk(os.path.join(tmp_dir, "build")): + if rpm_file: + break + for f in files: + if f.endswith('.rpm'): + rpm_file = os.path.join(root, f) + break + # If we are only build'ing the rpm, copy it to the cwd and exit + if self.args.system_package == 'build': + destination = os.path.join(os.getcwd(), os.path.basename(rpm_file)) + shutil.move(rpm_file, destination) + util.write_out("Generated rpm %s" % destination) + else: + self._install_rpm(rpm_file) + finally: + shutil.rmtree(tmp_dir) + return False + values = {} if self.args.setvalues is not None: setvalues = SystemContainers._split_set_args(self.args.setvalues) @@ -797,6 +830,7 @@ def update_container(self, name, setvalues=None, rebase=None): return self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote, installed_files=installed_files) + return def rollback(self, name): path = os.path.join(self._get_system_checkout_path(), name) @@ -1596,25 +1630,26 @@ def get_out_checksum(obj): return obj.out_checksum if hasattr(obj, 'out_checksum traverse(it) return ret - def generate_rpm(self, repo, name, image): + def generate_rpm(self, repo, auto, name, image): image_inspect = self.inspect_system_image(image) temp_dir = tempfile.mkdtemp() rpm_content = os.path.join(temp_dir, "rpmroot") rootfs = os.path.join(rpm_content, "usr/lib/containers/atomic", name) os.makedirs(rootfs) + success = False try: spec_file = os.path.join(temp_dir, "container.spec") self._checkout(repo, name, image, 0, False, destination=rootfs, prefix=rpm_content) if self.display: - return + return None installed_files = None with open(os.path.join(rootfs, "info"), "r") as info_file: info = json.loads(info_file.read()) installed_files = info["installed-files"] if "installed-files" in info else None - labels = {k.lower() : v for k, v in image_inspect.get('Labels', {})} + labels = {k.lower() : v for k, v in image_inspect.get('Labels', {}).items()} summary = labels.get('summary', name) version = labels.get("version", "1.0") release = labels.get("release", "1.0") @@ -1630,6 +1665,9 @@ def generate_rpm(self, repo, name, image): with open(os.path.join(rootfs, "rpm.spec"), "r") as f: spec_content = f.read() else: + # If there is no spec file and 'auto' is used, do not install an rpm + if auto: + return None spec_content = self._generate_spec_file(rpm_content, name, summary, license_, version=version, release=release, url=url, source0=source0, requires=requires, provides=provides, conflicts=conflicts, description=description, @@ -1639,20 +1677,25 @@ def generate_rpm(self, repo, name, image): f.write(spec_content) cwd = os.getcwd() + result_dir = os.path.join(temp_dir, "build") + if not os.path.exists(result_dir): + os.makedirs(result_dir) cmd = ["rpmbuild", "--noclean", "-bb", spec_file, "--define", "_sourcedir %s" % temp_dir, "--define", "_specdir %s" % temp_dir, "--define", "_builddir %s" % temp_dir, "--define", "_srcrpmdir %s" % cwd, - "--define", "_rpmdir %s" % cwd, + "--define", "_rpmdir %s" % result_dir, "--build-in-place", "--buildroot=%s" % rpm_content] util.write_out(" ".join(cmd)) if not self.display: util.check_call(cmd) - return False + success = True + return temp_dir finally: - shutil.rmtree(temp_dir) + if not success: + shutil.rmtree(temp_dir) def _generate_spec_file(self, destdir, name, summary, license_, version="1.0", release="1", url=None, source0=None, requires=None, conflicts=None, provides=None, description=None, diff --git a/bash/atomic b/bash/atomic index e6e7a583..ec10780a 100644 --- a/bash/atomic +++ b/bash/atomic @@ -718,6 +718,7 @@ _atomic_install() { --rootfs --storage --system + --system-package --set --user " diff --git a/docs/atomic-install.1.md b/docs/atomic-install.1.md index 15abc330..f8000b9a 100644 --- a/docs/atomic-install.1.md +++ b/docs/atomic-install.1.md @@ -12,6 +12,7 @@ atomic-install - Execute Image Install Method [**--rootfs**=*ROOTFS*] [**--set**=*NAME*=*VALUE*] [**--storage**] +[**--system-package=auto|build|yes|no**] [**--system**] IMAGE [ARG...] @@ -96,6 +97,18 @@ Note: If the image being pulled contains a label of `system.type=ostree`, atomic will automatically substitute the storage backend to be ostree. This can be overridden with the --storage option. +**--system-package=auto|build|no|yes** +Control how the container will be installed to the system. + +*auto* generates an rpm and install it to the system when the +image defines a .spec file. This is the default. + +*build* build only the software package, without installing it. + +*no* do not generate an rpm package to install the container. + +*yes* generate an rpm package and install it to the system. + **--user** If running as non-root, specify to install the image from the current OSTree repository and manage it through systemd and bubblewrap. From eadda0a6fe52b8501418e8fe64a7e278b8e3c457 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 6 Mar 2017 12:52:32 +0100 Subject: [PATCH 10/18] syscontainer: support rpm embedded in the image If an rpm file is already present the the container image. just use it. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 7fed6dd6..3dcd817a 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -1644,6 +1644,10 @@ def generate_rpm(self, repo, auto, name, image): if self.display: return None + included_rpm = os.path.join(rootfs, "rootfs", "exports", "container.rpm") + if os.path.exists(included_rpm): + return included_rpm + installed_files = None with open(os.path.join(rootfs, "info"), "r") as info_file: info = json.loads(info_file.read()) From 4a0f8491493754c4d8e0a7352a484ec86b3f150b Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 7 Mar 2017 12:27:07 +0100 Subject: [PATCH 11/18] syscontainers: add new variable IMAGE_ID it stores the image id used to install the container. Use it in the generated spec file to refer to the image ID. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 3dcd817a..0418ceaa 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -59,7 +59,7 @@ WantedBy=multi-user.target """ TEMPLATE_FORCED_VARIABLES = ["DESTDIR", "NAME", "EXEC_START", "EXEC_STOP", - "HOST_UID", "HOST_GID"] + "HOST_UID", "HOST_GID", "IMAGE_ID"] TEMPLATE_OVERRIDABLE_VARIABLES = ["RUN_DIRECTORY", "STATE_DIRECTORY", "UUID"] class SystemContainers(object): @@ -453,7 +453,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou was_service_active = self._is_service_active(name) if self.display: - return + return values if self.user: rootfs = os.path.join(destination, "rootfs") @@ -504,7 +504,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou os.close(rootfs_fd) if extract_only: - return + return values if self.user: values["RUN_DIRECTORY"] = os.environ.get("XDG_RUNTIME_DIR", "/run/user/%s" % (os.getuid())) @@ -543,6 +543,12 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou if "noContainerService" in manifest and manifest["noContainerService"]: has_container_service = False + image_manifest = self._image_manifest(repo, rev) + image_id = rev + if image_manifest: + image_manifest = json.loads(image_manifest) + image_id = SystemContainers._get_image_id_from_manifest(image_manifest) or image_id + if "UUID" not in values: values["UUID"] = str(uuid.uuid4()) values["DESTDIR"] = os.path.join("/", os.path.relpath(destination, prefix)) if prefix else destination @@ -550,6 +556,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou values["EXEC_START"], values["EXEC_STOP"] = self._generate_systemd_startstop_directives(name) values["HOST_UID"] = os.getuid() values["HOST_GID"] = os.getgid() + values["IMAGE_ID"] = image_id src = os.path.join(exports, "config.json") destination_path = os.path.join(destination, "config.json") @@ -600,12 +607,6 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) - image_manifest = self._image_manifest(repo, rev) - image_id = rev - if image_manifest: - image_manifest = json.loads(image_manifest) - image_id = SystemContainers._get_image_id_from_manifest(image_manifest) or image_id - # If rpm.spec or rpm.spec.template exist, copy them to the checkout directory, processing the .template version. if os.path.exists(os.path.join(exports, "rpm.spec.template")): with open(os.path.join(exports, "rpm.spec.template"), "r") as f: @@ -663,10 +664,10 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou if not has_container_service: if not remote_path: shutil.rmtree(os.path.join(destination, "rootfs")) - return + return values if prefix: - return + return values sym = "%s/%s" % (self._get_system_checkout_path(), name) if os.path.exists(sym): @@ -682,6 +683,8 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou elif was_service_active: self._systemctl_command("start", name) + return values + def _get_preinstalled_containers_path(self): return ATOMIC_USR @@ -1497,7 +1500,7 @@ 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) + self._checkout(repo, img, img, 0, False, destination=destination, extract_only=True) @staticmethod def _encode_to_ostree_ref(name): @@ -1639,7 +1642,7 @@ def generate_rpm(self, repo, auto, name, image): success = False try: spec_file = os.path.join(temp_dir, "container.spec") - self._checkout(repo, name, image, 0, False, destination=rootfs, prefix=rpm_content) + values = self._checkout(repo, name, image, 0, False, destination=rootfs, prefix=rpm_content) if self.display: return None @@ -1665,6 +1668,8 @@ def generate_rpm(self, repo, auto, name, image): conflicts = labels.get("conflicts") description = labels.get("description") + image_id = values["IMAGE_ID"] + if os.path.exists(os.path.join(rootfs, "rpm.spec")): with open(os.path.join(rootfs, "rpm.spec"), "r") as f: spec_content = f.read() @@ -1672,7 +1677,7 @@ def generate_rpm(self, repo, auto, name, image): # If there is no spec file and 'auto' is used, do not install an rpm if auto: return None - spec_content = self._generate_spec_file(rpm_content, name, summary, license_, version=version, + spec_content = self._generate_spec_file(rpm_content, name, summary, license_, image_id, version=version, release=release, url=url, source0=source0, requires=requires, provides=provides, conflicts=conflicts, description=description, installed_files=installed_files) @@ -1701,7 +1706,7 @@ def generate_rpm(self, repo, auto, name, image): if not success: shutil.rmtree(temp_dir) - def _generate_spec_file(self, destdir, name, summary, license_, version="1.0", release="1", url=None, + def _generate_spec_file(self, destdir, name, summary, license_, image_id, version="1.0", release="1", url=None, source0=None, requires=None, conflicts=None, provides=None, description=None, installed_files=None): spec = "%global __requires_exclude_from ^.*$\n" @@ -1714,7 +1719,7 @@ def _generate_spec_file(self, destdir, name, summary, license_, version="1.0", r if v is not None: spec = spec + "%s:\t%s\n" % (k, v) - spec = spec + "\n%description\n" + spec = spec + ("\n%%description\nImage ID: %s\n" % image_id) if description: spec = spec + "%s\n" % description From 2a71369d8fe450cf53b0af899f5f1008c016cd0f Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 7 Mar 2017 12:28:53 +0100 Subject: [PATCH 12/18] syscontainers: add new variable IMAGE_NAME it contains the name of the image used to install the container Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 0418ceaa..06328254 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -59,7 +59,7 @@ WantedBy=multi-user.target """ TEMPLATE_FORCED_VARIABLES = ["DESTDIR", "NAME", "EXEC_START", "EXEC_STOP", - "HOST_UID", "HOST_GID", "IMAGE_ID"] + "HOST_UID", "HOST_GID", "IMAGE_ID", "IMAGE_NAME"] TEMPLATE_OVERRIDABLE_VARIABLES = ["RUN_DIRECTORY", "STATE_DIRECTORY", "UUID"] class SystemContainers(object): @@ -239,7 +239,7 @@ def install(self, image, name): for k, v in setvalues.items(): values[k] = v - return self._checkout(repo, name, image, 0, False, values=values, remote=self.args.remote) + self._checkout(repo, name, image, 0, False, values=values, remote=self.args.remote) def _check_oci_configuration_file(self, conf_path, remote=None, include_all=False): with open(conf_path, 'r') as conf: @@ -556,6 +556,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou values["EXEC_START"], values["EXEC_STOP"] = self._generate_systemd_startstop_directives(name) values["HOST_UID"] = os.getuid() values["HOST_GID"] = os.getgid() + values["IMAGE_NAME"] = img values["IMAGE_ID"] = image_id src = os.path.join(exports, "config.json") From 6e9d0cd8ce0544b16253665b37d2dd4f75f7e079 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 7 Mar 2017 12:39:48 +0100 Subject: [PATCH 13/18] syscontainers: support uninstall of rpm installed containers Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 06328254..081cc21e 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -189,6 +189,14 @@ def _install_rpm(self, rpm_file): else: util.check_call(["yum", "install", "-y", rpm_file]) + def _uninstall_rpm(self, rpm): + if os.path.exists("/run/ostree-booted"): + raise ValueError("This doesn't work on Atomic Host yet") + elif os.path.exists("/usr/bin/dnf"): + util.check_call(["dnf", "remove", "-y", rpm]) + else: + util.check_call(["yum", "remove", "-y", rpm]) + def install(self, image, name): repo = self._get_ostree_repo() if not repo: @@ -1163,7 +1171,8 @@ def _is_preinstalled_container(self, name): def uninstall(self, name): if self._is_preinstalled_container(name): - raise ValueError("Cannot uninstall a preinstalled container") + self._uninstall_rpm("%s-%s" % (RPM_NAME_PREFIX, name)) + return with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: info = json.loads(info_file.read()) From 7864311e2ea841ca446eb8f5e747d0fd5f4be291 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 15 Mar 2017 10:20:07 +0100 Subject: [PATCH 14/18] syscontainers: do not delete files tracked via tmpfiles by default we have another mechanism to do this now. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 081cc21e..65db4948 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -653,7 +653,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou with open(tmpfiles, 'r') as infile: tmpfiles_template = infile.read() else: - tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths, values["STATE_DIRECTORY"]) + tmpfiles_template = SystemContainers._generate_tmpfiles_data(missing_bind_paths) if has_container_service: SystemContainers._write_template(unitfile, systemd_template, values, unitfileout) @@ -1486,16 +1486,12 @@ def _check_system_oci_image(self, repo, img, upgrade): return True @staticmethod - def _generate_tmpfiles_data(missing_bind_paths, state_directory): + def _generate_tmpfiles_data(missing_bind_paths): 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")) + lines.append(_generate_line(x, "d")) return "".join(lines) @staticmethod From 51e55dc355d2a4ce5693a53deed9df90b72c081c Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 20 Mar 2017 20:43:07 +0100 Subject: [PATCH 15/18] syscontainers: change $ORIGIN: prefix allow the $ORIGIN: prefix only when the branch is specified, otherwise it is not decidable if $ORIGIN is the source to pull the image from or the name of the image itself. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 8 ++++---- tests/integration/test_system_containers.sh | 4 ++-- tests/unit/test_pull.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 65db4948..bb1529f6 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -155,12 +155,12 @@ def _split_set_args(setvalues): def _pull_image_to_ostree(self, repo, image, upgrade): if not repo: raise ValueError("Cannot find a configured OSTree repo") - if image.startswith("ostree:"): + if image.startswith("ostree:") and image.count(':') > 1: self._check_system_ostree_image(repo, image, upgrade) - elif image.startswith("docker:"): + elif image.startswith("docker:") and image.count(':') > 1: image = self._pull_docker_image(repo, image.replace("docker:", "", 1)) - elif image.startswith("dockertar:"): - tarpath = image.replace("dockertar:", "", 1) + elif image.startswith("dockertar:/"): + tarpath = image.replace("dockertar:/", "", 1) image = self._pull_docker_tar(repo, tarpath, os.path.basename(tarpath).replace(".tar", "")) else: # Assume "oci:" self._check_system_oci_image(repo, image, upgrade) diff --git a/tests/integration/test_system_containers.sh b/tests/integration/test_system_containers.sh index c519715e..7eac82df 100755 --- a/tests/integration/test_system_containers.sh +++ b/tests/integration/test_system_containers.sh @@ -255,7 +255,7 @@ test \! -e ${ATOMIC_OSTREE_CHECKOUT_PATH}/${NAME} test \! -e ${ATOMIC_OSTREE_CHECKOUT_PATH}/${NAME}.0 test \! -e ${ATOMIC_OSTREE_CHECKOUT_PATH}/${NAME}.1 -${ATOMIC} pull --storage ostree docker:atomic-test-secret +${ATOMIC} pull --storage ostree docker:atomic-test-secret:latest # Move directly the OSTree reference to a new one, so that we have different names and info doesn't error out mv ${ATOMIC_OSTREE_REPO}/refs/heads/ociimage/atomic-test-secret_3Alatest ${ATOMIC_OSTREE_REPO}/refs/heads/ociimage/atomic-test-secret-ostree_3Alatest ${ATOMIC} info atomic-test-secret-ostree > version.out @@ -332,7 +332,7 @@ teardown # Install from a docker local docker image export NAME="test-docker-system-container-$$" -${ATOMIC} install --name=${NAME} --set=RECEIVER=${SECRET} --system docker:atomic-test-system +${ATOMIC} install --name=${NAME} --set=RECEIVER=${SECRET} --system docker:atomic-test-system:latest test -e /etc/tmpfiles.d/${NAME}.conf test -e ${ATOMIC_OSTREE_CHECKOUT_PATH}/${NAME}.0/${NAME}.service diff --git a/tests/unit/test_pull.py b/tests/unit/test_pull.py index 541d4d07..cc5dd931 100644 --- a/tests/unit/test_pull.py +++ b/tests/unit/test_pull.py @@ -20,7 +20,7 @@ class TestAtomicPull(unittest.TestCase): class Args(): def __init__(self): - self.image = "docker:centos" + self.image = "docker:centos:latest" self.user = False def test_pull_as_privileged_user(self): From 5cf995c2576eb1cae2b0be985db7f1c9d8ade40c Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 21 Mar 2017 15:03:45 +0100 Subject: [PATCH 16/18] syscontainers: with --system-package=yes rpm has not container files when using system-package add to the default generated rpm only the files that are exported to the host not every file that is part of the container. We will still be able to take advantage of the OSTree storage. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 213 +++++++++++++++++++++++----------------- 1 file changed, 124 insertions(+), 89 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index bb1529f6..4ec87ea6 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -214,15 +214,18 @@ def install(self, image, name): return image = self._pull_image_to_ostree(repo, image, False) - - if self.args.system_package in ['build', 'yes', 'auto']: - if not self.args.system: - raise ValueError("Only --system can generate rpms") - - rpm_file = None - tmp_dir = self.generate_rpm(repo, self.args.system_package == 'auto', name, image) - if tmp_dir: - try: + rpm_preinstalled = None + tmp_dir = None + try: + if self.args.system_package in ['build', 'yes', 'auto']: + if not self.args.system: + raise ValueError("Only --system can generate rpms") + + rpm_file = None + auto = self.args.system_package == 'auto' + include_containers_file = self.args.system_package == 'build' + tmp_dir = self.generate_rpm(repo, auto, name, image, include_containers_file=include_containers_file) + if tmp_dir: for root, _, files in os.walk(os.path.join(tmp_dir, "build")): if rpm_file: break @@ -237,17 +240,25 @@ def install(self, image, name): util.write_out("Generated rpm %s" % destination) else: self._install_rpm(rpm_file) - finally: - shutil.rmtree(tmp_dir) - return False + rpm_preinstalled = rpm_file - values = {} - if self.args.setvalues is not None: - setvalues = SystemContainers._split_set_args(self.args.setvalues) - for k, v in setvalues.items(): - values[k] = v + if self.args.system_package == "build": + return False - self._checkout(repo, name, image, 0, False, values=values, remote=self.args.remote) + values = {} + if self.args.setvalues is not None: + setvalues = SystemContainers._split_set_args(self.args.setvalues) + for k, v in setvalues.items(): + values[k] = v + + self._checkout(repo, name, image, 0, False, values=values, remote=self.args.remote, rpm_preinstalled=rpm_preinstalled) + except: + if rpm_preinstalled: + self._uninstall_rpm(os.path.basename(rpm_preinstalled).replace(".rpm", "")) + raise + finally: + if tmp_dir: + shutil.rmtree(tmp_dir) def _check_oci_configuration_file(self, conf_path, remote=None, include_all=False): with open(conf_path, 'r') as conf: @@ -334,7 +345,8 @@ def _resolve_remote_path(self, remote_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, prefix=None, installed_files=None): + def _checkout(self, repo, name, img, deployment, upgrade, values=None, destination=None, extract_only=False, remote=None, prefix=None, installed_files=None, + rpm_preinstalled=None): destination = destination or "%s/%s.%d" % (self._get_system_checkout_path(), name, deployment) unitfileout, tmpfilesout = self._get_systemd_destination_files(name, prefix) @@ -344,7 +356,8 @@ def _checkout(self, repo, name, img, deployment, upgrade, values=None, destinati raise ValueError("The file %s already exists." % f) try: - return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix, installed_files=installed_files) + return self._do_checkout(repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix, installed_files=installed_files, + rpm_preinstalled=rpm_preinstalled) except (ValueError, OSError) as e: try: if not extract_only and not upgrade: @@ -427,7 +440,8 @@ def _write_template(inputfilename, data, values, destination): outfile.write(result) return result - def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None, installed_files=None): + def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileout, tmpfilesout, extract_only, remote, prefix=None, installed_files=None, + rpm_preinstalled=None): if values is None: values = {} @@ -544,9 +558,9 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou for key, val in manifest["defaultValues"].items(): if key not in values: values[key] = val - if "installedFilesTemplate" in manifest: + if rpm_preinstalled is None and "installedFilesTemplate" in manifest: installed_files_template = manifest["installedFilesTemplate"] - if "renameFiles" in manifest: + if rpm_preinstalled is None and "renameFiles" in manifest: rename_files = manifest["renameFiles"] if "noContainerService" in manifest and manifest["noContainerService"]: has_container_service = False @@ -612,7 +626,11 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou (str(e))) rename_files[k] = new_v - new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values, rename_files=rename_files) + if rpm_preinstalled: + new_installed_files = [] + shutil.move(rpm_preinstalled, destination) + else: + new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values, rename_files=rename_files) missing_bind_paths = self._check_oci_configuration_file(destination_path, remote_path, True) @@ -625,6 +643,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou shutil.copyfile(os.path.join(rootfs, "rpm.spec"), os.path.join(destination, "rpm.spec")) try: + rpm_installed = os.path.basename(rpm_preinstalled).replace(".rpm", "") if rpm_preinstalled else None with open(os.path.join(destination, "info"), 'w') as info_file: info = {"image" : img, "revision" : image_id, @@ -635,6 +654,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou "installed-files": new_installed_files, "installed-files-template": installed_files_template, "rename-installed-files" : rename_files, + "rpm-installed" : rpm_installed, "remote" : remote} info_file.write(json.dumps(info, indent=4)) info_file.write("\n") @@ -854,8 +874,9 @@ def rollback(self, name): rename_files = None with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: info = json.loads(info_file.read()) - installed_files = info["installed-files"] if "installed-files" in info else None - installed_files_template = info["installed-files-template"] if "installed-files-template" in info else None + rpm_installed = info["rpm-installed"] if "rpm-installed" in info else True + installed_files = info["installed-files"] if "installed-files" in info and rpm_installed is None else None + installed_files_template = info["installed-files-template"] if "installed-files-template" in info and rpm_installed is None else None has_container_service = info["has-container-service"] if "has-container-service" in info else True rename_files = info["rename-installed-files"] if "rename-installed-files" in info else None @@ -887,7 +908,8 @@ def rollback(self, name): if (os.path.exists(tmpfiles)): shutil.copyfile(tmpfiles, tmpfilesout) - self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports"), files_template=installed_files_template, rename_files=rename_files) + if installed_files: + self._rm_add_files_to_host(installed_files, os.path.join(destination, "rootfs/exports"), files_template=installed_files_template, rename_files=rename_files) os.unlink(path) os.symlink(destination, path) @@ -1172,11 +1194,14 @@ def _is_preinstalled_container(self, name): def uninstall(self, name): if self._is_preinstalled_container(name): self._uninstall_rpm("%s-%s" % (RPM_NAME_PREFIX, name)) + + if not os.path.exists(os.path.join(self._get_system_checkout_path(), name)): return with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: info = json.loads(info_file.read()) has_container_service = info["has-container-service"] if "has-container-service" in info else True + rpm_installed = info["rpm-installed"] if "rpm-installed" in info else True unitfileout, tmpfilesout = self._get_systemd_destination_files(name) if has_container_service: @@ -1213,6 +1238,9 @@ def uninstall(self, name): if os.path.exists(unitfileout): os.unlink(unitfileout) + if rpm_installed: + self._uninstall_rpm(rpm_installed) + def prune_ostree_images(self): repo = self._get_ostree_repo() if not repo: @@ -1639,84 +1667,90 @@ def get_out_checksum(obj): return obj.out_checksum if hasattr(obj, 'out_checksum traverse(it) return ret - def generate_rpm(self, repo, auto, name, image): - image_inspect = self.inspect_system_image(image) + def generate_rpm(self, repo, auto, name, image, include_containers_file=True): temp_dir = tempfile.mkdtemp() rpm_content = os.path.join(temp_dir, "rpmroot") rootfs = os.path.join(rpm_content, "usr/lib/containers/atomic", name) os.makedirs(rootfs) success = False try: - spec_file = os.path.join(temp_dir, "container.spec") values = self._checkout(repo, name, image, 0, False, destination=rootfs, prefix=rpm_content) - if self.display: return None - - included_rpm = os.path.join(rootfs, "rootfs", "exports", "container.rpm") - if os.path.exists(included_rpm): - return included_rpm - - installed_files = None - with open(os.path.join(rootfs, "info"), "r") as info_file: - info = json.loads(info_file.read()) - installed_files = info["installed-files"] if "installed-files" in info else None - - labels = {k.lower() : v for k, v in image_inspect.get('Labels', {}).items()} - summary = labels.get('summary', name) - version = labels.get("version", "1.0") - release = labels.get("release", "1.0") - license_ = labels.get("license", "GPLv2") - url = labels.get("url") - source0 = labels.get("source0") - requires = labels.get("requires") - provides = labels.get("provides") - conflicts = labels.get("conflicts") - description = labels.get("description") - - image_id = values["IMAGE_ID"] - - if os.path.exists(os.path.join(rootfs, "rpm.spec")): - with open(os.path.join(rootfs, "rpm.spec"), "r") as f: - spec_content = f.read() - else: - # If there is no spec file and 'auto' is used, do not install an rpm - if auto: - return None - spec_content = self._generate_spec_file(rpm_content, name, summary, license_, image_id, version=version, - release=release, url=url, source0=source0, requires=requires, - provides=provides, conflicts=conflicts, description=description, - installed_files=installed_files) - - with open(spec_file, "w") as f: - f.write(spec_content) - - cwd = os.getcwd() - result_dir = os.path.join(temp_dir, "build") - if not os.path.exists(result_dir): - os.makedirs(result_dir) - cmd = ["rpmbuild", "--noclean", "-bb", spec_file, - "--define", "_sourcedir %s" % temp_dir, - "--define", "_specdir %s" % temp_dir, - "--define", "_builddir %s" % temp_dir, - "--define", "_srcrpmdir %s" % cwd, - "--define", "_rpmdir %s" % result_dir, - "--build-in-place", - "--buildroot=%s" % rpm_content] - util.write_out(" ".join(cmd)) - if not self.display: - util.check_call(cmd) - success = True - return temp_dir + ret = self._generate_rpm_from_rootfs(rootfs, temp_dir, auto, name, image, values, include_containers_file) + if ret: + success = True + return ret finally: if not success: shutil.rmtree(temp_dir) + def _generate_rpm_from_rootfs(self, rootfs, temp_dir, auto, name, image, values, include_containers_file): + image_inspect = self.inspect_system_image(image) + rpm_content = os.path.join(temp_dir, "rpmroot") + spec_file = os.path.join(temp_dir, "container.spec") + + included_rpm = os.path.join(rootfs, "rootfs", "exports", "container.rpm") + if os.path.exists(included_rpm): + return included_rpm + + installed_files = None + with open(os.path.join(rootfs, "info"), "r") as info_file: + info = json.loads(info_file.read()) + installed_files = info["installed-files"] if "installed-files" in info else None + + labels = {k.lower() : v for k, v in image_inspect.get('Labels', {}).items()} + summary = labels.get('summary', name) + version = labels.get("version", "1.0") + release = labels.get("release", "1.0") + license_ = labels.get("license", "GPLv2") + url = labels.get("url") + source0 = labels.get("source0") + requires = labels.get("requires") + provides = labels.get("provides") + conflicts = labels.get("conflicts") + description = labels.get("description") + + image_id = values["IMAGE_ID"] + + if os.path.exists(os.path.join(rootfs, "rpm.spec")): + with open(os.path.join(rootfs, "rpm.spec"), "r") as f: + spec_content = f.read() + else: + # If there is no spec file and 'auto' is used, do not install an rpm + if auto: + return None + spec_content = self._generate_spec_file(rpm_content, name, summary, license_, image_id, version=version, + release=release, url=url, source0=source0, requires=requires, + provides=provides, conflicts=conflicts, description=description, + installed_files=installed_files, include_containers_file=include_containers_file) + + with open(spec_file, "w") as f: + f.write(spec_content) + + cwd = os.getcwd() + result_dir = os.path.join(temp_dir, "build") + if not os.path.exists(result_dir): + os.makedirs(result_dir) + cmd = ["rpmbuild", "--noclean", "-bb", spec_file, + "--define", "_sourcedir %s" % temp_dir, + "--define", "_specdir %s" % temp_dir, + "--define", "_builddir %s" % temp_dir, + "--define", "_srcrpmdir %s" % cwd, + "--define", "_rpmdir %s" % result_dir, + "--build-in-place", + "--buildroot=%s" % rpm_content] + util.write_out(" ".join(cmd)) + if not self.display: + util.check_call(cmd) + return temp_dir + def _generate_spec_file(self, destdir, name, summary, license_, image_id, version="1.0", release="1", url=None, source0=None, requires=None, conflicts=None, provides=None, description=None, - installed_files=None): + installed_files=None, include_containers_file=True): spec = "%global __requires_exclude_from ^.*$\n" spec = spec + "%global __provides_exclude_from ^.*$\n" + spec = spec + "%define _unpackaged_files_terminate_build 0\n" fields = {"Name" : "%s-%s" % (RPM_NAME_PREFIX, name), "Version" : version, "Release" : release, "Summary" : summary, "License" : license_, "URL" : url, "Source0" : source0, "Requires" : requires, @@ -1735,7 +1769,8 @@ def _generate_spec_file(self, destdir, name, summary, license_, image_id, versio for f in files: spec += "%config \"%s\"\n" % os.path.join("/", rel_path, f) - spec += "/usr/lib/containers/atomic/%s\n" % name + if include_containers_file: + spec += "/usr/lib/containers/atomic/%s\n" % name for root, _, files in os.walk(os.path.join(destdir, "usr/lib/systemd/system")): for f in files: spec = spec + "/usr/lib/systemd/system/%s\n" % f From 0b85afdfd7a659bf2c5647f947575d21b3bb8008 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 21 Mar 2017 17:06:17 +0100 Subject: [PATCH 17/18] syscontainers: do not error out on --system-package=auto and --user Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 4ec87ea6..62aa98b6 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -217,7 +217,10 @@ def install(self, image, name): rpm_preinstalled = None tmp_dir = None try: - if self.args.system_package in ['build', 'yes', 'auto']: + if self.args.system_package == 'auto' and self.args.system: + self.args.system_package = 'no' + + if self.args.system_package in ['build', 'yes']: if not self.args.system: raise ValueError("Only --system can generate rpms") From 4321643b36bf1606f0369dcfdcc7f3efff186927 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Tue, 21 Mar 2017 17:37:52 +0100 Subject: [PATCH 18/18] syscontainers: support update/rollback for containers using rpm This works only for not preinstalled containers (the ones under /usr) that are handled separately. Signed-off-by: Giuseppe Scrivano --- Atomic/syscontainers.py | 57 +++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/Atomic/syscontainers.py b/Atomic/syscontainers.py index 62aa98b6..480e9781 100644 --- a/Atomic/syscontainers.py +++ b/Atomic/syscontainers.py @@ -197,6 +197,18 @@ def _uninstall_rpm(self, rpm): else: util.check_call(["yum", "remove", "-y", rpm]) + @staticmethod + def _find_rpm(tmp_dir): + rpm_file = None + for root, _, files in os.walk(os.path.join(tmp_dir, "build")): + if rpm_file: + break + for f in files: + if f.endswith('.rpm'): + rpm_file = os.path.join(root, f) + break + return rpm_file + def install(self, image, name): repo = self._get_ostree_repo() if not repo: @@ -224,26 +236,16 @@ def install(self, image, name): if not self.args.system: raise ValueError("Only --system can generate rpms") - rpm_file = None auto = self.args.system_package == 'auto' include_containers_file = self.args.system_package == 'build' tmp_dir = self.generate_rpm(repo, auto, name, image, include_containers_file=include_containers_file) if tmp_dir: - for root, _, files in os.walk(os.path.join(tmp_dir, "build")): - if rpm_file: - break - for f in files: - if f.endswith('.rpm'): - rpm_file = os.path.join(root, f) - break + rpm_preinstalled = SystemContainers._find_rpm(tmp_dir) # If we are only build'ing the rpm, copy it to the cwd and exit if self.args.system_package == 'build': - destination = os.path.join(os.getcwd(), os.path.basename(rpm_file)) - shutil.move(rpm_file, destination) + destination = os.path.join(os.getcwd(), os.path.basename(rpm_preinstalled)) + shutil.move(rpm_preinstalled, destination) util.write_out("Generated rpm %s" % destination) - else: - self._install_rpm(rpm_file) - rpm_preinstalled = rpm_file if self.args.system_package == "build": return False @@ -631,7 +633,6 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou if rpm_preinstalled: new_installed_files = [] - shutil.move(rpm_preinstalled, destination) else: new_installed_files = self._rm_add_files_to_host(installed_files, exports, prefix or "/", files_template=installed_files_template, values=values, rename_files=rename_files) @@ -646,7 +647,7 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou shutil.copyfile(os.path.join(rootfs, "rpm.spec"), os.path.join(destination, "rpm.spec")) try: - rpm_installed = os.path.basename(rpm_preinstalled).replace(".rpm", "") if rpm_preinstalled else None + rpm_installed = os.path.basename(rpm_preinstalled) if rpm_preinstalled else None with open(os.path.join(destination, "info"), 'w') as info_file: info = {"image" : img, "revision" : image_id, @@ -706,6 +707,10 @@ def _do_checkout(self, repo, name, img, upgrade, values, destination, unitfileou os.unlink(sym) os.symlink(destination, sym) + if rpm_preinstalled: + self._install_rpm(rpm_preinstalled) + shutil.move(rpm_preinstalled, destination) + self._systemctl_command("daemon-reload") if (tmpfiles_template): self._systemd_tmpfiles("--create", tmpfilesout) @@ -839,6 +844,7 @@ def update_container(self, name, setvalues=None, rebase=None): values = info["values"] revision = info["revision"] if "revision" in info else None installed_files = info["installed-files"] if "installed-files" in info else None + rpm_installed = info["rpm-installed"] if "rpm-installed" in info else True # Check if the image id or the configuration for the container has # changed before upgrading it. @@ -864,7 +870,14 @@ def update_container(self, name, setvalues=None, rebase=None): util.write_out("Latest version already installed.") return - self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote, installed_files=installed_files) + # was installed with an rpm, update in the same way. + rpm_preinstalled = None + if rpm_installed: + tmp_dir = self.generate_rpm(repo, False, name, image, include_containers_file=False) + if tmp_dir: + rpm_preinstalled = SystemContainers._find_rpm(tmp_dir) + + self._checkout(repo, name, image, next_deployment, True, values, remote=self.args.remote, installed_files=installed_files, rpm_preinstalled=rpm_preinstalled) return def rollback(self, name): @@ -916,6 +929,16 @@ def rollback(self, name): os.unlink(path) os.symlink(destination, path) + + # reinstall the previous rpm if any. + rpm_installed = None + with open(os.path.join(self._get_system_checkout_path(), name, "info"), "r") as info_file: + info = json.loads(info_file.read()) + rpm_installed = info["rpm-installed"] if "rpm-installed" in info else True + + if rpm_installed: + self._install_rpm(os.path.join(self._get_system_checkout_path(), name, rpm_installed)) + if has_container_service: self._systemctl_command("daemon-reload") if (os.path.exists(tmpfiles)): @@ -1242,7 +1265,7 @@ def uninstall(self, name): os.unlink(unitfileout) if rpm_installed: - self._uninstall_rpm(rpm_installed) + self._uninstall_rpm(rpm_installed.replace(".rpm", "")) def prune_ostree_images(self): repo = self._get_ostree_repo()