Discussion:
Best practice for prepopulating the CacheDirectory of dynamic users
(too old to reply)
Antoine Pietri
2018-02-27 13:37:33 UTC
Permalink
Hi!

To experiment with systemd dynamic users, I started working on a
wrapper around a program that builds user packages (Archlinux makepkg)
and that refuses to be launched as root (for very good reasons). The
idea is that the wrapper just calls:

systemd-run --pipe \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
-p WorkingDirectory=/var/cache/mywrapper/build/repo \
makepkg

However, to be able to run makepkg, its cache directory has to be
pre-populated with a clone of the package to build. So, from my
wrapper, I just did:

os.makedirs(os.path.dirname(build_dir), exist_ok=True)
shutil.copytree(repo_path, build_dir)

to copy the content of the repo to the build directory. But it fails with:

run-u63.service: Failed to set up special execution directory in
/var/cache: File exists

This makes sense, because of the symbolic link shenanigans to
/var/cache/private that systemd uses to keep the filesystem readonly.
So, now I'm wondering what would be the best practice to prepopulate
this directory:

- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.

- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.

- Maybe there's something else I'm missing that would allow me to do
this more cleanly?

Thanks,
--
Antoine Pietri
Antoine Pietri
2018-02-28 15:03:33 UTC
Permalink
On Tue, Feb 27, 2018 at 2:37 PM, Antoine Pietri
Post by Antoine Pietri
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.
- Maybe there's something else I'm missing that would allow me to do
this more cleanly?
We came up with a third option, which looks a bit weird at first but
should work:

1) systemd-run -P \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
sh -c read

2) do the file operations in the Python code
3) send a "\n" or just kill() the systemd-run process when the setup is done.

I am still not satisfied with any of the three options, so I would
love to know what you think would be best. :-)
--
Antoine Pietri

On Tue, Feb 27, 2018 at 2:37 PM, Antoine Pietri
Post by Antoine Pietri
Hi!
To experiment with systemd dynamic users, I started working on a
wrapper around a program that builds user packages (Archlinux makepkg)
and that refuses to be launched as root (for very good reasons). The
systemd-run --pipe \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
-p WorkingDirectory=/var/cache/mywrapper/build/repo \
makepkg
However, to be able to run makepkg, its cache directory has to be
pre-populated with a clone of the package to build. So, from my
os.makedirs(os.path.dirname(build_dir), exist_ok=True)
shutil.copytree(repo_path, build_dir)
run-u63.service: Failed to set up special execution directory in
/var/cache: File exists
This makes sense, because of the symbolic link shenanigans to
/var/cache/private that systemd uses to keep the filesystem readonly.
So, now I'm wondering what would be the best practice to prepopulate
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.
- Maybe there's something else I'm missing that would allow me to do
this more cleanly?
Thanks,
--
Antoine Pietri
--
Antoine Pietri
aleivag
2018-02-28 16:13:36 UTC
Permalink
Hi Antoine:

2 disclosure before reading this:

1) i'm not part of systemd-devel team, and
2) this is also a shameless plug because i'm talking about a lib i created.

with that out of the way, here is my advice/solution.

do everything in python and use `pystemd` (pip install pystemd, just have
libsystemd installed and you should be fine), and also ditch the
cachedirectory in favor of PrivateTmp, that is always new when you start
your unit, and always goes away with your unit.

pystemd is apython wrapper around a few libsystemd-dev, and it has a nice
module name pystemd.run, here is a example

import sys
import pystemd.run

pscript = """
import os
import shutil
print(shutil.copytree('/var/cache/dnf', '/tmp/dnf'))
print(os.listdir('/tmp/dnf'))
"""

pystemd.run(
['/usr/bin/python3', '-c', pscript],
stdout=sys.stdout, stderr=sys.stderr, wait=True,
env={'PYTHONUSERBASE': '/dev/null'},
extra={'DynamicUser': True, 'PrivateTmp': True},
)

this would output: something like
/tmp/dnf
['last_makecache', '.gpgkeyschecked.yum', 'rawhide-2d95c80a1fa0a67d',
'google-chrome-filenames.solvx', 'tempfiles.json',
'rawhide-filenames.solvx', 'google-chrome.solv', 'expired_repos.json',
'rawhide.solv', 'packages.db', 'google-chrome-eb0d6f10ccbdafba']


so my recommendation, create a custom script with your build process and
call it using pystemd.run. Now, you could also use systemd-run to run your
script, but then it would not have been a shameless plug, right?

hope it helps


Alvaro Leiva
Post by Antoine Pietri
On Tue, Feb 27, 2018 at 2:37 PM, Antoine Pietri
Post by Antoine Pietri
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.
- Maybe there's something else I'm missing that would allow me to do
this more cleanly?
We came up with a third option, which looks a bit weird at first but
1) systemd-run -P \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
sh -c read
2) do the file operations in the Python code
3) send a "\n" or just kill() the systemd-run process when the setup is done.
I am still not satisfied with any of the three options, so I would
love to know what you think would be best. :-)
--
Antoine Pietri
On Tue, Feb 27, 2018 at 2:37 PM, Antoine Pietri
Post by Antoine Pietri
Hi!
To experiment with systemd dynamic users, I started working on a
wrapper around a program that builds user packages (Archlinux makepkg)
and that refuses to be launched as root (for very good reasons). The
systemd-run --pipe \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
-p WorkingDirectory=/var/cache/mywrapper/build/repo \
makepkg
However, to be able to run makepkg, its cache directory has to be
pre-populated with a clone of the package to build. So, from my
os.makedirs(os.path.dirname(build_dir), exist_ok=True)
shutil.copytree(repo_path, build_dir)
to copy the content of the repo to the build directory. But it fails
run-u63.service: Failed to set up special execution directory in
/var/cache: File exists
This makes sense, because of the symbolic link shenanigans to
/var/cache/private that systemd uses to keep the filesystem readonly.
So, now I'm wondering what would be the best practice to prepopulate
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.
- Maybe there's something else I'm missing that would allow me to do
this more cleanly?
Thanks,
--
Antoine Pietri
--
Antoine Pietri
_______________________________________________
systemd-devel mailing list
https://lists.freedesktop.org/mailman/listinfo/systemd-devel
Antoine Pietri
2018-02-28 16:33:36 UTC
Permalink
Post by aleivag
do everything in python and use `pystemd` (pip install pystemd, just have
libsystemd installed and you should be fine)
This is not an option for me as one of our requirements is to have
everything packaged in the repos of Archlinux. But thanks for making
me aware of this library, and your advice also applies to shelling out
to systemd-run.
Post by aleivag
and also ditch the
cachedirectory in favor of PrivateTmp, that is always new when you start
your unit, and always goes away with your unit.
I do need persistent data here to avoid recloning the repo at each
build, so it's not an option either. I'm not sure that changes
anything, though?
Post by aleivag
so my recommendation, create a custom script with your build process and
call it using pystemd.run.
This is kind of what we're doing right now with the same downsides,
but yeah that also works.
--
Antoine Pietri
Lennart Poettering
2018-02-28 16:24:33 UTC
Permalink
Post by Antoine Pietri
Hi!
To experiment with systemd dynamic users, I started working on a
wrapper around a program that builds user packages (Archlinux makepkg)
and that refuses to be launched as root (for very good reasons). The
systemd-run --pipe \
-p DynamicUser=yes \
-p CacheDirectory=mywrapper \
-p WorkingDirectory=/var/cache/mywrapper/build/repo \
makepkg
However, to be able to run makepkg, its cache directory has to be
pre-populated with a clone of the package to build. So, from my
Does it have to be a writable copy? if not you could just do '-p
BindReadOnlyPaths=/path/to/my/source:/var/cache/mywrapper'

i.e. there's no need to actually use the CacheDirectory= logic if the
semantics aren't right for you.

That said, maybe we should add a concept of TemplateCacheDirectory= or
so, which would allow prepopulating the dir from some external
source.
Post by Antoine Pietri
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
This sounds OK to me tbh.
Post by Antoine Pietri
- I believe another solution would be to modify /var/cache/private
directly, but I'm not sure it's a good practice to do so because I
don't know if this path is reliable or just an implementation detail.
Plus, it requires a weird special case compared to when I don't run
makepkg with systemd-run, as I have to insert something in the middle
of the copy destination path.
I think it's safe to treat /var/cache/private/ as API. We document it
already at various places, and the semantics aren't overly
complex. Hence this approach is OK too, as long as you create the
relevant dirs if they are missing with the right perms.

Lennart
--
Lennart Poettering, Red Hat
Antoine Pietri
2018-02-28 16:38:50 UTC
Permalink
On Wed, Feb 28, 2018 at 5:24 PM, Lennart Poettering
Post by Lennart Poettering
Does it have to be a writable copy? if not you could just do '-p
BindReadOnlyPaths=/path/to/my/source:/var/cache/mywrapper'
Yes it does, the build happens in place.
Post by Lennart Poettering
That said, maybe we should add a concept of TemplateCacheDirectory= or
so, which would allow prepopulating the dir from some external
source.
That would be cool, although if we can treat /var/cache/private as an
API, it might be redundant with just using /var/cache/private as the
template cache directory directly?
Post by Lennart Poettering
Post by Antoine Pietri
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
This sounds OK to me tbh.
Okay! Does that also apply to the other possible approach I sent in my
second mail? (Running a `dummy sh -c read` service with systemd-run,
do the setup and kill it when the setup is done). The advantage of
that one is that you can do any arbitrary processing while staying in
the Python code.

Thanks,
--
Antoine Pietri
Lennart Poettering
2018-02-28 17:13:45 UTC
Permalink
Post by Antoine Pietri
On Wed, Feb 28, 2018 at 5:24 PM, Lennart Poettering
Post by Lennart Poettering
Does it have to be a writable copy? if not you could just do '-p
BindReadOnlyPaths=/path/to/my/source:/var/cache/mywrapper'
Yes it does, the build happens in place.
Post by Lennart Poettering
That said, maybe we should add a concept of TemplateCacheDirectory= or
so, which would allow prepopulating the dir from some external
source.
That would be cool, although if we can treat /var/cache/private as an
API, it might be redundant with just using /var/cache/private as the
template cache directory directly?
Well, if we'd have TemplateCacheDirectory= then you could do fun stuff
like having a single template dir, but multiple instances, and each
time you start a new instance it gets its own private copy
transparently and magically.
Post by Antoine Pietri
Post by Lennart Poettering
Post by Antoine Pietri
- My current workaround is to shell-out to `systemd-run -p
DynamicUser=yes ...` first to do a mkdir -p, then for a cp -R. This
solution requires a lot of boilerplate from the Python wrapper and
takes more time for no good reason, so I think it's not ideal.
This sounds OK to me tbh.
Okay! Does that also apply to the other possible approach I sent in my
second mail? (Running a `dummy sh -c read` service with systemd-run,
do the setup and kill it when the setup is done). The advantage of
that one is that you can do any arbitrary processing while staying in
the Python code.
I am not sure I follow?

Lennart
--
Lennart Poettering, Red Hat
Antoine Pietri
2018-02-28 17:22:37 UTC
Permalink
On Wed, Feb 28, 2018 at 6:13 PM, Lennart Poettering
Post by Lennart Poettering
Post by Antoine Pietri
Okay! Does that also apply to the other possible approach I sent in my
second mail? (Running a `dummy sh -c read` service with systemd-run,
do the setup and kill it when the setup is done). The advantage of
that one is that you can do any arbitrary processing while staying in
the Python code.
I am not sure I follow?
If you run from the script:

systemd-run -P -p DynamicUser=yes -p CacheDirectory=mywrapper sh -c read

This will do all the setup with the symlink to /var/cache/private, and
then just hang. While the process is hanging, you can do the
processing you need in the cache directory, including populating it
with whatever you want.

Once your processing is over, you can kill the systemd-run process. On
subsequent calls to systemd-run, the files you added will just be
recursively chmod()ed by systemd, so you should just get back the
directory populated with your files with the correct permissions.
--
Antoine Pietri
Lennart Poettering
2018-02-28 17:59:41 UTC
Permalink
Post by Antoine Pietri
On Wed, Feb 28, 2018 at 6:13 PM, Lennart Poettering
Post by Lennart Poettering
Post by Antoine Pietri
Okay! Does that also apply to the other possible approach I sent in my
second mail? (Running a `dummy sh -c read` service with systemd-run,
do the setup and kill it when the setup is done). The advantage of
that one is that you can do any arbitrary processing while staying in
the Python code.
I am not sure I follow?
systemd-run -P -p DynamicUser=yes -p CacheDirectory=mywrapper sh -c read
This will do all the setup with the symlink to /var/cache/private, and
then just hang. While the process is hanging, you can do the
processing you need in the cache directory, including populating it
with whatever you want.
Once your processing is over, you can kill the systemd-run process. On
subsequent calls to systemd-run, the files you added will just be
recursively chmod()ed by systemd, so you should just get back the
directory populated with your files with the correct permissions.
Not sure I follow. Why do you let the service hang around? If all you
want to do is have it create the directory for you you could just run:

# systemd-run -P -p DynamicUser=yes -p CacheDirectory=mywrapper --wait true

That would be synchronous, would set up the dir and immediately
return.

Lennart
--
Lennart Poettering, Red Hat
Antoine Pietri
2018-03-01 00:32:03 UTC
Permalink
On Wed, Feb 28, 2018 at 6:59 PM, Lennart Poettering
Post by Lennart Poettering
Not sure I follow. Why do you let the service hang around? If all you
# systemd-run -P -p DynamicUser=yes -p CacheDirectory=mywrapper --wait true
That would be synchronous, would set up the dir and immediately
return.
My bad, for some reason my understanding was that there was some
teardown happening after the service was run, but the problem was just
somewhere else.
This works great, thanks!
--
Antoine Pietri
Lennart Poettering
2018-03-01 10:03:16 UTC
Permalink
Post by Antoine Pietri
On Wed, Feb 28, 2018 at 6:59 PM, Lennart Poettering
Post by Lennart Poettering
Not sure I follow. Why do you let the service hang around? If all you
# systemd-run -P -p DynamicUser=yes -p CacheDirectory=mywrapper --wait true
That would be synchronous, would set up the dir and immediately
return.
My bad, for some reason my understanding was that there was some
teardown happening after the service was run, but the problem was just
somewhere else.
There's a destruction logic in place for RuntimeDirectory= but not for
CacheDirectory=. And even for RuntimeDirectory= you can turn it off
with RuntimeDirectoryPreserve=.

Lennart
--
Lennart Poettering, Red Hat
Loading...