summaryrefslogtreecommitdiff
path: root/src/tmpfiles
diff options
context:
space:
mode:
authorSrinidhi Kaushik <shrinidhi.kaushik@gmail.com>2021-06-08 15:49:48 +0530
committerLennart Poettering <lennart@poettering.net>2021-06-08 18:24:58 +0200
commit7f7a50dd157b3bfb4282c3cc3a56671121eabe24 (patch)
treed1fd075986945edbf504e85d264352a807c7cc97 /src/tmpfiles
parent66973219c0f2569041df69ad368fdb0ebdb8ff78 (diff)
downloadsystemd-7f7a50dd157b3bfb4282c3cc3a56671121eabe24.tar.gz
tmpfiles: extend "Age" to accept an "age-by" argument
For "systemd-tmpfiles --cleanup", when the "Age" parameter is specified, the criteria for deletion is determined from the path's last modification timestamp ("mtime"), its last access timestamp ("atime") and its last status change timestamp ("ctime"). For instance, if one of those paths to be cleaned up are opened, it results in the modification of "atime", which results file system entry to not be removed because the default aging algorithm would skip the entry. Add an optional "age-by" argument by extending the "Age" parameter to restrict the clean-up for a particular type of file timestamp, which can be specified in "tmpfiles.d" as follows: [age-by:]cleanup-age, where age-by is "[abcmACBM]+" For example: d /foo/bar - - - abM:1m - Would clean-up any files that were not accessed and created, or directories that were not modified less than a minute ago in "/foo/bar". Fixes: #17002
Diffstat (limited to 'src/tmpfiles')
-rw-r--r--src/tmpfiles/tmpfiles.c265
1 files changed, 201 insertions, 64 deletions
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index 8f4ceee037..197bb5d223 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -110,6 +110,17 @@ typedef enum ItemType {
ADJUST_MODE = 'm', /* legacy, 'z' is identical to this */
} ItemType;
+typedef enum AgeBy {
+ AGE_BY_ATIME = 1 << 0,
+ AGE_BY_BTIME = 1 << 1,
+ AGE_BY_CTIME = 1 << 2,
+ AGE_BY_MTIME = 1 << 3,
+
+ /* All file timestamp types are checked by default. */
+ AGE_BY_DEFAULT_FILE = AGE_BY_ATIME | AGE_BY_BTIME | AGE_BY_CTIME | AGE_BY_MTIME,
+ AGE_BY_DEFAULT_DIR = AGE_BY_ATIME | AGE_BY_BTIME | AGE_BY_MTIME
+} AgeBy;
+
typedef struct Item {
ItemType type;
@@ -124,6 +135,7 @@ typedef struct Item {
gid_t gid;
mode_t mode;
usec_t age;
+ AgeBy age_by_file, age_by_dir;
dev_t major_minor;
unsigned attribute_value;
@@ -505,6 +517,64 @@ static inline nsec_t load_statx_timestamp_nsec(const struct statx_timestamp *ts)
return ts->tv_sec * NSEC_PER_SEC + ts->tv_nsec;
}
+static bool needs_cleanup(
+ nsec_t atime,
+ nsec_t btime,
+ nsec_t ctime,
+ nsec_t mtime,
+ nsec_t cutoff,
+ const char *sub_path,
+ AgeBy age_by,
+ bool is_dir) {
+
+ if (FLAGS_SET(age_by, AGE_BY_MTIME) && mtime != NSEC_INFINITY && mtime >= cutoff) {
+ char a[FORMAT_TIMESTAMP_MAX];
+ /* Follows spelling in stat(1). */
+ log_debug("%s \"%s\": modify time %s is too new.",
+ is_dir ? "Directory" : "File",
+ sub_path,
+ format_timestamp_style(a, sizeof(a), mtime / NSEC_PER_USEC, TIMESTAMP_US));
+
+ return false;
+ }
+
+ if (FLAGS_SET(age_by, AGE_BY_ATIME) && atime != NSEC_INFINITY && atime >= cutoff) {
+ char a[FORMAT_TIMESTAMP_MAX];
+ log_debug("%s \"%s\": access time %s is too new.",
+ is_dir ? "Directory" : "File",
+ sub_path,
+ format_timestamp_style(a, sizeof(a), atime / NSEC_PER_USEC, TIMESTAMP_US));
+
+ return false;
+ }
+
+ /*
+ * Note: Unless explicitly specified by the user, "ctime" is ignored
+ * by default for directories, because we change it when deleting.
+ */
+ if (FLAGS_SET(age_by, AGE_BY_CTIME) && ctime != NSEC_INFINITY && ctime >= cutoff) {
+ char a[FORMAT_TIMESTAMP_MAX];
+ log_debug("%s \"%s\": change time %s is too new.",
+ is_dir ? "Directory" : "File",
+ sub_path,
+ format_timestamp_style(a, sizeof(a), ctime / NSEC_PER_USEC, TIMESTAMP_US));
+
+ return false;
+ }
+
+ if (FLAGS_SET(age_by, AGE_BY_BTIME) && btime != NSEC_INFINITY && btime >= cutoff) {
+ char a[FORMAT_TIMESTAMP_MAX];
+ log_debug("%s \"%s\": birth time %s is too new.",
+ is_dir ? "Directory" : "File",
+ sub_path,
+ format_timestamp_style(a, sizeof(a), btime / NSEC_PER_USEC, TIMESTAMP_US));
+
+ return false;
+ }
+
+ return true;
+}
+
static int dir_cleanup(
Item *i,
const char *p,
@@ -516,7 +586,9 @@ static int dir_cleanup(
dev_t rootdev_minor,
bool mountpoint,
int maxdepth,
- bool keep_this_level) {
+ bool keep_this_level,
+ AgeBy age_by_file,
+ AgeBy age_by_dir) {
bool deleted = false;
struct dirent *dent;
@@ -641,7 +713,8 @@ static int dir_cleanup(
sub_path, sub_dir,
atime_nsec, mtime_nsec, cutoff_nsec,
rootdev_major, rootdev_minor,
- false, maxdepth-1, false);
+ false, maxdepth-1, false,
+ age_by_file, age_by_dir);
if (q < 0)
r = q;
}
@@ -656,31 +729,13 @@ static int dir_cleanup(
continue;
}
- /* Ignore ctime, we change it when deleting */
- if (mtime_nsec != NSEC_INFINITY && mtime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- /* Follows spelling in stat(1). */
- log_debug("Directory \"%s\": modify time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), mtime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
+ /*
+ * Check the file timestamps of an entry against the
+ * given cutoff time; delete if it is older.
+ */
+ if (!needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec,
+ cutoff_nsec, sub_path, age_by_dir, true))
continue;
- }
-
- if (atime_nsec != NSEC_INFINITY && atime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- log_debug("Directory \"%s\": access time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), atime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
- continue;
- }
-
- if (btime_nsec != NSEC_INFINITY && btime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- log_debug("Directory \"%s\": birth time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), btime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
- continue;
- }
log_debug("Removing directory \"%s\".", sub_path);
if (unlinkat(dirfd(d), dent->d_name, AT_REMOVEDIR) < 0)
@@ -724,38 +779,9 @@ static int dir_cleanup(
continue;
}
- if (mtime_nsec != NSEC_INFINITY && mtime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- /* Follows spelling in stat(1). */
- log_debug("File \"%s\": modify time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), mtime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
- continue;
- }
-
- if (atime_nsec != NSEC_INFINITY && atime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- log_debug("File \"%s\": access time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), atime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
- continue;
- }
-
- if (ctime_nsec != NSEC_INFINITY && ctime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- log_debug("File \"%s\": change time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), ctime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
+ if (!needs_cleanup(atime_nsec, btime_nsec, ctime_nsec, mtime_nsec,
+ cutoff_nsec, sub_path, age_by_file, false))
continue;
- }
-
- if (btime_nsec != NSEC_INFINITY && btime_nsec >= cutoff_nsec) {
- char a[FORMAT_TIMESTAMP_MAX];
- log_debug("File \"%s\": birth time %s is too new.",
- sub_path,
- format_timestamp_style(a, sizeof(a), btime_nsec / NSEC_PER_USEC, TIMESTAMP_US));
- continue;
- }
log_debug("Removing \"%s\".", sub_path);
if (unlinkat(dirfd(d), dent->d_name, 0) < 0)
@@ -2443,6 +2469,23 @@ static int remove_item(Item *i) {
}
}
+static char *age_by_to_string(AgeBy ab, bool is_dir) {
+ static const char ab_map[] = { 'a', 'b', 'c', 'm' };
+ size_t j = 0;
+ char *ret;
+
+ ret = new(char, ELEMENTSOF(ab_map) + 1);
+ if (!ret)
+ return NULL;
+
+ for (size_t i = 0; i < ELEMENTSOF(ab_map); i++)
+ if (FLAGS_SET(ab, 1U << i))
+ ret[j++] = is_dir ? ascii_toupper(ab_map[i]) : ab_map[i];
+
+ ret[j] = 0;
+ return ret;
+}
+
static int clean_item_instance(Item *i, const char* instance) {
char timestamp[FORMAT_TIMESTAMP_MAX];
_cleanup_closedir_ DIR *d = NULL;
@@ -2489,17 +2532,31 @@ static int clean_item_instance(Item *i, const char* instance) {
sx.stx_ino != ps.st_ino;
}
- log_debug("Cleanup threshold for %s \"%s\" is %s",
- mountpoint ? "mount point" : "directory",
- instance,
- format_timestamp_style(timestamp, sizeof(timestamp), cutoff, TIMESTAMP_US));
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *ab_f = NULL, *ab_d = NULL;
+
+ ab_f = age_by_to_string(i->age_by_file, false);
+ if (!ab_f)
+ return log_oom();
+
+ ab_d = age_by_to_string(i->age_by_dir, true);
+ if (!ab_d)
+ return log_oom();
+
+ log_debug("Cleanup threshold for %s \"%s\" is %s; age-by: %s%s",
+ mountpoint ? "mount point" : "directory",
+ instance,
+ format_timestamp_style(timestamp, sizeof(timestamp), cutoff, TIMESTAMP_US),
+ ab_f, ab_d);
+ }
return dir_cleanup(i, instance, d,
load_statx_timestamp_nsec(&sx.stx_atime),
load_statx_timestamp_nsec(&sx.stx_mtime),
cutoff * NSEC_PER_USEC,
sx.stx_dev_major, sx.stx_dev_minor, mountpoint,
- MAX_DEPTH, i->keep_first_level);
+ MAX_DEPTH, i->keep_first_level,
+ i->age_by_file, i->age_by_dir);
}
static int clean_item(Item *i) {
@@ -2665,6 +2722,9 @@ static bool item_compatible(Item *a, Item *b) {
a->age_set == b->age_set &&
a->age == b->age &&
+ a->age_by_file == b->age_by_file &&
+ a->age_by_dir == b->age_by_dir &&
+
a->mask_perms == b->mask_perms &&
a->keep_first_level == b->keep_first_level &&
@@ -2829,6 +2889,58 @@ static int find_gid(const char *group, gid_t *ret_gid, Hashmap **cache) {
return name_to_gid_offline(arg_root, group, ret_gid, cache);
}
+static int parse_age_by_from_arg(const char *age_by_str, Item *item) {
+ AgeBy ab_f = 0, ab_d = 0;
+
+ static const struct {
+ char age_by_chr;
+ AgeBy age_by_flag;
+ } age_by_types[] = {
+ { 'a', AGE_BY_ATIME },
+ { 'b', AGE_BY_BTIME },
+ { 'c', AGE_BY_CTIME },
+ { 'm', AGE_BY_MTIME },
+ };
+
+ assert(age_by_str);
+ assert(item);
+
+ if (isempty(age_by_str))
+ return -EINVAL;
+
+ for (const char *s = age_by_str; *s != 0; s++) {
+ size_t i;
+
+ /* Ignore whitespace. */
+ if (strchr(WHITESPACE, *s))
+ continue;
+
+ for (i = 0; i < ELEMENTSOF(age_by_types); i++) {
+ /* Check lower-case for files, upper-case for directories. */
+ if (*s == age_by_types[i].age_by_chr) {
+ ab_f |= age_by_types[i].age_by_flag;
+ break;
+ } else if (*s == ascii_toupper(age_by_types[i].age_by_chr)) {
+ ab_d |= age_by_types[i].age_by_flag;
+ break;
+ }
+ }
+
+ /* Invalid character. */
+ if (i >= ELEMENTSOF(age_by_types))
+ return -EINVAL;
+ }
+
+ /* No match. */
+ if (ab_f == 0 && ab_d == 0)
+ return -EINVAL;
+
+ item->age_by_file = ab_f > 0 ? ab_f : AGE_BY_DEFAULT_FILE;
+ item->age_by_dir = ab_d > 0 ? ab_d : AGE_BY_DEFAULT_DIR;
+
+ return 0;
+}
+
static int parse_line(
const char *fname,
unsigned line,
@@ -2838,7 +2950,11 @@ static int parse_line(
Hashmap **gid_cache) {
_cleanup_free_ char *action = NULL, *mode = NULL, *user = NULL, *group = NULL, *age = NULL, *path = NULL;
- _cleanup_(item_free_contents) Item i = {};
+ _cleanup_(item_free_contents) Item i = {
+ /* The "age-by" argument considers all file timestamp types by default. */
+ .age_by_file = AGE_BY_DEFAULT_FILE,
+ .age_by_dir = AGE_BY_DEFAULT_DIR,
+ };
ItemArray *existing;
OrderedHashmap *h;
int r, pos;
@@ -3112,16 +3228,37 @@ static int parse_line(
if (!empty_or_dash(age)) {
const char *a = age;
+ _cleanup_free_ char *seconds = NULL, *age_by = NULL;
if (*a == '~') {
i.keep_first_level = true;
a++;
}
+ /* Format: "age-by:age"; where age-by is "[abcmABCM]+". */
+ r = split_pair(a, ":", &age_by, &seconds);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0 && r != -EINVAL)
+ return log_error_errno(r, "Failed to parse age-by for '%s': %m", age);
+ if (r >= 0) {
+ /* We found a ":", parse the "age-by" part. */
+ r = parse_age_by_from_arg(age_by, &i);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ *invalid_config = true;
+ return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid age-by '%s'.", age_by);
+ }
+
+ /* For parsing the "age" part, after the ":". */
+ a = seconds;
+ }
+
r = parse_sec(a, &i.age);
if (r < 0) {
*invalid_config = true;
- return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid age '%s'.", age);
+ return log_syntax(NULL, LOG_ERR, fname, line, r, "Invalid age '%s'.", a);
}
i.age_set = true;