]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
61d0578b | 2 | |
e08f94ac LP |
3 | #include <linux/loop.h> |
4 | ||
61d0578b LP |
5 | #include "bus-common-errors.h" |
6 | #include "bus-error.h" | |
f4351959 | 7 | #include "chase-symlinks.h" |
61d0578b LP |
8 | #include "conf-files.h" |
9 | #include "copy.h" | |
6a818c3c | 10 | #include "data-fd-util.h" |
61d0578b LP |
11 | #include "def.h" |
12 | #include "dirent-util.h" | |
57f1b61b | 13 | #include "discover-image.h" |
61d0578b | 14 | #include "dissect-image.h" |
239ac0c7 | 15 | #include "env-file.h" |
8a129c80 | 16 | #include "env-util.h" |
ba5b6c59 | 17 | #include "errno-list.h" |
907952bb | 18 | #include "escape.h" |
239ac0c7 | 19 | #include "extension-release.h" |
61d0578b LP |
20 | #include "fd-util.h" |
21 | #include "fileio.h" | |
22 | #include "fs-util.h" | |
5cfa33e0 | 23 | #include "install.h" |
61d0578b LP |
24 | #include "io-util.h" |
25 | #include "locale-util.h" | |
26 | #include "loop-util.h" | |
61d0578b | 27 | #include "mkdir.h" |
d8b4d14d | 28 | #include "nulstr-util.h" |
61d0578b LP |
29 | #include "os-util.h" |
30 | #include "path-lookup.h" | |
31 | #include "portable.h" | |
32 | #include "process-util.h" | |
409f4cf1 | 33 | #include "selinux-util.h" |
61d0578b LP |
34 | #include "set.h" |
35 | #include "signal-util.h" | |
36 | #include "socket-util.h" | |
760877e9 | 37 | #include "sort-util.h" |
61d0578b LP |
38 | #include "string-table.h" |
39 | #include "strv.h" | |
e4de7287 | 40 | #include "tmpfile-util.h" |
61d0578b LP |
41 | #include "user-util.h" |
42 | ||
61d0578b LP |
43 | /* Markers used in the first line of our 20-portable.conf unit file drop-in to determine, that a) the unit file was |
44 | * dropped there by the portable service logic and b) for which image it was dropped there. */ | |
45 | #define PORTABLE_DROPIN_MARKER_BEGIN "# Drop-in created for image '" | |
46 | #define PORTABLE_DROPIN_MARKER_END "', do not edit." | |
47 | ||
48 | static bool prefix_match(const char *unit, const char *prefix) { | |
49 | const char *p; | |
50 | ||
51 | p = startswith(unit, prefix); | |
52 | if (!p) | |
53 | return false; | |
54 | ||
55 | /* Only respect prefixes followed by dash or dot or when there's a complete match */ | |
56 | return IN_SET(*p, '-', '.', '@', 0); | |
57 | } | |
58 | ||
59 | static bool unit_match(const char *unit, char **matches) { | |
60 | const char *dot; | |
61d0578b LP |
61 | |
62 | dot = strrchr(unit, '.'); | |
63 | if (!dot) | |
64 | return false; | |
65 | ||
66 | if (!STR_IN_SET(dot, ".service", ".socket", ".target", ".timer", ".path")) | |
67 | return false; | |
68 | ||
69 | /* Empty match expression means: everything */ | |
70 | if (strv_isempty(matches)) | |
71 | return true; | |
72 | ||
73 | /* Otherwise, at least one needs to match */ | |
74 | STRV_FOREACH(i, matches) | |
75 | if (prefix_match(unit, *i)) | |
76 | return true; | |
77 | ||
78 | return false; | |
79 | } | |
80 | ||
409f4cf1 | 81 | static PortableMetadata *portable_metadata_new(const char *name, const char *path, const char *selinux_label, int fd) { |
61d0578b LP |
82 | PortableMetadata *m; |
83 | ||
84 | m = malloc0(offsetof(PortableMetadata, name) + strlen(name) + 1); | |
85 | if (!m) | |
86 | return NULL; | |
87 | ||
907952bb LB |
88 | /* In case of a layered attach, we want to remember which image the unit came from */ |
89 | if (path) { | |
90 | m->image_path = strdup(path); | |
2f092762 FS |
91 | if (!m->image_path) |
92 | return mfree(m); | |
907952bb LB |
93 | } |
94 | ||
409f4cf1 LB |
95 | /* The metadata file might have SELinux labels, we need to carry them and reapply them */ |
96 | if (!isempty(selinux_label)) { | |
97 | m->selinux_label = strdup(selinux_label); | |
98 | if (!m->selinux_label) { | |
99 | free(m->image_path); | |
100 | return mfree(m); | |
101 | } | |
102 | } | |
103 | ||
61d0578b LP |
104 | strcpy(m->name, name); |
105 | m->fd = fd; | |
106 | ||
907952bb | 107 | return TAKE_PTR(m); |
61d0578b LP |
108 | } |
109 | ||
110 | PortableMetadata *portable_metadata_unref(PortableMetadata *i) { | |
111 | if (!i) | |
112 | return NULL; | |
113 | ||
114 | safe_close(i->fd); | |
115 | free(i->source); | |
907952bb | 116 | free(i->image_path); |
409f4cf1 | 117 | free(i->selinux_label); |
61d0578b LP |
118 | |
119 | return mfree(i); | |
120 | } | |
121 | ||
61d0578b LP |
122 | static int compare_metadata(PortableMetadata *const *x, PortableMetadata *const *y) { |
123 | return strcmp((*x)->name, (*y)->name); | |
124 | } | |
125 | ||
126 | int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret) { | |
127 | ||
128 | _cleanup_free_ PortableMetadata **sorted = NULL; | |
61d0578b LP |
129 | PortableMetadata *item; |
130 | size_t k = 0; | |
131 | ||
132 | sorted = new(PortableMetadata*, hashmap_size(unit_files)); | |
133 | if (!sorted) | |
134 | return -ENOMEM; | |
135 | ||
90e74a66 | 136 | HASHMAP_FOREACH(item, unit_files) |
61d0578b LP |
137 | sorted[k++] = item; |
138 | ||
139 | assert(k == hashmap_size(unit_files)); | |
140 | ||
141 | typesafe_qsort(sorted, k, compare_metadata); | |
142 | ||
143 | *ret = TAKE_PTR(sorted); | |
144 | return 0; | |
145 | } | |
146 | ||
447d4687 | 147 | static int send_one_fd_iov_with_data_fd( |
61d0578b | 148 | int socket_fd, |
447d4687 LB |
149 | const struct iovec *iov, |
150 | size_t iovlen, | |
61d0578b LP |
151 | int fd) { |
152 | ||
61d0578b LP |
153 | _cleanup_close_ int data_fd = -1; |
154 | ||
447d4687 | 155 | assert(iov || iovlen == 0); |
61d0578b | 156 | assert(socket_fd >= 0); |
61d0578b LP |
157 | assert(fd >= 0); |
158 | ||
6a818c3c | 159 | data_fd = copy_data_fd(fd); |
61d0578b LP |
160 | if (data_fd < 0) |
161 | return data_fd; | |
162 | ||
447d4687 | 163 | return send_one_fd_iov(socket_fd, data_fd, iov, iovlen, 0); |
61d0578b LP |
164 | } |
165 | ||
67818055 YW |
166 | DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(portable_metadata_hash_ops, char, string_hash_func, string_compare_func, |
167 | PortableMetadata, portable_metadata_unref); | |
168 | ||
61d0578b LP |
169 | static int extract_now( |
170 | const char *where, | |
171 | char **matches, | |
239ac0c7 LB |
172 | const char *image_name, |
173 | bool path_is_extension, | |
61d0578b LP |
174 | int socket_fd, |
175 | PortableMetadata **ret_os_release, | |
176 | Hashmap **ret_unit_files) { | |
177 | ||
67818055 | 178 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b LP |
179 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; |
180 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
181 | _cleanup_close_ int os_release_fd = -1; | |
182 | _cleanup_free_ char *os_release_path = NULL; | |
239ac0c7 | 183 | const char *os_release_id; |
61d0578b LP |
184 | int r; |
185 | ||
186 | /* Extracts the metadata from a directory tree 'where'. Extracts two kinds of information: the /etc/os-release | |
187 | * data, and all unit files matching the specified expression. Note that this function is called in two very | |
188 | * different but also similar contexts. When the tool gets invoked on a directory tree, we'll process it | |
189 | * directly, and in-process, and thus can return the requested data directly, via 'ret_os_release' and | |
190 | * 'ret_unit_files'. However, if the tool is invoked on a raw disk image — which needs to be mounted first — we | |
191 | * are invoked in a child process with private mounts and then need to send the collected data to our | |
192 | * parent. To handle both cases in one call this function also gets a 'socket_fd' parameter, which when >= 0 is | |
193 | * used to send the data to the parent. */ | |
194 | ||
195 | assert(where); | |
196 | ||
239ac0c7 LB |
197 | /* First, find os-release/extension-release and send it upstream (or just save it). */ |
198 | if (path_is_extension) { | |
199 | os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name); | |
200 | r = open_extension_release(where, image_name, &os_release_path, &os_release_fd); | |
201 | } else { | |
202 | os_release_id = "/etc/os-release"; | |
203 | r = open_os_release(where, &os_release_path, &os_release_fd); | |
204 | } | |
61d0578b | 205 | if (r < 0) |
239ac0c7 LB |
206 | log_debug_errno(r, |
207 | "Couldn't acquire %s file, ignoring: %m", | |
208 | path_is_extension ? "extension-release " : "os-release"); | |
61d0578b LP |
209 | else { |
210 | if (socket_fd >= 0) { | |
447d4687 LB |
211 | struct iovec iov[] = { |
212 | IOVEC_MAKE_STRING(os_release_id), | |
409f4cf1 | 213 | IOVEC_MAKE((char *)"\0", sizeof(char)), |
447d4687 LB |
214 | }; |
215 | ||
216 | r = send_one_fd_iov_with_data_fd(socket_fd, iov, ELEMENTSOF(iov), os_release_fd); | |
61d0578b LP |
217 | if (r < 0) |
218 | return log_debug_errno(r, "Failed to send os-release file: %m"); | |
219 | } | |
220 | ||
221 | if (ret_os_release) { | |
409f4cf1 | 222 | os_release = portable_metadata_new(os_release_id, NULL, NULL, os_release_fd); |
61d0578b LP |
223 | if (!os_release) |
224 | return -ENOMEM; | |
225 | ||
226 | os_release_fd = -1; | |
227 | os_release->source = TAKE_PTR(os_release_path); | |
228 | } | |
229 | } | |
230 | ||
231 | /* Then, send unit file data to the parent (or/and add it to the hashmap). For that we use our usual unit | |
232 | * discovery logic. Note that we force looking inside of /lib/systemd/system/ for units too, as we mightbe | |
233 | * compiled for a split-usr system but the image might be a legacy-usr one. */ | |
234 | r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, where); | |
235 | if (r < 0) | |
236 | return log_debug_errno(r, "Failed to acquire lookup paths: %m"); | |
237 | ||
67818055 | 238 | unit_files = hashmap_new(&portable_metadata_hash_ops); |
61d0578b LP |
239 | if (!unit_files) |
240 | return -ENOMEM; | |
241 | ||
242 | STRV_FOREACH(i, paths.search_path) { | |
243 | _cleanup_free_ char *resolved = NULL; | |
244 | _cleanup_closedir_ DIR *d = NULL; | |
61d0578b LP |
245 | |
246 | r = chase_symlinks_and_opendir(*i, where, 0, &resolved, &d); | |
247 | if (r < 0) { | |
248 | log_debug_errno(r, "Failed to open unit path '%s', ignoring: %m", *i); | |
249 | continue; | |
250 | } | |
251 | ||
252 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to read directory: %m")) { | |
253 | _cleanup_(portable_metadata_unrefp) PortableMetadata *m = NULL; | |
2ef20244 | 254 | _cleanup_(mac_selinux_freep) char *con = NULL; |
61d0578b LP |
255 | _cleanup_close_ int fd = -1; |
256 | ||
257 | if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) | |
258 | continue; | |
259 | ||
260 | if (!unit_match(de->d_name, matches)) | |
261 | continue; | |
262 | ||
263 | /* Filter out duplicates */ | |
264 | if (hashmap_get(unit_files, de->d_name)) | |
265 | continue; | |
266 | ||
61d0578b LP |
267 | if (!IN_SET(de->d_type, DT_LNK, DT_REG)) |
268 | continue; | |
269 | ||
270 | fd = openat(dirfd(d), de->d_name, O_CLOEXEC|O_RDONLY); | |
271 | if (fd < 0) { | |
272 | log_debug_errno(errno, "Failed to open unit file '%s', ignoring: %m", de->d_name); | |
273 | continue; | |
274 | } | |
275 | ||
409f4cf1 | 276 | #if HAVE_SELINUX |
2ef20244 LB |
277 | /* The units will be copied on the host's filesystem, so if they had a SELinux label |
278 | * we have to preserve it. Copy it out so that it can be applied later. */ | |
409f4cf1 | 279 | |
2ef20244 LB |
280 | r = fgetfilecon_raw(fd, &con); |
281 | if (r < 0 && errno != ENODATA) | |
282 | log_debug_errno(errno, "Failed to get SELinux file context from '%s', ignoring: %m", de->d_name); | |
409f4cf1 | 283 | #endif |
2ef20244 LB |
284 | |
285 | if (socket_fd >= 0) { | |
447d4687 LB |
286 | struct iovec iov[] = { |
287 | IOVEC_MAKE_STRING(de->d_name), | |
409f4cf1 LB |
288 | IOVEC_MAKE((char *)"\0", sizeof(char)), |
289 | IOVEC_MAKE_STRING(strempty(con)), | |
447d4687 LB |
290 | }; |
291 | ||
292 | r = send_one_fd_iov_with_data_fd(socket_fd, iov, ELEMENTSOF(iov), fd); | |
61d0578b LP |
293 | if (r < 0) |
294 | return log_debug_errno(r, "Failed to send unit metadata to parent: %m"); | |
295 | } | |
296 | ||
2ef20244 | 297 | m = portable_metadata_new(de->d_name, where, con, fd); |
61d0578b LP |
298 | if (!m) |
299 | return -ENOMEM; | |
300 | fd = -1; | |
301 | ||
657ee2d8 | 302 | m->source = path_join(resolved, de->d_name); |
61d0578b LP |
303 | if (!m->source) |
304 | return -ENOMEM; | |
305 | ||
306 | r = hashmap_put(unit_files, m->name, m); | |
307 | if (r < 0) | |
308 | return log_debug_errno(r, "Failed to add unit to hashmap: %m"); | |
309 | m = NULL; | |
310 | } | |
311 | } | |
312 | ||
313 | if (ret_os_release) | |
314 | *ret_os_release = TAKE_PTR(os_release); | |
315 | if (ret_unit_files) | |
316 | *ret_unit_files = TAKE_PTR(unit_files); | |
317 | ||
318 | return 0; | |
319 | } | |
320 | ||
321 | static int portable_extract_by_path( | |
322 | const char *path, | |
239ac0c7 | 323 | bool path_is_extension, |
61d0578b LP |
324 | char **matches, |
325 | PortableMetadata **ret_os_release, | |
326 | Hashmap **ret_unit_files, | |
327 | sd_bus_error *error) { | |
328 | ||
67818055 | 329 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b LP |
330 | _cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL; |
331 | _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; | |
332 | int r; | |
333 | ||
334 | assert(path); | |
335 | ||
e08f94ac | 336 | r = loop_device_make_by_path(path, O_RDONLY, LO_FLAGS_PARTSCAN, &d); |
61d0578b | 337 | if (r == -EISDIR) { |
2ef20244 LB |
338 | _cleanup_free_ char *image_name = NULL; |
339 | ||
61d0578b LP |
340 | /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory |
341 | * tree and not a raw device. It's easy then. */ | |
342 | ||
2ef20244 LB |
343 | r = path_extract_filename(path, &image_name); |
344 | if (r < 0) | |
345 | return log_error_errno(r, "Failed to extract image name from path '%s': %m", path); | |
346 | ||
347 | r = extract_now(path, matches, image_name, path_is_extension, -1, &os_release, &unit_files); | |
61d0578b LP |
348 | if (r < 0) |
349 | return r; | |
350 | ||
351 | } else if (r < 0) | |
7b87fe4c | 352 | return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path); |
61d0578b LP |
353 | else { |
354 | _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; | |
355 | _cleanup_(rmdir_and_freep) char *tmpdir = NULL; | |
356 | _cleanup_(close_pairp) int seq[2] = { -1, -1 }; | |
357 | _cleanup_(sigkill_waitp) pid_t child = 0; | |
358 | ||
359 | /* We now have a loopback block device, let's fork off a child in its own mount namespace, mount it | |
360 | * there, and extract the metadata we need. The metadata is sent from the child back to us. */ | |
361 | ||
362 | BLOCK_SIGNALS(SIGCHLD); | |
363 | ||
364 | r = mkdtemp_malloc("/tmp/inspect-XXXXXX", &tmpdir); | |
365 | if (r < 0) | |
366 | return log_debug_errno(r, "Failed to create temporary directory: %m"); | |
367 | ||
4b5de5dd LP |
368 | r = dissect_image( |
369 | d->fd, | |
370 | NULL, NULL, | |
a3642997 | 371 | d->diskseq, |
75dc190d | 372 | d->uevent_seqnum_not_before, |
4a62257d | 373 | d->timestamp_not_before, |
4b5de5dd LP |
374 | DISSECT_IMAGE_READ_ONLY | |
375 | DISSECT_IMAGE_GENERIC_ROOT | | |
376 | DISSECT_IMAGE_REQUIRE_ROOT | | |
377 | DISSECT_IMAGE_DISCARD_ON_LOOP | | |
378 | DISSECT_IMAGE_RELAX_VAR_CHECK | | |
379 | DISSECT_IMAGE_USR_NO_ROOT, | |
380 | &m); | |
61d0578b LP |
381 | if (r == -ENOPKG) |
382 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Couldn't identify a suitable partition table or file system in '%s'.", path); | |
383 | else if (r == -EADDRNOTAVAIL) | |
384 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No root partition for specified root hash found in '%s'.", path); | |
385 | else if (r == -ENOTUNIQ) | |
386 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Multiple suitable root partitions found in image '%s'.", path); | |
387 | else if (r == -ENXIO) | |
388 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No suitable root partition found in image '%s'.", path); | |
389 | else if (r == -EPROTONOSUPPORT) | |
390 | sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Device '%s' is loopback block device with partition scanning turned off, please turn it on.", path); | |
391 | if (r < 0) | |
392 | return r; | |
393 | ||
394 | if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0) | |
395 | return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m"); | |
396 | ||
397 | r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE|FORK_LOG, &child); | |
398 | if (r < 0) | |
399 | return r; | |
400 | if (r == 0) { | |
9ccb531a LB |
401 | DissectImageFlags flags = DISSECT_IMAGE_READ_ONLY; |
402 | ||
61d0578b LP |
403 | seq[0] = safe_close(seq[0]); |
404 | ||
239ac0c7 | 405 | if (path_is_extension) |
9ccb531a LB |
406 | flags |= DISSECT_IMAGE_VALIDATE_OS_EXT; |
407 | else | |
408 | flags |= DISSECT_IMAGE_VALIDATE_OS; | |
409 | ||
410 | r = dissected_image_mount(m, tmpdir, UID_INVALID, UID_INVALID, flags); | |
61d0578b LP |
411 | if (r < 0) { |
412 | log_debug_errno(r, "Failed to mount dissected image: %m"); | |
413 | goto child_finish; | |
414 | } | |
415 | ||
239ac0c7 | 416 | r = extract_now(tmpdir, matches, m->image_name, path_is_extension, seq[1], NULL, NULL); |
61d0578b LP |
417 | |
418 | child_finish: | |
419 | _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); | |
420 | } | |
421 | ||
422 | seq[1] = safe_close(seq[1]); | |
423 | ||
67818055 | 424 | unit_files = hashmap_new(&portable_metadata_hash_ops); |
61d0578b LP |
425 | if (!unit_files) |
426 | return -ENOMEM; | |
427 | ||
428 | for (;;) { | |
429 | _cleanup_(portable_metadata_unrefp) PortableMetadata *add = NULL; | |
61d0578b | 430 | _cleanup_close_ int fd = -1; |
409f4cf1 LB |
431 | /* We use NAME_MAX space for the SELinux label here. The kernel currently enforces no limit, but |
432 | * according to suggestions from the SELinux people this will change and it will probably be | |
433 | * identical to NAME_MAX. For now we use that, but this should be updated one day when the final | |
434 | * limit is known. */ | |
435 | char iov_buffer[PATH_MAX + NAME_MAX + 2]; | |
447d4687 | 436 | struct iovec iov = IOVEC_INIT(iov_buffer, sizeof(iov_buffer)); |
61d0578b | 437 | |
447d4687 LB |
438 | ssize_t n = receive_one_fd_iov(seq[0], &iov, 1, 0, &fd); |
439 | if (n == -EIO) | |
440 | break; | |
441 | if (n < 0) | |
442 | return log_debug_errno(n, "Failed to receive item: %m"); | |
443 | iov_buffer[n] = 0; | |
61d0578b | 444 | |
5238e957 | 445 | /* We can't really distinguish a zero-length datagram without any fds from EOF (both are signalled the |
61d0578b | 446 | * same way by recvmsg()). Hence, accept either as end notification. */ |
447d4687 | 447 | if (isempty(iov_buffer) && fd < 0) |
61d0578b LP |
448 | break; |
449 | ||
447d4687 | 450 | if (isempty(iov_buffer) || fd < 0) |
d7a0f1f4 FS |
451 | return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), |
452 | "Invalid item sent from child."); | |
61d0578b | 453 | |
409f4cf1 LB |
454 | /* Given recvmsg cannot be used with multiple io vectors if you don't know the size in advance, |
455 | * use a marker to separate the name and the optional SELinux context. */ | |
456 | char *selinux_label = memchr(iov_buffer, 0, n); | |
457 | assert(selinux_label); | |
458 | selinux_label++; | |
459 | ||
460 | add = portable_metadata_new(iov_buffer, path, selinux_label, fd); | |
61d0578b LP |
461 | if (!add) |
462 | return -ENOMEM; | |
463 | fd = -1; | |
464 | ||
465 | /* Note that we do not initialize 'add->source' here, as the source path is not usable here as | |
466 | * it refers to a path only valid in the short-living namespaced child process we forked | |
467 | * here. */ | |
468 | ||
469 | if (PORTABLE_METADATA_IS_UNIT(add)) { | |
470 | r = hashmap_put(unit_files, add->name, add); | |
471 | if (r < 0) | |
472 | return log_debug_errno(r, "Failed to add item to unit file list: %m"); | |
473 | ||
474 | add = NULL; | |
475 | ||
239ac0c7 | 476 | } else if (PORTABLE_METADATA_IS_OS_RELEASE(add) || PORTABLE_METADATA_IS_EXTENSION_RELEASE(add)) { |
61d0578b LP |
477 | |
478 | assert(!os_release); | |
479 | os_release = TAKE_PTR(add); | |
480 | } else | |
04499a70 | 481 | assert_not_reached(); |
61d0578b LP |
482 | } |
483 | ||
484 | r = wait_for_terminate_and_check("(sd-dissect)", child, 0); | |
485 | if (r < 0) | |
486 | return r; | |
487 | child = 0; | |
488 | } | |
489 | ||
239ac0c7 LB |
490 | if (!os_release) |
491 | return sd_bus_error_setf(error, | |
492 | SD_BUS_ERROR_INVALID_ARGS, | |
493 | "Image '%s' lacks %s data, refusing.", | |
494 | path, | |
495 | path_is_extension ? "extension-release" : "os-release"); | |
61d0578b | 496 | |
61d0578b LP |
497 | if (ret_unit_files) |
498 | *ret_unit_files = TAKE_PTR(unit_files); | |
499 | ||
500 | if (ret_os_release) | |
501 | *ret_os_release = TAKE_PTR(os_release); | |
502 | ||
503 | return 0; | |
504 | } | |
505 | ||
9ff61565 | 506 | static int extract_image_and_extensions( |
61d0578b LP |
507 | const char *name_or_path, |
508 | char **matches, | |
907952bb | 509 | char **extension_image_paths, |
9ff61565 LB |
510 | bool validate_sysext, |
511 | Image **ret_image, | |
512 | OrderedHashmap **ret_extension_images, | |
e3f7ed94 | 513 | OrderedHashmap **ret_extension_releases, |
61d0578b LP |
514 | PortableMetadata **ret_os_release, |
515 | Hashmap **ret_unit_files, | |
8a129c80 | 516 | char ***ret_valid_prefixes, |
61d0578b LP |
517 | sd_bus_error *error) { |
518 | ||
9ff61565 | 519 | _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL; |
907952bb | 520 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; |
e3f7ed94 | 521 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL; |
907952bb | 522 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
8a129c80 | 523 | _cleanup_strv_free_ char **valid_prefixes = NULL; |
61d0578b | 524 | _cleanup_(image_unrefp) Image *image = NULL; |
907952bb | 525 | Image *ext; |
61d0578b LP |
526 | int r; |
527 | ||
528 | assert(name_or_path); | |
9ff61565 | 529 | assert(matches); |
61d0578b | 530 | |
d577d4a4 | 531 | r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image); |
61d0578b LP |
532 | if (r < 0) |
533 | return r; | |
534 | ||
907952bb | 535 | if (!strv_isempty(extension_image_paths)) { |
907952bb LB |
536 | extension_images = ordered_hashmap_new(&image_hash_ops); |
537 | if (!extension_images) | |
538 | return -ENOMEM; | |
539 | ||
e3f7ed94 LB |
540 | if (ret_extension_releases) { |
541 | extension_releases = ordered_hashmap_new(&portable_metadata_hash_ops); | |
542 | if (!extension_releases) | |
543 | return -ENOMEM; | |
544 | } | |
545 | ||
907952bb LB |
546 | STRV_FOREACH(p, extension_image_paths) { |
547 | _cleanup_(image_unrefp) Image *new = NULL; | |
548 | ||
549 | r = image_find_harder(IMAGE_PORTABLE, *p, NULL, &new); | |
550 | if (r < 0) | |
551 | return r; | |
552 | ||
553 | r = ordered_hashmap_put(extension_images, new->name, new); | |
554 | if (r < 0) | |
555 | return r; | |
556 | TAKE_PTR(new); | |
557 | } | |
558 | } | |
559 | ||
239ac0c7 | 560 | r = portable_extract_by_path(image->path, /* path_is_extension= */ false, matches, &os_release, &unit_files, error); |
907952bb LB |
561 | if (r < 0) |
562 | return r; | |
563 | ||
8a129c80 LP |
564 | /* If we are layering extension images on top of a runtime image, check that the os-release and |
565 | * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when | |
566 | * the units are started. Also, collect valid portable prefixes if caller requested that. */ | |
567 | if (validate_sysext || ret_valid_prefixes) { | |
9ff61565 | 568 | _cleanup_fclose_ FILE *f = NULL; |
8a129c80 | 569 | _cleanup_free_ char *prefixes = NULL; |
9ff61565 LB |
570 | |
571 | r = take_fdopen_unlocked(&os_release->fd, "r", &f); | |
572 | if (r < 0) | |
573 | return r; | |
574 | ||
575 | r = parse_env_file(f, os_release->name, | |
576 | "ID", &id, | |
577 | "VERSION_ID", &version_id, | |
8a129c80 LP |
578 | "SYSEXT_LEVEL", &sysext_level, |
579 | "PORTABLE_PREFIXES", &prefixes); | |
9ff61565 LB |
580 | if (r < 0) |
581 | return r; | |
8a129c80 LP |
582 | |
583 | if (prefixes) { | |
584 | valid_prefixes = strv_split(prefixes, WHITESPACE); | |
585 | if (!valid_prefixes) | |
586 | return -ENOMEM; | |
587 | } | |
9ff61565 LB |
588 | } |
589 | ||
907952bb | 590 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { |
9ff61565 | 591 | _cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL; |
907952bb | 592 | _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; |
9ff61565 | 593 | _cleanup_strv_free_ char **extension_release = NULL; |
e3f7ed94 | 594 | _cleanup_close_ int extension_release_fd = -1; |
9ff61565 | 595 | _cleanup_fclose_ FILE *f = NULL; |
8a129c80 | 596 | const char *e; |
907952bb | 597 | |
9ff61565 | 598 | r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, matches, &extension_release_meta, &extra_unit_files, error); |
907952bb LB |
599 | if (r < 0) |
600 | return r; | |
9ff61565 | 601 | |
907952bb LB |
602 | r = hashmap_move(unit_files, extra_unit_files); |
603 | if (r < 0) | |
604 | return r; | |
9ff61565 | 605 | |
e3f7ed94 | 606 | if (!validate_sysext && !ret_valid_prefixes && !ret_extension_releases) |
9ff61565 LB |
607 | continue; |
608 | ||
e3f7ed94 | 609 | /* We need to keep the fd valid, to return the PortableMetadata to the caller. */ |
1d2bf1f0 | 610 | extension_release_fd = fd_reopen(extension_release_meta->fd, O_CLOEXEC|O_RDONLY); |
e3f7ed94 LB |
611 | if (extension_release_fd < 0) |
612 | return extension_release_fd; | |
613 | ||
614 | r = take_fdopen_unlocked(&extension_release_fd, "r", &f); | |
9ff61565 LB |
615 | if (r < 0) |
616 | return r; | |
617 | ||
618 | r = load_env_file_pairs(f, extension_release_meta->name, &extension_release); | |
619 | if (r < 0) | |
620 | return r; | |
621 | ||
8a129c80 LP |
622 | if (validate_sysext) { |
623 | r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release); | |
624 | if (r == 0) | |
625 | return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path); | |
626 | if (r < 0) | |
627 | return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path); | |
628 | } | |
629 | ||
630 | e = strv_env_pairs_get(extension_release, "PORTABLE_PREFIXES"); | |
631 | if (e) { | |
632 | _cleanup_strv_free_ char **l = NULL; | |
633 | ||
634 | l = strv_split(e, WHITESPACE); | |
635 | if (!l) | |
636 | return -ENOMEM; | |
637 | ||
638 | r = strv_extend_strv(&valid_prefixes, l, true); | |
639 | if (r < 0) | |
640 | return r; | |
641 | } | |
e3f7ed94 LB |
642 | |
643 | if (ret_extension_releases) { | |
644 | r = ordered_hashmap_put(extension_releases, ext->name, extension_release_meta); | |
645 | if (r < 0) | |
646 | return r; | |
647 | TAKE_PTR(extension_release_meta); | |
648 | } | |
907952bb LB |
649 | } |
650 | ||
8a129c80 LP |
651 | strv_sort(valid_prefixes); |
652 | ||
653 | if (ret_image) | |
654 | *ret_image = TAKE_PTR(image); | |
655 | if (ret_extension_images) | |
656 | *ret_extension_images = TAKE_PTR(extension_images); | |
e3f7ed94 LB |
657 | if (ret_extension_releases) |
658 | *ret_extension_releases = TAKE_PTR(extension_releases); | |
9ff61565 LB |
659 | if (ret_os_release) |
660 | *ret_os_release = TAKE_PTR(os_release); | |
661 | if (ret_unit_files) | |
662 | *ret_unit_files = TAKE_PTR(unit_files); | |
8a129c80 LP |
663 | if (ret_valid_prefixes) |
664 | *ret_valid_prefixes = TAKE_PTR(valid_prefixes); | |
9ff61565 LB |
665 | |
666 | return 0; | |
667 | } | |
668 | ||
669 | int portable_extract( | |
670 | const char *name_or_path, | |
671 | char **matches, | |
672 | char **extension_image_paths, | |
673 | PortableMetadata **ret_os_release, | |
e3f7ed94 | 674 | OrderedHashmap **ret_extension_releases, |
9ff61565 | 675 | Hashmap **ret_unit_files, |
8a129c80 | 676 | char ***ret_valid_prefixes, |
9ff61565 LB |
677 | sd_bus_error *error) { |
678 | ||
679 | _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; | |
e3f7ed94 | 680 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL; |
9ff61565 | 681 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
8a129c80 | 682 | _cleanup_(strv_freep) char **valid_prefixes = NULL; |
9ff61565 LB |
683 | _cleanup_(image_unrefp) Image *image = NULL; |
684 | int r; | |
685 | ||
8a129c80 LP |
686 | assert(name_or_path); |
687 | ||
688 | r = extract_image_and_extensions( | |
689 | name_or_path, | |
690 | matches, | |
691 | extension_image_paths, | |
692 | /* validate_sysext= */ false, | |
693 | &image, | |
694 | &extension_images, | |
e3f7ed94 | 695 | &extension_releases, |
8a129c80 LP |
696 | &os_release, |
697 | &unit_files, | |
698 | ret_valid_prefixes ? &valid_prefixes : NULL, | |
699 | error); | |
9ff61565 LB |
700 | if (r < 0) |
701 | return r; | |
702 | ||
7bf5ec45 LB |
703 | if (hashmap_isempty(unit_files)) { |
704 | _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); | |
705 | if (!extensions) | |
706 | return -ENOMEM; | |
707 | ||
708 | return sd_bus_error_setf(error, | |
709 | SD_BUS_ERROR_INVALID_ARGS, | |
710 | "Couldn't find any matching unit files in image '%s%s%s', refusing.", | |
711 | image->path, | |
712 | isempty(extensions) ? "" : "' or any of its extensions '", | |
713 | isempty(extensions) ? "" : extensions); | |
714 | } | |
715 | ||
8a129c80 LP |
716 | if (ret_os_release) |
717 | *ret_os_release = TAKE_PTR(os_release); | |
e3f7ed94 LB |
718 | if (ret_extension_releases) |
719 | *ret_extension_releases = TAKE_PTR(extension_releases); | |
8a129c80 LP |
720 | if (ret_unit_files) |
721 | *ret_unit_files = TAKE_PTR(unit_files); | |
722 | if (ret_valid_prefixes) | |
723 | *ret_valid_prefixes = TAKE_PTR(valid_prefixes); | |
907952bb LB |
724 | |
725 | return 0; | |
61d0578b LP |
726 | } |
727 | ||
728 | static int unit_file_is_active( | |
729 | sd_bus *bus, | |
730 | const char *name, | |
731 | sd_bus_error *error) { | |
732 | ||
733 | static const char *const active_states[] = { | |
734 | "activating", | |
735 | "active", | |
736 | "reloading", | |
737 | "deactivating", | |
738 | NULL, | |
739 | }; | |
740 | int r; | |
741 | ||
742 | if (!bus) | |
743 | return false; | |
744 | ||
745 | /* If we are looking at a plain or instance things are easy, we can just query the state */ | |
746 | if (unit_name_is_valid(name, UNIT_NAME_PLAIN|UNIT_NAME_INSTANCE)) { | |
747 | _cleanup_free_ char *path = NULL, *buf = NULL; | |
748 | ||
749 | path = unit_dbus_path_from_name(name); | |
750 | if (!path) | |
751 | return -ENOMEM; | |
752 | ||
753 | r = sd_bus_get_property_string( | |
754 | bus, | |
755 | "org.freedesktop.systemd1", | |
756 | path, | |
757 | "org.freedesktop.systemd1.Unit", | |
758 | "ActiveState", | |
759 | error, | |
760 | &buf); | |
761 | if (r < 0) | |
762 | return log_debug_errno(r, "Failed to retrieve unit state: %s", bus_error_message(error, r)); | |
763 | ||
764 | return strv_contains((char**) active_states, buf); | |
765 | } | |
766 | ||
767 | /* Otherwise we need to enumerate. But let's build the most restricted query we can */ | |
768 | if (unit_name_is_valid(name, UNIT_NAME_TEMPLATE)) { | |
769 | _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; | |
770 | const char *at, *prefix, *joined; | |
771 | ||
772 | r = sd_bus_message_new_method_call( | |
773 | bus, | |
774 | &m, | |
775 | "org.freedesktop.systemd1", | |
776 | "/org/freedesktop/systemd1", | |
777 | "org.freedesktop.systemd1.Manager", | |
778 | "ListUnitsByPatterns"); | |
779 | if (r < 0) | |
780 | return r; | |
781 | ||
782 | r = sd_bus_message_append_strv(m, (char**) active_states); | |
783 | if (r < 0) | |
784 | return r; | |
785 | ||
786 | at = strchr(name, '@'); | |
787 | assert(at); | |
788 | ||
2f82562b | 789 | prefix = strndupa_safe(name, at + 1 - name); |
61d0578b LP |
790 | joined = strjoina(prefix, "*", at + 1); |
791 | ||
792 | r = sd_bus_message_append_strv(m, STRV_MAKE(joined)); | |
793 | if (r < 0) | |
794 | return r; | |
795 | ||
796 | r = sd_bus_call(bus, m, 0, error, &reply); | |
797 | if (r < 0) | |
798 | return log_debug_errno(r, "Failed to list units: %s", bus_error_message(error, r)); | |
799 | ||
800 | r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)"); | |
801 | if (r < 0) | |
802 | return r; | |
803 | ||
804 | r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_STRUCT, "ssssssouso"); | |
805 | if (r < 0) | |
806 | return r; | |
807 | ||
808 | return r > 0; | |
809 | } | |
810 | ||
811 | return -EINVAL; | |
812 | } | |
813 | ||
814 | static int portable_changes_add( | |
815 | PortableChange **changes, | |
816 | size_t *n_changes, | |
ba5b6c59 | 817 | int type_or_errno, /* PORTABLE_COPY, PORTABLE_SYMLINK, … if positive, or errno if negative */ |
61d0578b LP |
818 | const char *path, |
819 | const char *source) { | |
820 | ||
821 | _cleanup_free_ char *p = NULL, *s = NULL; | |
822 | PortableChange *c; | |
823 | ||
824 | assert(path); | |
825 | assert(!changes == !n_changes); | |
826 | ||
ba5b6c59 LP |
827 | if (type_or_errno >= 0) |
828 | assert(type_or_errno < _PORTABLE_CHANGE_TYPE_MAX); | |
829 | else | |
830 | assert(type_or_errno >= -ERRNO_MAX); | |
831 | ||
61d0578b LP |
832 | if (!changes) |
833 | return 0; | |
834 | ||
835 | c = reallocarray(*changes, *n_changes + 1, sizeof(PortableChange)); | |
836 | if (!c) | |
837 | return -ENOMEM; | |
838 | *changes = c; | |
839 | ||
840 | p = strdup(path); | |
841 | if (!p) | |
842 | return -ENOMEM; | |
843 | ||
4ff361cc | 844 | path_simplify(p); |
61d0578b LP |
845 | |
846 | if (source) { | |
847 | s = strdup(source); | |
848 | if (!s) | |
849 | return -ENOMEM; | |
850 | ||
4ff361cc | 851 | path_simplify(s); |
61d0578b LP |
852 | } |
853 | ||
854 | c[(*n_changes)++] = (PortableChange) { | |
ba5b6c59 | 855 | .type_or_errno = type_or_errno, |
61d0578b LP |
856 | .path = TAKE_PTR(p), |
857 | .source = TAKE_PTR(s), | |
858 | }; | |
859 | ||
860 | return 0; | |
861 | } | |
862 | ||
863 | static int portable_changes_add_with_prefix( | |
864 | PortableChange **changes, | |
865 | size_t *n_changes, | |
ba5b6c59 | 866 | int type_or_errno, |
61d0578b LP |
867 | const char *prefix, |
868 | const char *path, | |
869 | const char *source) { | |
870 | ||
871 | assert(path); | |
872 | assert(!changes == !n_changes); | |
873 | ||
874 | if (!changes) | |
875 | return 0; | |
876 | ||
877 | if (prefix) { | |
270384b2 | 878 | path = prefix_roota(prefix, path); |
61d0578b LP |
879 | |
880 | if (source) | |
270384b2 | 881 | source = prefix_roota(prefix, source); |
61d0578b LP |
882 | } |
883 | ||
ba5b6c59 | 884 | return portable_changes_add(changes, n_changes, type_or_errno, path, source); |
61d0578b LP |
885 | } |
886 | ||
887 | void portable_changes_free(PortableChange *changes, size_t n_changes) { | |
888 | size_t i; | |
889 | ||
890 | assert(changes || n_changes == 0); | |
891 | ||
892 | for (i = 0; i < n_changes; i++) { | |
893 | free(changes[i].path); | |
894 | free(changes[i].source); | |
895 | } | |
896 | ||
897 | free(changes); | |
898 | } | |
899 | ||
907952bb LB |
900 | static const char *root_setting_from_image(ImageType type) { |
901 | return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "RootDirectory=" : "RootImage="; | |
902 | } | |
903 | ||
2ef20244 LB |
904 | static const char *extension_setting_from_image(ImageType type) { |
905 | return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "ExtensionDirectories=" : "ExtensionImages="; | |
906 | } | |
907 | ||
907952bb LB |
908 | static int make_marker_text(const char *image_path, OrderedHashmap *extension_images, char **ret_text) { |
909 | _cleanup_free_ char *text = NULL, *escaped_image_path = NULL; | |
910 | Image *ext; | |
911 | ||
912 | assert(image_path); | |
913 | assert(ret_text); | |
914 | ||
915 | escaped_image_path = xescape(image_path, ":"); | |
916 | if (!escaped_image_path) | |
917 | return -ENOMEM; | |
918 | ||
919 | /* If the image is layered, include all layers in the marker as a colon-separated | |
920 | * list of paths, so that we can do exact matches on removal. */ | |
921 | text = strjoin(PORTABLE_DROPIN_MARKER_BEGIN, escaped_image_path); | |
922 | if (!text) | |
923 | return -ENOMEM; | |
924 | ||
925 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { | |
926 | _cleanup_free_ char *escaped = NULL; | |
927 | ||
928 | escaped = xescape(ext->path, ":"); | |
929 | if (!escaped) | |
930 | return -ENOMEM; | |
931 | ||
932 | if (!strextend(&text, ":", escaped)) | |
933 | return -ENOMEM; | |
934 | } | |
935 | ||
936 | if (!strextend(&text, PORTABLE_DROPIN_MARKER_END "\n")) | |
937 | return -ENOMEM; | |
938 | ||
939 | *ret_text = TAKE_PTR(text); | |
940 | return 0; | |
941 | } | |
942 | ||
61d0578b LP |
943 | static int install_chroot_dropin( |
944 | const char *image_path, | |
945 | ImageType type, | |
907952bb | 946 | OrderedHashmap *extension_images, |
61d0578b LP |
947 | const PortableMetadata *m, |
948 | const char *dropin_dir, | |
949 | char **ret_dropin, | |
950 | PortableChange **changes, | |
951 | size_t *n_changes) { | |
952 | ||
953 | _cleanup_free_ char *text = NULL, *dropin = NULL; | |
954 | int r; | |
955 | ||
956 | assert(image_path); | |
957 | assert(m); | |
958 | assert(dropin_dir); | |
959 | ||
657ee2d8 | 960 | dropin = path_join(dropin_dir, "20-portable.conf"); |
61d0578b LP |
961 | if (!dropin) |
962 | return -ENOMEM; | |
963 | ||
907952bb LB |
964 | r = make_marker_text(image_path, extension_images, &text); |
965 | if (r < 0) | |
966 | return log_debug_errno(r, "Failed to generate marker string for portable drop-in: %m"); | |
61d0578b | 967 | |
62b0ee9e | 968 | if (endswith(m->name, ".service")) { |
907952bb LB |
969 | const char *os_release_source, *root_type; |
970 | _cleanup_free_ char *base_name = NULL; | |
2ef20244 | 971 | Image *ext; |
907952bb LB |
972 | |
973 | root_type = root_setting_from_image(type); | |
62b0ee9e LB |
974 | |
975 | if (access("/etc/os-release", F_OK) < 0) { | |
976 | if (errno != ENOENT) | |
977 | return log_debug_errno(errno, "Failed to check if /etc/os-release exists: %m"); | |
978 | ||
979 | os_release_source = "/usr/lib/os-release"; | |
980 | } else | |
981 | os_release_source = "/etc/os-release"; | |
982 | ||
907952bb LB |
983 | r = path_extract_filename(m->image_path ?: image_path, &base_name); |
984 | if (r < 0) | |
985 | return log_debug_errno(r, "Failed to extract basename from '%s': %m", m->image_path ?: image_path); | |
986 | ||
61d0578b LP |
987 | if (!strextend(&text, |
988 | "\n" | |
989 | "[Service]\n", | |
907952bb LB |
990 | root_type, image_path, "\n" |
991 | "Environment=PORTABLE=", base_name, "\n" | |
62b0ee9e | 992 | "BindReadOnlyPaths=", os_release_source, ":/run/host/os-release\n" |
907952bb | 993 | "LogExtraFields=PORTABLE=", base_name, "\n")) |
61d0578b | 994 | return -ENOMEM; |
907952bb LB |
995 | |
996 | if (m->image_path && !path_equal(m->image_path, image_path)) | |
997 | ORDERED_HASHMAP_FOREACH(ext, extension_images) | |
2ef20244 | 998 | if (!strextend(&text, extension_setting_from_image(ext->type), ext->path, "\n")) |
907952bb | 999 | return -ENOMEM; |
62b0ee9e | 1000 | } |
61d0578b LP |
1001 | |
1002 | r = write_string_file(dropin, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); | |
1003 | if (r < 0) | |
1004 | return log_debug_errno(r, "Failed to write '%s': %m", dropin); | |
1005 | ||
1006 | (void) portable_changes_add(changes, n_changes, PORTABLE_WRITE, dropin, NULL); | |
1007 | ||
1008 | if (ret_dropin) | |
1009 | *ret_dropin = TAKE_PTR(dropin); | |
1010 | ||
1011 | return 0; | |
1012 | } | |
1013 | ||
61d0578b LP |
1014 | static int install_profile_dropin( |
1015 | const char *image_path, | |
1016 | const PortableMetadata *m, | |
1017 | const char *dropin_dir, | |
1018 | const char *profile, | |
1019 | PortableFlags flags, | |
1020 | char **ret_dropin, | |
1021 | PortableChange **changes, | |
1022 | size_t *n_changes) { | |
1023 | ||
1024 | _cleanup_free_ char *dropin = NULL, *from = NULL; | |
1025 | int r; | |
1026 | ||
1027 | assert(image_path); | |
1028 | assert(m); | |
1029 | assert(dropin_dir); | |
1030 | ||
1031 | if (!profile) | |
1032 | return 0; | |
1033 | ||
13c02e7b | 1034 | r = find_portable_profile(profile, m->name, &from); |
61d0578b | 1035 | if (r < 0) { |
99c89da0 | 1036 | if (r != -ENOENT) |
61d0578b LP |
1037 | return log_debug_errno(errno, "Profile '%s' is not accessible: %m", profile); |
1038 | ||
1039 | log_debug_errno(errno, "Skipping link to profile '%s', as it does not exist: %m", profile); | |
1040 | return 0; | |
1041 | } | |
1042 | ||
657ee2d8 | 1043 | dropin = path_join(dropin_dir, "10-profile.conf"); |
61d0578b LP |
1044 | if (!dropin) |
1045 | return -ENOMEM; | |
1046 | ||
1047 | if (flags & PORTABLE_PREFER_COPY) { | |
1048 | ||
8a016c74 | 1049 | r = copy_file_atomic(from, dropin, 0644, 0, 0, COPY_REFLINK); |
61d0578b | 1050 | if (r < 0) |
fc03e80c | 1051 | return log_debug_errno(r, "Failed to copy %s %s %s: %m", from, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), dropin); |
61d0578b LP |
1052 | |
1053 | (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, dropin, from); | |
1054 | ||
1055 | } else { | |
1056 | ||
1057 | if (symlink(from, dropin) < 0) | |
fc03e80c | 1058 | return log_debug_errno(errno, "Failed to link %s %s %s: %m", from, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), dropin); |
61d0578b LP |
1059 | |
1060 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, dropin, from); | |
1061 | } | |
1062 | ||
1063 | if (ret_dropin) | |
1064 | *ret_dropin = TAKE_PTR(dropin); | |
1065 | ||
1066 | return 0; | |
1067 | } | |
1068 | ||
40a7b232 | 1069 | static const char *attached_path(const LookupPaths *paths, PortableFlags flags) { |
61d0578b LP |
1070 | const char *where; |
1071 | ||
1072 | assert(paths); | |
1073 | ||
1074 | if (flags & PORTABLE_RUNTIME) | |
40a7b232 | 1075 | where = paths->runtime_attached; |
61d0578b | 1076 | else |
40a7b232 | 1077 | where = paths->persistent_attached; |
61d0578b LP |
1078 | |
1079 | assert(where); | |
1080 | return where; | |
1081 | } | |
1082 | ||
1083 | static int attach_unit_file( | |
1084 | const LookupPaths *paths, | |
1085 | const char *image_path, | |
1086 | ImageType type, | |
907952bb | 1087 | OrderedHashmap *extension_images, |
61d0578b LP |
1088 | const PortableMetadata *m, |
1089 | const char *profile, | |
1090 | PortableFlags flags, | |
1091 | PortableChange **changes, | |
1092 | size_t *n_changes) { | |
1093 | ||
1094 | _cleanup_(unlink_and_freep) char *chroot_dropin = NULL, *profile_dropin = NULL; | |
1095 | _cleanup_(rmdir_and_freep) char *dropin_dir = NULL; | |
1096 | const char *where, *path; | |
1097 | int r; | |
1098 | ||
1099 | assert(paths); | |
1100 | assert(image_path); | |
1101 | assert(m); | |
1102 | assert(PORTABLE_METADATA_IS_UNIT(m)); | |
1103 | ||
40a7b232 | 1104 | where = attached_path(paths, flags); |
61d0578b | 1105 | |
d09d85a2 LP |
1106 | (void) mkdir_parents(where, 0755); |
1107 | if (mkdir(where, 0755) < 0) { | |
1108 | if (errno != EEXIST) | |
1109 | return -errno; | |
1110 | } else | |
1111 | (void) portable_changes_add(changes, n_changes, PORTABLE_MKDIR, where, NULL); | |
1112 | ||
270384b2 | 1113 | path = prefix_roota(where, m->name); |
61d0578b LP |
1114 | dropin_dir = strjoin(path, ".d"); |
1115 | if (!dropin_dir) | |
1116 | return -ENOMEM; | |
1117 | ||
d09d85a2 LP |
1118 | if (mkdir(dropin_dir, 0755) < 0) { |
1119 | if (errno != EEXIST) | |
1120 | return -errno; | |
1121 | } else | |
1122 | (void) portable_changes_add(changes, n_changes, PORTABLE_MKDIR, dropin_dir, NULL); | |
61d0578b LP |
1123 | |
1124 | /* We install the drop-ins first, and the actual unit file last to achieve somewhat atomic behaviour if PID 1 | |
1125 | * is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at | |
1126 | * all for PID 1. */ | |
1127 | ||
907952bb | 1128 | r = install_chroot_dropin(image_path, type, extension_images, m, dropin_dir, &chroot_dropin, changes, n_changes); |
61d0578b LP |
1129 | if (r < 0) |
1130 | return r; | |
1131 | ||
1132 | r = install_profile_dropin(image_path, m, dropin_dir, profile, flags, &profile_dropin, changes, n_changes); | |
1133 | if (r < 0) | |
1134 | return r; | |
1135 | ||
1136 | if ((flags & PORTABLE_PREFER_SYMLINK) && m->source) { | |
1137 | ||
1138 | if (symlink(m->source, path) < 0) | |
1139 | return log_debug_errno(errno, "Failed to symlink unit file '%s': %m", path); | |
1140 | ||
1141 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, path, m->source); | |
1142 | ||
1143 | } else { | |
1144 | _cleanup_(unlink_and_freep) char *tmp = NULL; | |
1145 | _cleanup_close_ int fd = -1; | |
1146 | ||
409f4cf1 LB |
1147 | (void) mac_selinux_create_file_prepare_label(path, m->selinux_label); |
1148 | ||
6d88513e | 1149 | fd = open_tmpfile_linkable(path, O_WRONLY|O_CLOEXEC, &tmp); |
409f4cf1 | 1150 | mac_selinux_create_file_clear(); /* Clear immediately in case of errors */ |
61d0578b LP |
1151 | if (fd < 0) |
1152 | return log_debug_errno(fd, "Failed to create unit file '%s': %m", path); | |
1153 | ||
1154 | r = copy_bytes(m->fd, fd, UINT64_MAX, COPY_REFLINK); | |
1155 | if (r < 0) | |
1156 | return log_debug_errno(r, "Failed to copy unit file '%s': %m", path); | |
1157 | ||
1158 | if (fchmod(fd, 0644) < 0) | |
1159 | return log_debug_errno(errno, "Failed to change unit file access mode for '%s': %m", path); | |
1160 | ||
1161 | r = link_tmpfile(fd, tmp, path); | |
1162 | if (r < 0) | |
1163 | return log_debug_errno(r, "Failed to install unit file '%s': %m", path); | |
1164 | ||
1165 | tmp = mfree(tmp); | |
1166 | ||
1167 | (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, path, m->source); | |
1168 | } | |
1169 | ||
1170 | /* All is established now, now let's disable any rollbacks */ | |
1171 | chroot_dropin = mfree(chroot_dropin); | |
1172 | profile_dropin = mfree(profile_dropin); | |
1173 | dropin_dir = mfree(dropin_dir); | |
1174 | ||
1175 | return 0; | |
1176 | } | |
1177 | ||
1178 | static int image_symlink( | |
1179 | const char *image_path, | |
1180 | PortableFlags flags, | |
1181 | char **ret) { | |
1182 | ||
1183 | const char *fn, *where; | |
1184 | char *joined = NULL; | |
1185 | ||
1186 | assert(image_path); | |
1187 | assert(ret); | |
1188 | ||
1189 | fn = last_path_component(image_path); | |
1190 | ||
1191 | if (flags & PORTABLE_RUNTIME) | |
1192 | where = "/run/portables/"; | |
1193 | else | |
1194 | where = "/etc/portables/"; | |
1195 | ||
1196 | joined = strjoin(where, fn); | |
1197 | if (!joined) | |
1198 | return -ENOMEM; | |
1199 | ||
1200 | *ret = joined; | |
1201 | return 0; | |
1202 | } | |
1203 | ||
1204 | static int install_image_symlink( | |
1205 | const char *image_path, | |
1206 | PortableFlags flags, | |
1207 | PortableChange **changes, | |
1208 | size_t *n_changes) { | |
1209 | ||
1210 | _cleanup_free_ char *sl = NULL; | |
1211 | int r; | |
1212 | ||
1213 | assert(image_path); | |
1214 | ||
1215 | /* If the image is outside of the image search also link it into it, so that it can be found with short image | |
1216 | * names and is listed among the images. */ | |
1217 | ||
d577d4a4 | 1218 | if (image_in_search_path(IMAGE_PORTABLE, NULL, image_path)) |
61d0578b LP |
1219 | return 0; |
1220 | ||
1221 | r = image_symlink(image_path, flags, &sl); | |
1222 | if (r < 0) | |
1223 | return log_debug_errno(r, "Failed to generate image symlink path: %m"); | |
1224 | ||
1225 | (void) mkdir_parents(sl, 0755); | |
1226 | ||
1227 | if (symlink(image_path, sl) < 0) | |
fc03e80c | 1228 | return log_debug_errno(errno, "Failed to link %s %s %s: %m", image_path, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), sl); |
61d0578b LP |
1229 | |
1230 | (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, sl, image_path); | |
1231 | return 0; | |
1232 | } | |
1233 | ||
907952bb LB |
1234 | static int install_image_and_extensions_symlinks( |
1235 | const Image *image, | |
1236 | OrderedHashmap *extension_images, | |
1237 | PortableFlags flags, | |
1238 | PortableChange **changes, | |
1239 | size_t *n_changes) { | |
1240 | ||
1241 | Image *ext; | |
1242 | int r; | |
1243 | ||
1244 | assert(image); | |
1245 | ||
1246 | ORDERED_HASHMAP_FOREACH(ext, extension_images) { | |
1247 | r = install_image_symlink(ext->path, flags, changes, n_changes); | |
1248 | if (r < 0) | |
1249 | return r; | |
1250 | } | |
1251 | ||
1252 | r = install_image_symlink(image->path, flags, changes, n_changes); | |
1253 | if (r < 0) | |
1254 | return r; | |
1255 | ||
1256 | return 0; | |
1257 | } | |
1258 | ||
8a129c80 | 1259 | static bool prefix_matches_compatible(char **matches, char **valid_prefixes) { |
8a129c80 LP |
1260 | /* Checks if all 'matches' are included in the list of 'valid_prefixes' */ |
1261 | ||
1262 | STRV_FOREACH(m, matches) | |
1263 | if (!strv_contains(valid_prefixes, *m)) | |
1264 | return false; | |
1265 | ||
1266 | return true; | |
1267 | } | |
1268 | ||
61d0578b LP |
1269 | int portable_attach( |
1270 | sd_bus *bus, | |
1271 | const char *name_or_path, | |
1272 | char **matches, | |
1273 | const char *profile, | |
907952bb | 1274 | char **extension_image_paths, |
61d0578b LP |
1275 | PortableFlags flags, |
1276 | PortableChange **changes, | |
1277 | size_t *n_changes, | |
1278 | sd_bus_error *error) { | |
1279 | ||
907952bb | 1280 | _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL; |
67818055 | 1281 | _cleanup_hashmap_free_ Hashmap *unit_files = NULL; |
61d0578b | 1282 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; |
8a129c80 | 1283 | _cleanup_strv_free_ char **valid_prefixes = NULL; |
61d0578b LP |
1284 | _cleanup_(image_unrefp) Image *image = NULL; |
1285 | PortableMetadata *item; | |
61d0578b LP |
1286 | int r; |
1287 | ||
8a129c80 LP |
1288 | r = extract_image_and_extensions( |
1289 | name_or_path, | |
1290 | matches, | |
1291 | extension_image_paths, | |
1292 | /* validate_sysext= */ true, | |
1293 | &image, | |
1294 | &extension_images, | |
e3f7ed94 | 1295 | /* extension_releases= */ NULL, |
8a129c80 LP |
1296 | /* os_release= */ NULL, |
1297 | &unit_files, | |
1298 | &valid_prefixes, | |
1299 | error); | |
61d0578b LP |
1300 | if (r < 0) |
1301 | return r; | |
1302 | ||
8a129c80 LP |
1303 | if (valid_prefixes && !prefix_matches_compatible(matches, valid_prefixes)) { |
1304 | _cleanup_free_ char *matches_joined = NULL, *extensions_joined = NULL, *valid_prefixes_joined = NULL; | |
1305 | ||
1306 | matches_joined = strv_join(matches, "', '"); | |
1307 | if (!matches_joined) | |
1308 | return -ENOMEM; | |
1309 | ||
1310 | extensions_joined = strv_join(extension_image_paths, ", "); | |
1311 | if (!extensions_joined) | |
1312 | return -ENOMEM; | |
1313 | ||
1314 | valid_prefixes_joined = strv_join(valid_prefixes, ", "); | |
1315 | if (!valid_prefixes_joined) | |
1316 | return -ENOMEM; | |
1317 | ||
1318 | return sd_bus_error_setf( | |
1319 | error, | |
1320 | SD_BUS_ERROR_INVALID_ARGS, | |
1321 | "Selected matches '%s' are not compatible with portable service image '%s%s%s', refusing. (Acceptable prefix matches are: %s)", | |
1322 | matches_joined, | |
1323 | image->path, | |
1324 | isempty(extensions_joined) ? "" : "' or any of its extensions '", | |
1325 | strempty(extensions_joined), | |
1326 | valid_prefixes_joined); | |
1327 | } | |
1328 | ||
7bf5ec45 | 1329 | if (hashmap_isempty(unit_files)) { |
8a129c80 LP |
1330 | _cleanup_free_ char *extensions_joined = strv_join(extension_image_paths, ", "); |
1331 | if (!extensions_joined) | |
7bf5ec45 LB |
1332 | return -ENOMEM; |
1333 | ||
8a129c80 LP |
1334 | return sd_bus_error_setf( |
1335 | error, | |
1336 | SD_BUS_ERROR_INVALID_ARGS, | |
1337 | "Couldn't find any matching unit files in image '%s%s%s', refusing.", | |
1338 | image->path, | |
1339 | isempty(extensions_joined) ? "" : "' or any of its extensions '", | |
1340 | strempty(extensions_joined)); | |
7bf5ec45 LB |
1341 | } |
1342 | ||
61d0578b LP |
1343 | r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); |
1344 | if (r < 0) | |
1345 | return r; | |
1346 | ||
90e74a66 | 1347 | HASHMAP_FOREACH(item, unit_files) { |
61d0578b LP |
1348 | r = unit_file_exists(UNIT_FILE_SYSTEM, &paths, item->name); |
1349 | if (r < 0) | |
1350 | return sd_bus_error_set_errnof(error, r, "Failed to determine whether unit '%s' exists on the host: %m", item->name); | |
e26fe5f9 | 1351 | if (!FLAGS_SET(flags, PORTABLE_REATTACH) && r > 0) |
61d0578b LP |
1352 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' exists on the host already, refusing.", item->name); |
1353 | ||
1354 | r = unit_file_is_active(bus, item->name, error); | |
1355 | if (r < 0) | |
1356 | return r; | |
e26fe5f9 | 1357 | if (!FLAGS_SET(flags, PORTABLE_REATTACH) && r > 0) |
61d0578b LP |
1358 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active already, refusing.", item->name); |
1359 | } | |
1360 | ||
90e74a66 | 1361 | HASHMAP_FOREACH(item, unit_files) { |
907952bb LB |
1362 | r = attach_unit_file(&paths, image->path, image->type, extension_images, |
1363 | item, profile, flags, changes, n_changes); | |
61d0578b LP |
1364 | if (r < 0) |
1365 | return r; | |
1366 | } | |
1367 | ||
1368 | /* We don't care too much for the image symlink, it's just a convenience thing, it's not necessary for proper | |
1369 | * operation otherwise. */ | |
907952bb | 1370 | (void) install_image_and_extensions_symlinks(image, extension_images, flags, changes, n_changes); |
61d0578b LP |
1371 | |
1372 | return 0; | |
1373 | } | |
1374 | ||
907952bb LB |
1375 | static bool marker_matches_images(const char *marker, const char *name_or_path, char **extension_image_paths) { |
1376 | _cleanup_strv_free_ char **root_and_extensions = NULL; | |
61d0578b | 1377 | const char *a; |
907952bb | 1378 | int r; |
61d0578b LP |
1379 | |
1380 | assert(marker); | |
1381 | assert(name_or_path); | |
1382 | ||
907952bb LB |
1383 | /* If extensions were used when attaching, the marker will be a colon-separated |
1384 | * list of images/paths. We enforce strict 1:1 matching, so that we are sure | |
1385 | * we are detaching exactly what was attached. | |
1386 | * For each image, starting with the root, we look for a token in the marker, | |
1387 | * and return a negative answer on any non-matching combination. */ | |
61d0578b | 1388 | |
907952bb LB |
1389 | root_and_extensions = strv_new(name_or_path); |
1390 | if (!root_and_extensions) | |
1391 | return -ENOMEM; | |
61d0578b | 1392 | |
907952bb LB |
1393 | r = strv_extend_strv(&root_and_extensions, extension_image_paths, false); |
1394 | if (r < 0) | |
1395 | return r; | |
61d0578b | 1396 | |
907952bb LB |
1397 | STRV_FOREACH(image_name_or_path, root_and_extensions) { |
1398 | _cleanup_free_ char *image = NULL; | |
1399 | ||
1400 | r = extract_first_word(&marker, &image, ":", EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE); | |
1401 | if (r < 0) | |
1402 | return log_debug_errno(r, "Failed to parse marker: %s", marker); | |
1403 | if (r == 0) | |
61d0578b LP |
1404 | return false; |
1405 | ||
907952bb LB |
1406 | a = last_path_component(image); |
1407 | ||
1408 | if (image_name_is_valid(*image_name_or_path)) { | |
1409 | const char *e, *underscore; | |
61d0578b | 1410 | |
907952bb LB |
1411 | /* We shall match against an image name. In that case let's compare the last component, and optionally |
1412 | * allow either a suffix of ".raw" or a series of "/". | |
1413 | * But allow matching on a different version of the same image, when a "_" is used as a separator. */ | |
1414 | underscore = strchr(*image_name_or_path, '_'); | |
1415 | if (underscore) { | |
1416 | if (strneq(a, *image_name_or_path, underscore - *image_name_or_path)) | |
1417 | continue; | |
1418 | return false; | |
1419 | } | |
61d0578b | 1420 | |
907952bb LB |
1421 | e = startswith(a, *image_name_or_path); |
1422 | if (!e) | |
1423 | return false; | |
61d0578b | 1424 | |
907952bb LB |
1425 | if(!(e[strspn(e, "/")] == 0 || streq(e, ".raw"))) |
1426 | return false; | |
1427 | } else { | |
1428 | const char *b, *underscore; | |
1429 | size_t l; | |
1430 | ||
1431 | /* We shall match against a path. Let's ignore any prefix here though, as often there are many ways to | |
1432 | * reach the same file. However, in this mode, let's validate any file suffix. */ | |
1433 | ||
1434 | l = strcspn(a, "/"); | |
1435 | b = last_path_component(*image_name_or_path); | |
1436 | ||
1437 | if (strcspn(b, "/") != l) | |
1438 | return false; | |
61d0578b | 1439 | |
907952bb LB |
1440 | underscore = strchr(b, '_'); |
1441 | if (underscore) | |
1442 | l = underscore - b; | |
9e4079d4 | 1443 | |
907952bb LB |
1444 | if (!strneq(a, b, l)) |
1445 | return false; | |
1446 | } | |
61d0578b | 1447 | } |
907952bb LB |
1448 | |
1449 | return true; | |
61d0578b LP |
1450 | } |
1451 | ||
1452 | static int test_chroot_dropin( | |
1453 | DIR *d, | |
1454 | const char *where, | |
1455 | const char *fname, | |
1456 | const char *name_or_path, | |
907952bb | 1457 | char **extension_image_paths, |
61d0578b LP |
1458 | char **ret_marker) { |
1459 | ||
1460 | _cleanup_free_ char *line = NULL, *marker = NULL; | |
1461 | _cleanup_fclose_ FILE *f = NULL; | |
1462 | _cleanup_close_ int fd = -1; | |
1463 | const char *p, *e, *k; | |
1464 | int r; | |
1465 | ||
1466 | assert(d); | |
1467 | assert(where); | |
1468 | assert(fname); | |
1469 | ||
1470 | /* We recognize unis created from portable images via the drop-in we created for them */ | |
1471 | ||
1472 | p = strjoina(fname, ".d/20-portable.conf"); | |
1473 | fd = openat(dirfd(d), p, O_RDONLY|O_CLOEXEC); | |
1474 | if (fd < 0) { | |
1475 | if (errno == ENOENT) | |
1476 | return 0; | |
1477 | ||
1478 | return log_debug_errno(errno, "Failed to open %s/%s: %m", where, p); | |
1479 | } | |
1480 | ||
4fa744a3 | 1481 | r = take_fdopen_unlocked(&fd, "r", &f); |
02e23d1a ZJS |
1482 | if (r < 0) |
1483 | return log_debug_errno(r, "Failed to convert file handle: %m"); | |
61d0578b LP |
1484 | |
1485 | r = read_line(f, LONG_LINE_MAX, &line); | |
1486 | if (r < 0) | |
1487 | return log_debug_errno(r, "Failed to read from %s/%s: %m", where, p); | |
1488 | ||
1489 | e = startswith(line, PORTABLE_DROPIN_MARKER_BEGIN); | |
1490 | if (!e) | |
1491 | return 0; | |
1492 | ||
1493 | k = endswith(e, PORTABLE_DROPIN_MARKER_END); | |
1494 | if (!k) | |
1495 | return 0; | |
1496 | ||
1497 | marker = strndup(e, k - e); | |
1498 | if (!marker) | |
1499 | return -ENOMEM; | |
1500 | ||
1501 | if (!name_or_path) | |
1502 | r = true; | |
1503 | else | |
907952bb | 1504 | r = marker_matches_images(marker, name_or_path, extension_image_paths); |
61d0578b LP |
1505 | |
1506 | if (ret_marker) | |
1507 | *ret_marker = TAKE_PTR(marker); | |
1508 | ||
1509 | return r; | |
1510 | } | |
1511 | ||
1512 | int portable_detach( | |
1513 | sd_bus *bus, | |
1514 | const char *name_or_path, | |
907952bb | 1515 | char **extension_image_paths, |
61d0578b LP |
1516 | PortableFlags flags, |
1517 | PortableChange **changes, | |
1518 | size_t *n_changes, | |
1519 | sd_bus_error *error) { | |
1520 | ||
1521 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
d378ef70 | 1522 | _cleanup_set_free_ Set *unit_files = NULL, *markers = NULL; |
61d0578b LP |
1523 | _cleanup_closedir_ DIR *d = NULL; |
1524 | const char *where, *item; | |
61d0578b LP |
1525 | int ret = 0; |
1526 | int r; | |
1527 | ||
1528 | assert(name_or_path); | |
1529 | ||
1530 | r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); | |
1531 | if (r < 0) | |
1532 | return r; | |
1533 | ||
40a7b232 | 1534 | where = attached_path(&paths, flags); |
61d0578b LP |
1535 | |
1536 | d = opendir(where); | |
339731db LP |
1537 | if (!d) { |
1538 | if (errno == ENOENT) | |
1539 | goto not_found; | |
1540 | ||
61d0578b | 1541 | return log_debug_errno(errno, "Failed to open '%s' directory: %m", where); |
339731db | 1542 | } |
61d0578b | 1543 | |
61d0578b LP |
1544 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { |
1545 | _cleanup_free_ char *marker = NULL; | |
1546 | UnitFileState state; | |
1547 | ||
1548 | if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) | |
1549 | continue; | |
1550 | ||
1551 | /* Filter out duplicates */ | |
be327321 | 1552 | if (set_contains(unit_files, de->d_name)) |
61d0578b LP |
1553 | continue; |
1554 | ||
61d0578b LP |
1555 | if (!IN_SET(de->d_type, DT_LNK, DT_REG)) |
1556 | continue; | |
1557 | ||
907952bb | 1558 | r = test_chroot_dropin(d, where, de->d_name, name_or_path, extension_image_paths, &marker); |
61d0578b LP |
1559 | if (r < 0) |
1560 | return r; | |
1561 | if (r == 0) | |
1562 | continue; | |
1563 | ||
1564 | r = unit_file_lookup_state(UNIT_FILE_SYSTEM, &paths, de->d_name, &state); | |
1565 | if (r < 0) | |
1566 | return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name); | |
c3d809ef | 1567 | if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_RUNTIME, UNIT_FILE_LINKED_RUNTIME)) |
61d0578b LP |
1568 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is in state '%s', can't detach.", de->d_name, unit_file_state_to_string(state)); |
1569 | ||
1570 | r = unit_file_is_active(bus, de->d_name, error); | |
1571 | if (r < 0) | |
1572 | return r; | |
e26fe5f9 | 1573 | if (!FLAGS_SET(flags, PORTABLE_REATTACH) && r > 0) |
61d0578b LP |
1574 | return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active, can't detach.", de->d_name); |
1575 | ||
be327321 | 1576 | r = set_put_strdup(&unit_files, de->d_name); |
61d0578b LP |
1577 | if (r < 0) |
1578 | return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name); | |
1579 | ||
907952bb LB |
1580 | for (const char *p = marker;;) { |
1581 | _cleanup_free_ char *image = NULL; | |
61d0578b | 1582 | |
907952bb | 1583 | r = extract_first_word(&p, &image, ":", EXTRACT_UNESCAPE_SEPARATORS|EXTRACT_RETAIN_ESCAPE); |
d378ef70 | 1584 | if (r < 0) |
907952bb LB |
1585 | return log_debug_errno(r, "Failed to parse marker: %s", p); |
1586 | if (r == 0) | |
1587 | break; | |
1588 | ||
1589 | if (path_is_absolute(image) && !image_in_search_path(IMAGE_PORTABLE, NULL, image)) { | |
1590 | r = set_ensure_consume(&markers, &path_hash_ops_free, TAKE_PTR(image)); | |
1591 | if (r < 0) | |
1592 | return r; | |
1593 | } | |
61d0578b LP |
1594 | } |
1595 | } | |
1596 | ||
339731db LP |
1597 | if (set_isempty(unit_files)) |
1598 | goto not_found; | |
61d0578b | 1599 | |
90e74a66 | 1600 | SET_FOREACH(item, unit_files) { |
61d0578b LP |
1601 | _cleanup_free_ char *md = NULL; |
1602 | const char *suffix; | |
1603 | ||
1604 | if (unlinkat(dirfd(d), item, 0) < 0) { | |
1605 | log_debug_errno(errno, "Can't remove unit file %s/%s: %m", where, item); | |
1606 | ||
1607 | if (errno != ENOENT && ret >= 0) | |
1608 | ret = -errno; | |
1609 | } else | |
1610 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, item, NULL); | |
1611 | ||
1612 | FOREACH_STRING(suffix, ".d/10-profile.conf", ".d/20-portable.conf") { | |
1613 | _cleanup_free_ char *dropin = NULL; | |
1614 | ||
1615 | dropin = strjoin(item, suffix); | |
1616 | if (!dropin) | |
1617 | return -ENOMEM; | |
1618 | ||
1619 | if (unlinkat(dirfd(d), dropin, 0) < 0) { | |
1620 | log_debug_errno(errno, "Can't remove drop-in %s/%s: %m", where, dropin); | |
1621 | ||
1622 | if (errno != ENOENT && ret >= 0) | |
1623 | ret = -errno; | |
1624 | } else | |
1625 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, dropin, NULL); | |
1626 | } | |
1627 | ||
1628 | md = strjoin(item, ".d"); | |
1629 | if (!md) | |
1630 | return -ENOMEM; | |
1631 | ||
1632 | if (unlinkat(dirfd(d), md, AT_REMOVEDIR) < 0) { | |
1633 | log_debug_errno(errno, "Can't remove drop-in directory %s/%s: %m", where, md); | |
1634 | ||
1635 | if (errno != ENOENT && ret >= 0) | |
1636 | ret = -errno; | |
1637 | } else | |
1638 | portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, md, NULL); | |
1639 | } | |
1640 | ||
1641 | /* Now, also drop any image symlink, for images outside of the sarch path */ | |
90e74a66 | 1642 | SET_FOREACH(item, markers) { |
61d0578b LP |
1643 | _cleanup_free_ char *sl = NULL; |
1644 | struct stat st; | |
1645 | ||
1646 | r = image_symlink(item, flags, &sl); | |
1647 | if (r < 0) { | |
1648 | log_debug_errno(r, "Failed to determine image symlink for '%s', ignoring: %m", item); | |
1649 | continue; | |
1650 | } | |
1651 | ||
1652 | if (lstat(sl, &st) < 0) { | |
1653 | log_debug_errno(errno, "Failed to stat '%s', ignoring: %m", sl); | |
1654 | continue; | |
1655 | } | |
1656 | ||
1657 | if (!S_ISLNK(st.st_mode)) { | |
1658 | log_debug("Image '%s' is not a symlink, ignoring.", sl); | |
1659 | continue; | |
1660 | } | |
1661 | ||
1662 | if (unlink(sl) < 0) { | |
1663 | log_debug_errno(errno, "Can't remove image symlink '%s': %m", sl); | |
1664 | ||
1665 | if (errno != ENOENT && ret >= 0) | |
1666 | ret = -errno; | |
1667 | } else | |
1668 | portable_changes_add(changes, n_changes, PORTABLE_UNLINK, sl, NULL); | |
1669 | } | |
1670 | ||
d09d85a2 LP |
1671 | /* Try to remove the unit file directory, if we can */ |
1672 | if (rmdir(where) >= 0) | |
1673 | portable_changes_add(changes, n_changes, PORTABLE_UNLINK, where, NULL); | |
1674 | ||
61d0578b | 1675 | return ret; |
339731db LP |
1676 | |
1677 | not_found: | |
1678 | log_debug("No unit files associated with '%s' found. Image not attached?", name_or_path); | |
1679 | return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_UNIT, "No unit files associated with '%s' found. Image not attached?", name_or_path); | |
61d0578b LP |
1680 | } |
1681 | ||
1682 | static int portable_get_state_internal( | |
1683 | sd_bus *bus, | |
1684 | const char *name_or_path, | |
0017415c | 1685 | char **extension_image_paths, |
61d0578b LP |
1686 | PortableFlags flags, |
1687 | PortableState *ret, | |
1688 | sd_bus_error *error) { | |
1689 | ||
1690 | _cleanup_(lookup_paths_free) LookupPaths paths = {}; | |
1691 | bool found_enabled = false, found_running = false; | |
be327321 | 1692 | _cleanup_set_free_ Set *unit_files = NULL; |
61d0578b LP |
1693 | _cleanup_closedir_ DIR *d = NULL; |
1694 | const char *where; | |
61d0578b LP |
1695 | int r; |
1696 | ||
1697 | assert(name_or_path); | |
1698 | assert(ret); | |
1699 | ||
1700 | r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL); | |
1701 | if (r < 0) | |
1702 | return r; | |
1703 | ||
40a7b232 | 1704 | where = attached_path(&paths, flags); |
61d0578b LP |
1705 | |
1706 | d = opendir(where); | |
339731db LP |
1707 | if (!d) { |
1708 | if (errno == ENOENT) { | |
1709 | /* If the 'attached' directory doesn't exist at all, then we know for sure this image isn't attached. */ | |
1710 | *ret = PORTABLE_DETACHED; | |
1711 | return 0; | |
1712 | } | |
1713 | ||
61d0578b | 1714 | return log_debug_errno(errno, "Failed to open '%s' directory: %m", where); |
339731db | 1715 | } |
61d0578b | 1716 | |
61d0578b LP |
1717 | FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) { |
1718 | UnitFileState state; | |
1719 | ||
1720 | if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY)) | |
1721 | continue; | |
1722 | ||
1723 | /* Filter out duplicates */ | |
be327321 | 1724 | if (set_contains(unit_files, de->d_name)) |
61d0578b LP |
1725 | continue; |
1726 | ||
61d0578b LP |
1727 | if (!IN_SET(de->d_type, DT_LNK, DT_REG)) |
1728 | continue; | |
1729 | ||
0017415c | 1730 | r = test_chroot_dropin(d, where, de->d_name, name_or_path, extension_image_paths, NULL); |
61d0578b LP |
1731 | if (r < 0) |
1732 | return r; | |
1733 | if (r == 0) | |
1734 | continue; | |
1735 | ||
1736 | r = unit_file_lookup_state(UNIT_FILE_SYSTEM, &paths, de->d_name, &state); | |
1737 | if (r < 0) | |
1738 | return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name); | |
1739 | if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME)) | |
1740 | found_enabled = true; | |
1741 | ||
1742 | r = unit_file_is_active(bus, de->d_name, error); | |
1743 | if (r < 0) | |
1744 | return r; | |
1745 | if (r > 0) | |
1746 | found_running = true; | |
1747 | ||
be327321 | 1748 | r = set_put_strdup(&unit_files, de->d_name); |
61d0578b LP |
1749 | if (r < 0) |
1750 | return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name); | |
1751 | } | |
1752 | ||
1753 | *ret = found_running ? (!set_isempty(unit_files) && (flags & PORTABLE_RUNTIME) ? PORTABLE_RUNNING_RUNTIME : PORTABLE_RUNNING) : | |
1754 | found_enabled ? (flags & PORTABLE_RUNTIME ? PORTABLE_ENABLED_RUNTIME : PORTABLE_ENABLED) : | |
1755 | !set_isempty(unit_files) ? (flags & PORTABLE_RUNTIME ? PORTABLE_ATTACHED_RUNTIME : PORTABLE_ATTACHED) : PORTABLE_DETACHED; | |
1756 | ||
1757 | return 0; | |
1758 | } | |
1759 | ||
1760 | int portable_get_state( | |
1761 | sd_bus *bus, | |
1762 | const char *name_or_path, | |
0017415c | 1763 | char **extension_image_paths, |
61d0578b LP |
1764 | PortableFlags flags, |
1765 | PortableState *ret, | |
1766 | sd_bus_error *error) { | |
1767 | ||
1768 | PortableState state; | |
1769 | int r; | |
1770 | ||
1771 | assert(name_or_path); | |
1772 | assert(ret); | |
1773 | ||
1774 | /* We look for matching units twice: once in the regular directories, and once in the runtime directories — but | |
1775 | * the latter only if we didn't find anything in the former. */ | |
1776 | ||
0017415c | 1777 | r = portable_get_state_internal(bus, name_or_path, extension_image_paths, flags & ~PORTABLE_RUNTIME, &state, error); |
61d0578b LP |
1778 | if (r < 0) |
1779 | return r; | |
1780 | ||
1781 | if (state == PORTABLE_DETACHED) { | |
0017415c | 1782 | r = portable_get_state_internal(bus, name_or_path, extension_image_paths, flags | PORTABLE_RUNTIME, &state, error); |
61d0578b LP |
1783 | if (r < 0) |
1784 | return r; | |
1785 | } | |
1786 | ||
1787 | *ret = state; | |
1788 | return 0; | |
1789 | } | |
1790 | ||
1791 | int portable_get_profiles(char ***ret) { | |
1792 | assert(ret); | |
1793 | ||
13c02e7b | 1794 | return conf_files_list_nulstr(ret, NULL, NULL, CONF_FILES_DIRECTORY|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED, PORTABLE_PROFILE_DIRS); |
61d0578b LP |
1795 | } |
1796 | ||
1797 | static const char* const portable_change_type_table[_PORTABLE_CHANGE_TYPE_MAX] = { | |
1798 | [PORTABLE_COPY] = "copy", | |
1799 | [PORTABLE_MKDIR] = "mkdir", | |
1800 | [PORTABLE_SYMLINK] = "symlink", | |
1801 | [PORTABLE_UNLINK] = "unlink", | |
1802 | [PORTABLE_WRITE] = "write", | |
1803 | }; | |
1804 | ||
ba5b6c59 | 1805 | DEFINE_STRING_TABLE_LOOKUP(portable_change_type, int); |
61d0578b LP |
1806 | |
1807 | static const char* const portable_state_table[_PORTABLE_STATE_MAX] = { | |
1808 | [PORTABLE_DETACHED] = "detached", | |
1809 | [PORTABLE_ATTACHED] = "attached", | |
1810 | [PORTABLE_ATTACHED_RUNTIME] = "attached-runtime", | |
1811 | [PORTABLE_ENABLED] = "enabled", | |
1812 | [PORTABLE_ENABLED_RUNTIME] = "enabled-runtime", | |
1813 | [PORTABLE_RUNNING] = "running", | |
1814 | [PORTABLE_RUNNING_RUNTIME] = "running-runtime", | |
1815 | }; | |
1816 | ||
1817 | DEFINE_STRING_TABLE_LOOKUP(portable_state, PortableState); |