Browse Source

Import patch from backup as original branch was deleted: Pro Tip: Make sure you have merged your branch before deleting it

develop
Sam Black 2 years ago
parent
commit
c5636d0e08
20 changed files with 342 additions and 299 deletions
  1. +1
    -1
      services/dbus/services/org.lapwing.sponson.Build.service
  2. +1
    -1
      services/dbus/services/org.lapwing.sponson.Container.service
  3. +1
    -1
      services/dbus/services/org.lapwing.sponson.Image.service
  4. +1
    -1
      services/dbus/services/org.lapwing.sponson.Publish.service
  5. +1
    -1
      services/dbus/services/org.lapwing.sponson.Remote.service
  6. +0
    -20
      services/org.lapwing.sponson.Build.conf
  7. +0
    -5
      services/org.lapwing.sponson.Build.service
  8. +0
    -5
      services/org.lapwing.sponson.Container.service
  9. +0
    -5
      services/org.lapwing.sponson.Image.service
  10. +0
    -5
      services/org.lapwing.sponson.Publish.service
  11. +0
    -5
      services/org.lapwing.sponson.Remote.service
  12. +1
    -1
      services/systemd/system/sponson-build.service
  13. +76
    -0
      sponson/builder/dbus.py
  14. +1
    -1
      sponson/cli.py
  15. +24
    -14
      sponson/container/clone.py
  16. +87
    -21
      sponson/container/new.py
  17. +22
    -19
      sponson/delete.py
  18. +10
    -163
      sponson/systemd/__init__.py
  19. +110
    -26
      sponson/systemd/container.py
  20. +6
    -4
      sponson/utils.py

+ 1
- 1
services/dbus/services/org.lapwing.sponson.Build.service View File

@@ -1,5 +1,5 @@
[D-BUS Service]
Name=org.lapwing.sponson.Build
Exec=/usr/libexec/sponson/sponson-build
Exec=/bin/false
User=root
SystemdService=sponson-build.service

+ 1
- 1
services/dbus/services/org.lapwing.sponson.Container.service View File

@@ -1,5 +1,5 @@
[D-BUS Service]
Name=org.lapwing.sponson.Container
Exec=/usr/libexec/sponson/sponson-container
Exec=/bin/false
User=root
SystemdService=sponson-container.service

+ 1
- 1
services/dbus/services/org.lapwing.sponson.Image.service View File

@@ -1,5 +1,5 @@
[D-BUS Service]
Name=org.lapwing.sponson.Image
Exec=/usr/libexec/sponson/sponson-image
Exec=/bin/false
User=root
SystemdService=sponson-image.service

+ 1
- 1
services/dbus/services/org.lapwing.sponson.Publish.service View File

@@ -1,5 +1,5 @@
[D-BUS Service]
Name=org.lapwing.sponson.Publish
Exec=/usr/libexec/sponson/sponson-publish
Exec=/bin/false
User=root
SystemdService=sponson-publish.service

+ 1
- 1
services/dbus/services/org.lapwing.sponson.Remote.service View File

@@ -1,5 +1,5 @@
[D-BUS Service]
Name=org.lapwing.sponson.Remote
Exec=/usr/libexec/sponson/sponson-remote
Exec=/bin/false
User=root
SystemdService=sponson-remote.service

+ 0
- 20
services/org.lapwing.sponson.Build.conf View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Only root can own the service -->
<policy user="root">
<allow own="org.lapwing.sponson.Build"/>
</policy>

<policy context="default">
<allow send_destination="org.lapwing.sponson.Build"/>
<allow send_destination="org.lapwing.sponson.Build"
send_interface="org.freedesktop.DBus.Properties"/>
<allow send_destination="org.lapwing.sponson.Build"
send_interface="org.freedesktop.DBus.Introspectable"/>
</policy>

</busconfig>

+ 0
- 5
services/org.lapwing.sponson.Build.service View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=org.lapwing.sponson.Build
Exec=/bin/false
User=root
SystemdService=sponson-build.service

+ 0
- 5
services/org.lapwing.sponson.Container.service View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=org.lapwing.sponson.Container
Exec=/bin/false
User=root
SystemdService=sponson-container.service

+ 0
- 5
services/org.lapwing.sponson.Image.service View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=org.lapwing.sponson.Image
Exec=/bin/false
User=root
SystemdService=sponson-image.service

+ 0
- 5
services/org.lapwing.sponson.Publish.service View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=org.lapwing.sponson.Publish
Exec=/bin/false
User=root
SystemdService=sponson-publish.service

+ 0
- 5
services/org.lapwing.sponson.Remote.service View File

@@ -1,5 +0,0 @@
[D-BUS Service]
Name=org.lapwing.sponson.Remote
Exec=/bin/false
User=root
SystemdService=sponson-remote.service

+ 1
- 1
services/systemd/system/sponson-build.service View File

@@ -1,5 +1,5 @@
[Unit]
Description=Daemon for Sponson image builder service
Description=Daemon for Sponson image building

[Service]
Type=dbus


+ 76
- 0
sponson/builder/dbus.py View File

@@ -0,0 +1,76 @@
# coding=utf8
#
# dbus.py: DBUS service for building images
# Copyright (C) 2017 Sam Black <samwwwblack@lapwing.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pydbus.generic import signal
from pydbus.strong_typing import typed_method
from pydbus.xml_generator import attach_introspection_xml

from sponson.builder.build import Builder
from sponson.builder.build import BuilderError
from sponson.configfile import get_runtime_config
from sponson.configfile import read_config_file


@attach_introspection_xml
class BuilderDbus(object):
BUS_NAME = "org.lapwing.sponson.Build"
INTERFACE_NAME = BUS_NAME
OBJECT_PREFIX = "/org/lapwing/sponson/Build"
OBJECT_NAME = OBJECT_PREFIX

def __init__(self, system_bus):
self.system_bus = system_bus
self.runtime_config = get_runtime_config()

self.build_lock = False

@signal
@typed_method(("s",), None)
def BuildStarted(self, build_name):
pass

@signal
@typed_method(("s",), None)
def BuildFinished(self, build_name):
pass

@typed_method(("h", "s", "s"), None)
def Build(self, config_path, working_dir, remote):
if self.build_lock:
raise BuilderError("Image builder is already running")

if not working_dir:
working_dir = None

if not remote or remote == "local":
remote = None

config = read_config_file(config_path)
builder = Builder(config, self.runtime_config, remote)

self.build_lock = True
self.BuildStarted(builder.name)

try:
builder.start(working_dir)
except BuilderError:
self.build_lock = False
finally:
self.build_lock = False

self.BuildFinished(builder.name)

+ 1
- 1
sponson/cli.py View File

@@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import click
import os
import os.path
from pydbus import SystemBus




+ 24
- 14
sponson/container/clone.py View File

@@ -1,7 +1,7 @@
# coding=utf8
#
# clone.py: Container cloning
# Copyright (C) 2016, 2017 Sam Black <samwwwblack@lapwing.org>
# Copyright (C) 2016-2018 Sam Black <samwwwblack@lapwing.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,8 +19,8 @@
import os.path

from sponson.configfile import read_config_file
from sponson.constants import BASE_CONTAINER_DIR
from sponson.constants import ETC_CONTAINER_CONF_DIR
from sponson.constants import MOUNT_OVERLAY_CONTAINER
from sponson.container.new import New
from sponson.utils import clone_copy
from sponson.utils import get_logger
@@ -50,31 +50,33 @@ class Clone(New):
image_name = self.container_config["container"]["image"]["name"]
image_version = self.container_config["container"]["image"]["version"]

parent_name = self.container_config["container"]["name"]
parent_version = self.container_config["container"]["version"]

super().__init__(new_container, image_name, image_version,
runtime_config)
self.old_overlay = os.path.join(MOUNT_OVERLAY_CONTAINER, self.image,
parent_name, parent_version)
self.new_overlay = os.path.join(MOUNT_OVERLAY_CONTAINER, self.image,
self.name, self.version)

def start(self):
"""
Start creating new container.
Start cloning into a new container.
"""
raise NotImplementedError("Container cloning disabled")

logger.info("Starting")

logger.info("Sanity check")
self._sanity_check()
logger.info("Creating image mounts")
self._image_mount()
logger.info("Skeleton directory creation")
self._create_dirs()
logger.info("Creating container mounts")
self._container_mount()
logger.info("Cloning parent container")
self._copy_container()
logger.info("Creating container mounts")
logger.info("Creating container volume mounts")
self._mounts()
logger.info("Container firstboot")
logger.info("Container first boot")
self._firstboot()
logger.info("Link container to machines")
self._link_container()
logger.info("Create container unit file")
self._create_unit()
logger.info("Saving configuration")
self._save_config()

@@ -85,6 +87,14 @@ class Clone(New):
Copy container information from the old container
to the clone.
"""
parent_name = self.container_config["container"]["name"]
parent_version = self.container_config["container"]["version"]

self.old_overlay = os.path.join(BASE_CONTAINER_DIR, parent_name,
parent_version)
self.new_overlay = os.path.join(BASE_CONTAINER_DIR, self.name,
self.version)

old_path = os.path.join(self.old_overlay, "upper")
new_path = os.path.join(self.new_overlay, "upper")



+ 87
- 21
sponson/container/new.py View File

@@ -1,7 +1,7 @@
# coding=utf8
#
# new.py: New container creation
# Copyright (C) 2016, 2017 Sam Black <samwwwblack@lapwing.org>
# Copyright (C) 2016-2018 Sam Black <samwwwblack@lapwing.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,9 +22,9 @@ from datetime import datetime

from sponson import configfile
from sponson.constants import BASE_CONF_DIR
from sponson.constants import BASE_CONTAINER_DIR
from sponson.constants import BASE_IMAGE_DIR
from sponson.constants import ETC_CONTAINER_CONF_DIR
from sponson.constants import ETC_DIR
from sponson.constants import SYSTEMD_MACHINE_DIR
from sponson.image.ostree import ImageOstree
from sponson.systemd.container import SystemdContainer
@@ -77,7 +77,14 @@ class New(object):
self.image_version = str(self.all_config["image"]["version"])

self.name = name
self.path = os.path.join(SYSTEMD_MACHINE_DIR, name)
self.path = os.path.join(BASE_CONTAINER_DIR, name)
self.container_paths = {
"container": self.path,
"layer": ["firstboot"],
"volumes": [],
"instances": [],
"mount": os.path.join(self.path, "mnt")
}
self.version = datetime.now().strftime("%Y%m%dT%H%M%S")

self.architecture = self.all_config["image"].get("architecture")
@@ -107,12 +114,16 @@ class New(object):
logger.info("Starting")
logger.info("Sanity check")
self._sanity_check()
logger.info("Creating image mounts")
self._image_mount()
logger.info("Skeleton directory creation")
self._create_dirs()
logger.info("Creating container mounts")
self._container_mount()
logger.info("Creating container volume mounts")
self._mounts()
logger.info("Container firstboot")
logger.info("Container first boot")
self._firstboot()
logger.info("Link container to machines")
self._link_container()
logger.info("Create container unit file")
self._create_unit()
logger.info("Saving configuration")
@@ -124,35 +135,84 @@ class New(object):
"""
Check directories exist and required configuration is set.
"""
if not os.path.exists(ETC_DIR):
os.makedirs(ETC_DIR, 0o755)

if os.path.exists(self.path):
raise NewContainerError("Container path already exists")

try:
os.makedirs(self.path, 0o755)
except PermissionError:
raise NewContainerError(
"Insufficient permissions to make container")

def _image_mount(self):
def _create_dirs(self):
"""
Create directories for the new container.
"""
Create image overlay.
container_path_list = [
self.path,
os.path.join(self.path, "layer"),
os.path.join(self.path, "layer", "firstboot"),
self.container_paths["mount"]
]

if self.config.get("config"):
self.container_paths["layer"].append("config")
container_path_list.append(
os.path.join(self.path, "layer", "config"))

# TODO: Add layers provided by the user here

if self.config.get("volumes"):
container_path_list.append(os.path.join(self.path, "volumes"))
# TODO: Enumerate the unset volumes here

if self.config.get("readwrite"):
self.container_paths["instances"].append(self.version)
container_path_list.append(os.path.join(self.path, "instances"))
container_path_list.append(
os.path.join(self.path, "instances", "workdir"))
container_path_list.append(
os.path.join(self.path, "instances", self.version))

for path in container_path_list:
try:
os.makedirs(path, 0o750, True)
except PermissionError:
raise NewContainerError(
"Insufficient permissions to make container paths")

def _container_mount(self):
"""
Create container mount, including image, config, firstrun and layers.
"""
image_path = os.path.join(BASE_IMAGE_DIR, self.image,
self.image_version)
self.systemd.mount_image(self.name, self.version, self.path,
self.image, image_path)

extra_mounts = []

for layer in self.container_paths["layer"]:
if layer == "firstboot":
continue
extra_mounts.append(os.path.join(self.path, "layer", layer))

if self.config.get("readwrite"):
container_version = self.version

instances_path = os.path.join(self.path, "instances")
previous_versions = sorted([
d.path.lstrip("./") for d in os.scandir(instances_path)
if d.is_dir() and not d.path.startswith("./.")
])
for previous_version in previous_versions:
extra_mounts.append(
os.path.join(instances_path, previous_version))
else:
container_version = None

self.systemd.base_mount(self.name, image_path, container_version,
extra_mounts)

def _mounts(self):
"""
Add mounts to image.
"""
container = os.path.join(self.name, self.version)
for mount in self.mounts:
self.systemd.mount_path_inside(
mount["src"], self.image, container, mount["dest"],
mount["src"], self.name, mount["dest"],
mount.get("type", "overlay"), mount.get("mode", "rw"))

def _firstboot(self):
@@ -161,6 +221,12 @@ class New(object):
"""
self.systemd.firstboot(self.name, self.path)

def _link_container(self):
"""
Link container mount directory to systemd-nspawn's /var/lib/machines.
"""
os.link(self.path, os.path.join(SYSTEMD_MACHINE_DIR, self.name))

def _create_unit(self):
"""
Creates container unit file.


+ 22
- 19
sponson/delete.py View File

@@ -22,9 +22,8 @@ import shutil
from sponson.configfile import read_config_file
from sponson.constants import BASE_CONF_DIR
from sponson.constants import BASE_IMAGE_DIR
from sponson.constants import BASE_CONTAINER_DIR
from sponson.constants import ETC_CONTAINER_CONF_DIR
from sponson.constants import MOUNT_OVERLAY
from sponson.constants import MOUNT_OVERLAY_CONTAINER
from sponson.constants import SYSTEMD_MACHINE_DIR
from sponson.constants import SYSTEMD_NSPAWN_DIR
from sponson.constants import SYSTEMD_SYSTEM_DIR
@@ -38,8 +37,9 @@ from sponson.utils import get_logger
logger = get_logger(__name__)

# List of directories to stop deleting when reached
DELETION_STOP_DIRS = (BASE_IMAGE_DIR, ETC_CONTAINER_CONF_DIR,
SYSTEMD_MACHINE_DIR, SYSTEMD_SYSTEM_DIR)
DELETION_STOP_DIRS = (BASE_IMAGE_DIR, BASE_CONTAINER_DIR,
ETC_CONTAINER_CONF_DIR, SYSTEMD_MACHINE_DIR,
SYSTEMD_SYSTEM_DIR)


class DeleteError(Exception):
@@ -169,13 +169,17 @@ class Delete(object):
Remove container directory.
"""
for path in self.systemd_paths:
# Protect against traversal attacks
path = os.path.abspath(path)
# Some paths might be broken symlinks
if not os.path.lexists(path):
continue

if (path.startswith(MOUNT_OVERLAY) and
os.path.basename(path) in ("lower", "upper")):
path = os.path.dirname(path)
if not (path.startswith(BASE_IMAGE_DIR) or
path.startswith(BASE_CONTAINER_DIR)):
logger.warning("Cannot delete outside image or container "
"directories: '{}'".format(path))
continue

if os.path.isdir(path):
shutil.rmtree(path, True)
@@ -210,11 +214,9 @@ class DeleteContainer(Delete):

super().__init__(name, config, runtime_config)

self.path = os.path.join(SYSTEMD_MACHINE_DIR, self.name)
self.container_path = os.path.join(MOUNT_OVERLAY_CONTAINER,
self.config["image"]["name"],
self.name,
self.config["container"]["version"])
self.path = os.path.join(BASE_CONTAINER_DIR, self.name)
self.link = os.path.join(SYSTEMD_SYSTEM_DIR, self.name)
self.container_mount = os.path.join(self.path, "mnt")
self.unit_name = "systemd-nspawn@{}.service".format(self.name)
self.systemd = SystemdContainer(runtime_config)

@@ -230,7 +232,7 @@ class DeleteContainer(Delete):
"""
Unmount containers directories.
"""
self.systemd.unmount_path(self.path)
self.systemd.unmount_path(self.container_mount)

def _clean_sponson_conf(self):
"""
@@ -240,17 +242,18 @@ class DeleteContainer(Delete):

self.systemd_paths.append(
os.path.join(SYSTEMD_SYSTEM_DIR, "{}.d".format(self.unit_name)))
self.systemd_paths.append(self.container_path)
self.systemd_paths.append(os.path.join(SYSTEMD_NSPAWN_DIR,
"{}.nspawn".format(self.name)))

self.systemd_paths.append(
os.path.join(SYSTEMD_SYSTEM_DIR,
"{}.mount".format(self.systemd._escape(self.path))))
os.path.join(
SYSTEMD_SYSTEM_DIR,
"{}.mount".format(self.systemd._escape(self.container_mount))))
self.systemd_paths.append(
os.path.join(SYSTEMD_SYSTEM_DIR,
"{}.automount".format(
self.systemd._escape(self.path))))
os.path.join(
SYSTEMD_SYSTEM_DIR,
"{}.automount".format(
self.systemd._escape(self.container_mount))))
self.systemd_paths.append(self.path)

os.remove(os.path.join(ETC_CONTAINER_CONF_DIR,


+ 10
- 163
sponson/systemd/__init__.py View File

@@ -1,7 +1,7 @@
# coding=utf8
#
# systemd.py: systemd wrapper and related functions.
# Copyright (C) 2016 Sam Black <samwwwblack@lapwing.org>
# Copyright (C) 2016, 2018 Sam Black <samwwwblack@lapwing.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -50,6 +50,7 @@ class Systemd(object):
"""
DBUS_UNIT_PATH = "org.freedesktop.systemd1.Unit"
DBUS_MOUNT_PATH = "org.freedesktop.systemd1.Mount"
MINIMUM_VERSION = 234

def __init__(self, runtime_config):
"""
@@ -91,12 +92,13 @@ class Systemd(object):
raise SystemdException(
"Cannot determine running systemd version")
else:
if version >= 222:
if version >= self.MINIMUM_VERSION:
return version
else:
raise SystemdException(
"Running systemd version is {}: "
"222 or greater is required".format(version))
"{} or greater is required".format(
version, self.MINIMUM_VERSION))

def _escape(self, path):
"""
@@ -311,8 +313,11 @@ class Systemd(object):
:param path: directory to unmount
:type path: str
"""
base_path = os.path.dirname(path)
if base_path not in (BASE_DIR, SYSTEMD_MACHINE_DIR):
path = os.path.abspath(path)
for allowed_path in (BASE_DIR, SYSTEMD_MACHINE_DIR):
if path.startswith(allowed_path):
break
else:
raise SystemdException("Cannot unmount non sponson path")

systemd_unit = self._escape(path)
@@ -353,164 +358,6 @@ class Systemd(object):
# TODO: Do we want to propagate unmounting Requires?
self.unmount_unit(sub_mount, False)

def _mount_create_overlay(self, where, lower, upper=None, workdir=None,
automount=True, requires=None):
"""
Create an ``overlay`` ``mount`` systemd unit file.

:param where: Path to mount to.
Can be a ':' separated list of paths to mount in order.
:type where: str
:param lower: Lower device or path to mount as read-only
:type lower: str
:param upper: Upper directory to mount in the image.
This should be the same as the ``where`` argument.
If ommitted or None, the mount is considered read-only.
:type upper: str or None
:param workdir: Workdir path.
If ommitted or None, the mount is considered read-only.
:type workdir: str or None
:param automount: enable as an automounted directory
:type automount: bool
:param requires: required unit file, such that this overlay mount
will pull in another service or mount
:type requires: str or None
"""
if upper and not workdir:
raise SystemdException("Missing required argument 'workdir'")

if not upper and ":" not in lower:
raise SystemdException("Missing multiple lowerdir directives")

opt = "lowerdir={}".format(lower)

if not os.path.isdir(where):
os.makedirs(where, 0o755)

if upper:
opt = "{},upperdir={},workdir={}".format(opt, upper, workdir)
if not os.path.isdir(upper):
os.makedirs(upper, 0o755)
if not os.path.isdir(workdir):
os.makedirs(workdir, 0o755)

self.mount_create_unit("overlay", where, "overlay", opt, automount,
requires)

def _mount_create_bind(self, image_path, mount_path, mode="ro",
automount=True):
"""
Create an ``bind`` ``mount`` systemd unit file.

:param image_path: Path to mount to.
:type image_path: str
:param mount_path: Path to mount from.
:type mount_path: str
:param mode: Mount source as either "read-only" (default)
or "read-write"
:type mode: str
:param automount: Create corresponding automount unit file
:type automount: bool
"""
if mode == "rw":
opt = "bind,rw"
else:
opt = "bind,ro"
self.mount_create_unit(mount_path, image_path, "none", opt, automount)

def _overlay_mtab(self):
"""
Collates all overlayfs mounts.

:return: list of overlay mounts
:rtype: list
"""
overlay_mtab = []
with open("/proc/mounts") as f:
for mtab in f.readlines():
if "overlay" in mtab:
overlay_mtab.append(mtab)
return overlay_mtab

def _recurse_overlay_mounts(self, mount_path, overlay_mtab=None):
"""
Find the directories for an overlayfs mount point.

This will recurse down,
such that if another overlayfs mount is found,
its lower directories will be discovered.

:param mount_path: path to investigate
:type mount_path: str
:param overlay_mtab: list of overlay mount points,
a fresh copy will be made if this is omitted.
:type overlay_mtab: list or None
:return: list of "lower" directories for the mount point
:rtype: list
"""
if not overlay_mtab:
overlay_mtab = self._overlay_mtab()

# We can't mount multiple OverlayFS on top of each other,
# as the kernel only traverses 2 mount layers.
# Determine the "lower" layers to mount, if any.
src_paths = []
for mtab in overlay_mtab:
split_mtab = mtab.split()
# We're only interested in the mount point,
# not any containers that use the image.
if mount_path in mtab and mount_path == split_mtab[1]:
opts = split_mtab[3]

if "upperdir" in opts:
src_paths.append(
opts.split("upperdir=")[1].split(",")[0])

mount_lower_dirs = opts.split("lowerdir=")[1].split(",")[0]

lower_dirs = []
for lowerd in mount_lower_dirs.split(":"):
if os.path.ismount(lowerd):
lower_dirs.extend(
self._recurse_overlay_mounts(lowerd, overlay_mtab))
else:
lower_dirs.append(lowerd)
src_paths.extend(lower_dirs)

return src_paths

def _lower_overlay(self, overlay_dir, source, dest):
"""
Create lower mounts for overlayfs mount points.

:param overlay_dir: base overlay directory
:type overlay_dir: str
:param source: source image/container path
:type source: str
:param dest: mount location
:type dest: str
"""
upper = os.path.join(overlay_dir, "upper")
workdir = os.path.join(overlay_dir, "workdir")

if os.path.islink(source):
source = os.path.realpath(source)

if not os.path.ismount(source):
self._mount_create_overlay(dest, source, upper, workdir)
else:
overlay_mtab = self._overlay_mtab()

src_paths = self._recurse_overlay_mounts(source, overlay_mtab)

lower = os.path.join(overlay_dir, "lower")
lower_mount = "{}.mount".format(self._escape(lower))
self._mount_create_overlay(lower, ":".join(src_paths),
automount=False)

self._mount_create_overlay(dest, lower, upper, workdir,
requires=lower_mount)

def network_check(self):
"""
Check the networking setup is sane,


+ 110
- 26
sponson/systemd/container.py View File

@@ -1,7 +1,7 @@
# coding=utf8
#
# systemd.py: systemd wrapper and container related functions.
# Copyright (C) 2016 Sam Black <samwwwblack@lapwing.org>
# Copyright (C) 2016, 2018 Sam Black <samwwwblack@lapwing.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@ import os.path
import subprocess

from sponson.constants import FIREWALL_INTERFACE
from sponson.constants import MOUNT_OVERLAY_CONTAINER
from sponson.constants import BASE_CONTAINER_DIR
from sponson.constants import SYSTEMD_MACHINE_DIR
from sponson.constants import SYSTEMD_NSPAWN_DIR
from sponson.constants import SYSTEMD_SYSTEM_DIR
@@ -49,15 +49,78 @@ class SystemdContainer(Systemd):
super().__init__(runtime_config)
self.firewall = Firewall(runtime_config)

def mount_path_inside(self, mount_path, image, container, container_path,
def _mount_create_overlay(self, where, lower, upper=None, workdir=None,
automount=True, requires=None):
"""
Create an ``overlay`` ``mount`` systemd unit file.

:param where: Path to mount to.
Can be a ':' separated list of paths to mount in order.
:type where: str
:param lower: Lower device or path to mount as read-only
:type lower: str
:param upper: Upper directory to mount in the image.
This should be the same as the ``where`` argument.
If omitted or None, the mount is considered read-only.
:type upper: str or None
:param workdir: Workdir path.
If omitted or None, the mount is considered read-only.
:type workdir: str or None
:param automount: enable as an automounted directory
:type automount: bool
:param requires: required unit file, such that this overlay mount
will pull in another service or mount
:type requires: str or None
"""
if upper and not workdir:
raise SystemdException("Missing required argument 'workdir'")

if not upper and ":" not in lower:
raise SystemdException("Missing multiple lowerdir directives")

opt = "lowerdir={}".format(lower)

if not os.path.isdir(where):
os.makedirs(where, 0o750)

if upper:
opt = "{},upperdir={},workdir={}".format(opt, upper, workdir)
if not os.path.isdir(upper):
os.makedirs(upper, 0o755)
if not os.path.isdir(workdir):
os.makedirs(workdir, 0o755)

self.mount_create_unit("overlay", where, "overlay", opt, automount,
requires)

def _mount_create_bind(self, image_path, mount_path, mode="ro",
automount=True):
"""
Create an ``bind`` ``mount`` systemd unit file.

:param image_path: Path to mount to.
:type image_path: str
:param mount_path: Path to mount from.
:type mount_path: str
:param mode: Mount source as either "read-only" (default)
or "read-write"
:type mode: str
:param automount: Create corresponding automount unit file
:type automount: bool
"""
if mode == "rw":
opt = "bind,rw"
else:
opt = "bind,ro"
self.mount_create_unit(mount_path, image_path, "none", opt, automount)

def mount_path_inside(self, mount_path, container, container_path,
mount_type="overlay", mode="rw"):
"""
Mount a path into a container.

:param mount_path: path to mount from
:type mount_path: str
:param image: image base of the container
:type image: str
:param container: container to mount into
:type container: str
:param container_path: path to mount into.
@@ -72,17 +135,18 @@ class SystemdContainer(Systemd):
"""
if os.path.abspath(container_path) == "/":
raise SystemdException("Cannot mount '/' to a container")
container_mount = os.path.join(SYSTEMD_MACHINE_DIR, container,

container_base = os.path.join(BASE_CONTAINER_DIR, container)
container_mount = os.path.join(container_base, "mnt",
container_path.lstrip("/"))
container_volumes = os.path.join(container_base, "volumes")

if mode == "rw" and mount_type == "overlay":
overlay_path = os.path.join(MOUNT_OVERLAY_CONTAINER,
image, container)
escape_image_path = self._escape(container_mount)

upper = os.path.join(overlay_path, escape_image_path,
upper = os.path.join(container_volumes, escape_image_path,
"upper")
workdir = os.path.join(overlay_path, escape_image_path,
workdir = os.path.join(container_volumes, escape_image_path,
"workdir")

self._mount_create_overlay(container_mount, mount_path, upper,
@@ -92,26 +156,49 @@ class SystemdContainer(Systemd):
else:
self._mount_create_bind(container_mount, mount_path, mode)

def mount_image(self, container, container_version, container_path,
image, image_path):
def base_mount(self, container, image_path, container_version=None,
ro_mounts=None):
"""
Mount an image for a container.
Set up mounts for the container, including the image, first run, config
and layer setups.

:param container: container name
:type container: str
:param container_version: container version
:type container_version: str
:param container_path: path to the container mount
:type container_path: str
:param image: image name
:type image: str
:param image_path: image path to mount
:type image_path: str
:param container_version: version of the container to mount for,
defaults to None for a read only container.
:type container_version: str or None
:param ro_mounts: extra mounts to add to the
read only part of the overlay mount,
including config, layer or container instance versions.
:type ro_mounts: list
"""
container_overlay = os.path.join(MOUNT_OVERLAY_CONTAINER, image,
container, container_version)
container_mount = os.path.join(BASE_CONTAINER_DIR, container, "mnt")
container_first_run = os.path.join(BASE_CONTAINER_DIR, container,
"config", "firstrun")

if ro_mounts:
# Don't mount the image or first run directories twice
ro_mounts.remove(container_mount)
ro_mounts.remove(container_first_run)

self._lower_overlay(container_overlay, image_path, container_path)
lower_mount = "{}:{}:{}".format(image_path, container_first_run,
":".join(ro_mounts))
else:
lower_mount = "{}:{}".format(image_path, container_first_run)

if container_version:
workdir = os.path.join(BASE_CONTAINER_DIR, container, "instances",
"workdir")
upper = os.path.join(BASE_CONTAINER_DIR, container, "instances",
container_version)
else:
workdir = None
upper = None

self._mount_create_overlay(container_mount, lower_mount, upper,
workdir)

def _limits(self, limits):
"""
@@ -283,10 +370,7 @@ class SystemdContainer(Systemd):
:return: True if running, False otherwise
:rtype: bool
"""
if self.version < 229:
unit_name = "sponson-container-{}.service".format(name)
else:
unit_name = "systemd-nspawn@{}.service".format(name)
unit_name = "systemd-nspawn@{}.service".format(name)
unit_obj = self._unit_properties(unit_name)
return (unit_obj.Get(self.DBUS_UNIT_PATH, "ActiveState") in
("active", "activating", "reloading"))

+ 6
- 4
sponson/utils.py View File

@@ -42,7 +42,7 @@ import pwd
import shutil
import stat

from sponson.constants import MOUNT_OVERLAY_CONTAINER
from sponson.constants import BASE_CONTAINER_DIR

gi.require_version("GLib", "2.0")
from gi.repository import GLib # noqa: E402
@@ -215,9 +215,11 @@ def clone_copy(src, dst):
:param dst: destination file or directory
:type dst: str
"""
if (not src.startswith(MOUNT_OVERLAY_CONTAINER) or
not dst.startswith(MOUNT_OVERLAY_CONTAINER)):
raise shutil.Error("Not in container overlay directory")
if (not src.startswith(BASE_CONTAINER_DIR) or
"instances" not in src or
not dst.startswith(BASE_CONTAINER_DIR) or
"instances" not in dst):
raise shutil.Error("Not in container instance directory")
names = os.listdir(src)
os.makedirs(dst, exist_ok=True)
errors = []


Loading…
Cancel
Save