Merge pull request #33398 from AdrianVovk/sysupdate-optional
sysupdate: Add support for optional features
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
systemd_sysupdate_sources = files(
|
||||
'sysupdate-cache.c',
|
||||
'sysupdate-feature.c',
|
||||
'sysupdate-instance.c',
|
||||
'sysupdate-partition.c',
|
||||
'sysupdate-pattern.c',
|
||||
|
||||
@@ -78,6 +78,18 @@
|
||||
send_interface="org.freedesktop.sysupdate1.Target"
|
||||
send_member="GetVersion"/>
|
||||
|
||||
<allow send_destination="org.freedesktop.sysupdate1"
|
||||
send_interface="org.freedesktop.sysupdate1.Target"
|
||||
send_member="ListFeatures"/>
|
||||
|
||||
<allow send_destination="org.freedesktop.sysupdate1"
|
||||
send_interface="org.freedesktop.sysupdate1.Target"
|
||||
send_member="DescribeFeature"/>
|
||||
|
||||
<allow send_destination="org.freedesktop.sysupdate1"
|
||||
send_interface="org.freedesktop.sysupdate1.Target"
|
||||
send_member="SetFeatureEnabled"/>
|
||||
|
||||
<allow send_destination="org.freedesktop.sysupdate1"
|
||||
send_interface="org.freedesktop.sysupdate1.Job"
|
||||
send_member="Cancel"/>
|
||||
|
||||
@@ -71,4 +71,14 @@
|
||||
</defaults>
|
||||
</action>
|
||||
|
||||
<action id="org.freedesktop.sysupdate1.manage-features">
|
||||
<description gettext-domain="systemd">Manage optional features</description>
|
||||
<message gettext-domain="systemd">Authentication is required to manage optional features</message>
|
||||
<defaults>
|
||||
<allow_any>auth_admin</allow_any>
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>auth_admin_keep</allow_active>
|
||||
</defaults>
|
||||
</action>
|
||||
|
||||
</policyconfig>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "conf-parser.h"
|
||||
#include "hash-funcs.h"
|
||||
#include "path-util.h"
|
||||
#include "sysupdate-feature.h"
|
||||
#include "sysupdate.h"
|
||||
#include "web-util.h"
|
||||
|
||||
static Feature *feature_free(Feature *f) {
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
free(f->id);
|
||||
|
||||
free(f->description);
|
||||
free(f->documentation);
|
||||
free(f->appstream);
|
||||
|
||||
return mfree(f);
|
||||
}
|
||||
|
||||
Feature *feature_new(void) {
|
||||
Feature *f;
|
||||
|
||||
f = new(Feature, 1);
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
*f = (Feature) {
|
||||
.n_ref = 1,
|
||||
};
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_REF_UNREF_FUNC(Feature, feature, feature_free);
|
||||
|
||||
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops,
|
||||
char, string_hash_func, string_compare_func,
|
||||
Feature, feature_unref);
|
||||
|
||||
static int config_parse_url_specifiers(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
char **s = ASSERT_PTR(data);
|
||||
_cleanup_free_ char *resolved = NULL;
|
||||
int r;
|
||||
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*s = mfree(*s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = specifier_printf(rvalue, NAME_MAX, specifier_table, arg_root, NULL, &resolved);
|
||||
if (r < 0) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, r,
|
||||
"Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!http_url_is_valid(resolved)) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"%s= URL is not valid, ignoring: %s", lvalue, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return free_and_replace(*s, resolved);
|
||||
}
|
||||
|
||||
|
||||
int feature_read_definition(Feature *f, const char *path, const char *const *dirs) {
|
||||
assert(f);
|
||||
|
||||
ConfigTableItem table[] = {
|
||||
{ "Feature", "Description", config_parse_string, 0, &f->description },
|
||||
{ "Feature", "Documentation", config_parse_url_specifiers, 0, &f->documentation },
|
||||
{ "Feature", "AppStream", config_parse_url_specifiers, 0, &f->appstream },
|
||||
{ "Feature", "Enabled", config_parse_bool, 0, &f->enabled },
|
||||
{}
|
||||
};
|
||||
|
||||
_cleanup_free_ char *filename = NULL;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(dirs);
|
||||
|
||||
r = path_extract_filename(path, &filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", path);
|
||||
|
||||
r = config_parse_many(
|
||||
STRV_MAKE_CONST(path),
|
||||
dirs,
|
||||
strjoina(filename, ".d"),
|
||||
arg_root,
|
||||
"Feature\0",
|
||||
config_item_table_lookup, table,
|
||||
CONFIG_PARSE_WARN,
|
||||
/* userdata= */ NULL,
|
||||
/* stats_by_path= */ NULL,
|
||||
/* drop_in_files= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ASSERT_PTR(endswith(filename, ".feature")) = 0; /* Remove the file extension */
|
||||
f->id = TAKE_PTR(filename);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "hash-funcs.h"
|
||||
#include "macro.h"
|
||||
#include "sysupdate-transfer.h"
|
||||
|
||||
typedef struct Feature {
|
||||
unsigned n_ref;
|
||||
|
||||
char *id;
|
||||
|
||||
char *description;
|
||||
char *documentation;
|
||||
char *appstream;
|
||||
|
||||
bool enabled;
|
||||
} Feature;
|
||||
|
||||
Feature *feature_new(void);
|
||||
|
||||
Feature *feature_ref(Feature *f);
|
||||
Feature *feature_unref(Feature *f);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(Feature*, feature_unref);
|
||||
|
||||
extern const struct hash_ops feature_hash_ops;
|
||||
|
||||
int feature_read_definition(Feature *f, const char *path, const char *const *conf_file_dirs);
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "stdio-util.h"
|
||||
#include "strv.h"
|
||||
#include "sync-util.h"
|
||||
#include "sysupdate-feature.h"
|
||||
#include "sysupdate-pattern.h"
|
||||
#include "sysupdate-resource.h"
|
||||
#include "sysupdate-transfer.h"
|
||||
@@ -44,12 +45,16 @@ Transfer* transfer_free(Transfer *t) {
|
||||
|
||||
t->temporary_path = rm_rf_subvolume_and_free(t->temporary_path);
|
||||
|
||||
free(t->definition_path);
|
||||
free(t->id);
|
||||
|
||||
free(t->min_version);
|
||||
strv_free(t->protected_versions);
|
||||
free(t->current_symlink);
|
||||
free(t->final_path);
|
||||
|
||||
strv_free(t->features);
|
||||
strv_free(t->requisite_features);
|
||||
|
||||
strv_free(t->changelog);
|
||||
strv_free(t->appstream);
|
||||
|
||||
@@ -93,12 +98,6 @@ Transfer* transfer_new(Context *ctx) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static const Specifier specifier_table[] = {
|
||||
COMMON_SYSTEM_SPECIFIERS,
|
||||
COMMON_TMP_SPECIFIERS,
|
||||
{}
|
||||
};
|
||||
|
||||
static int config_parse_protect_version(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
@@ -466,11 +465,33 @@ static int config_parse_partition_flags(
|
||||
return 0;
|
||||
}
|
||||
|
||||
int transfer_read_definition(Transfer *t, const char *path) {
|
||||
int r;
|
||||
static bool transfer_decide_if_enabled(Transfer *t, Hashmap *known_features) {
|
||||
assert(t);
|
||||
|
||||
/* Requisite feature disabled -> transfer disabled */
|
||||
STRV_FOREACH(id, t->requisite_features) {
|
||||
Feature *f = hashmap_get(known_features, *id);
|
||||
if (!f || !f->enabled) /* missing features are implicitly disabled */
|
||||
return false;
|
||||
}
|
||||
|
||||
/* No features defined -> transfer implicitly enabled */
|
||||
if (strv_isempty(t->features))
|
||||
return true;
|
||||
|
||||
/* At least one feature enabled -> transfer enabled */
|
||||
STRV_FOREACH(id, t->features) {
|
||||
Feature *f = hashmap_get(known_features, *id);
|
||||
if (f && f->enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* All listed features disabled -> transfer disabled */
|
||||
return false;
|
||||
}
|
||||
|
||||
int transfer_read_definition(Transfer *t, const char *path, const char **dirs, Hashmap *known_features) {
|
||||
assert(t);
|
||||
assert(path);
|
||||
|
||||
ConfigTableItem table[] = {
|
||||
{ "Transfer", "MinVersion", config_parse_min_version, 0, &t->min_version },
|
||||
@@ -478,6 +499,8 @@ int transfer_read_definition(Transfer *t, const char *path) {
|
||||
{ "Transfer", "Verify", config_parse_bool, 0, &t->verify },
|
||||
{ "Transfer", "ChangeLog", config_parse_url_specifiers, 0, &t->changelog },
|
||||
{ "Transfer", "AppStream", config_parse_url_specifiers, 0, &t->appstream },
|
||||
{ "Transfer", "Features", config_parse_strv, 0, &t->features },
|
||||
{ "Transfer", "RequisiteFeatures", config_parse_strv, 0, &t->requisite_features },
|
||||
{ "Source", "Type", config_parse_resource_type, 0, &t->source.type },
|
||||
{ "Source", "Path", config_parse_resource_path, 0, &t->source },
|
||||
{ "Source", "PathRelativeTo", config_parse_resource_path_relto, 0, &t->source.path_relative_to },
|
||||
@@ -501,17 +524,39 @@ int transfer_read_definition(Transfer *t, const char *path) {
|
||||
{}
|
||||
};
|
||||
|
||||
r = config_parse(NULL, path, NULL,
|
||||
"Transfer\0"
|
||||
"Source\0"
|
||||
"Target\0",
|
||||
config_item_table_lookup, table,
|
||||
CONFIG_PARSE_WARN,
|
||||
t,
|
||||
NULL);
|
||||
_cleanup_free_ char *filename = NULL;
|
||||
char *e;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
assert(dirs);
|
||||
|
||||
r = path_extract_filename(path, &filename);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", path);
|
||||
|
||||
r = config_parse_many(
|
||||
STRV_MAKE_CONST(path),
|
||||
dirs,
|
||||
strjoina(filename, ".d"),
|
||||
arg_root,
|
||||
"Transfer\0"
|
||||
"Source\0"
|
||||
"Target\0",
|
||||
config_item_table_lookup, table,
|
||||
CONFIG_PARSE_WARN,
|
||||
/* userdata= */ NULL,
|
||||
/* stats_by_path= */ NULL,
|
||||
/* drop_in_files= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
e = ASSERT_PTR(endswith(filename, ".transfer") ?: endswith(filename, ".conf"));
|
||||
*e = 0; /* Remove the file extension */
|
||||
t->id = TAKE_PTR(filename);
|
||||
|
||||
t->enabled = transfer_decide_if_enabled(t, known_features);
|
||||
|
||||
if (!RESOURCE_IS_SOURCE(t->source.type))
|
||||
return log_syntax(NULL, LOG_ERR, path, 1, SYNTHETIC_ERRNO(EINVAL),
|
||||
"Source Type= must be one of url-file, url-tar, tar, regular-file, directory, subvolume.");
|
||||
@@ -704,6 +749,8 @@ int transfer_vacuum(
|
||||
assert(instances_max >= 1);
|
||||
if (instances_max == UINT64_MAX) /* Keep infinite instances? */
|
||||
limit = UINT64_MAX;
|
||||
else if (space == UINT64_MAX) /* forcibly delete all instances? */
|
||||
limit = 0;
|
||||
else if (space > instances_max)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOSPC),
|
||||
"Asked to delete more instances than total maximum allowed number of instances, refusing.");
|
||||
@@ -713,7 +760,7 @@ int transfer_vacuum(
|
||||
else
|
||||
limit = instances_max - space;
|
||||
|
||||
if (t->target.type == RESOURCE_PARTITION) {
|
||||
if (t->target.type == RESOURCE_PARTITION && space != UINT64_MAX) {
|
||||
uint64_t rm, remain;
|
||||
|
||||
/* If we are looking at a partition table, we also have to take into account how many
|
||||
@@ -767,7 +814,11 @@ int transfer_vacuum(
|
||||
|
||||
assert(oldest->resource);
|
||||
|
||||
log_info("%s Removing old '%s' (%s).", special_glyph(SPECIAL_GLYPH_RECYCLING), oldest->path, resource_type_to_string(oldest->resource->type));
|
||||
log_info("%s Removing %s '%s' (%s).",
|
||||
special_glyph(SPECIAL_GLYPH_RECYCLING),
|
||||
space == UINT64_MAX ? "disabled" : "old",
|
||||
oldest->path,
|
||||
resource_type_to_string(oldest->resource->type));
|
||||
|
||||
switch (t->target.type) {
|
||||
|
||||
|
||||
@@ -15,12 +15,17 @@ typedef struct Transfer Transfer;
|
||||
#include "sysupdate.h"
|
||||
|
||||
struct Transfer {
|
||||
char *definition_path;
|
||||
char *id;
|
||||
|
||||
char *min_version;
|
||||
char **protected_versions;
|
||||
char *current_symlink;
|
||||
bool verify;
|
||||
|
||||
char **features;
|
||||
char **requisite_features;
|
||||
bool enabled;
|
||||
|
||||
Resource source, target;
|
||||
|
||||
uint64_t instances_max;
|
||||
@@ -55,11 +60,10 @@ struct Transfer {
|
||||
typedef int (*TransferProgress)(const Transfer *t, const Instance *inst, unsigned percentage);
|
||||
|
||||
Transfer* transfer_new(Context *ctx);
|
||||
|
||||
Transfer* transfer_free(Transfer *t);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(Transfer*, transfer_free);
|
||||
|
||||
int transfer_read_definition(Transfer *t, const char *path);
|
||||
int transfer_read_definition(Transfer *t, const char *path, const char **dirs, Hashmap *features);
|
||||
|
||||
int transfer_resolve_paths(Transfer *t, const char *root, const char *node);
|
||||
|
||||
|
||||
+296
-54
@@ -25,9 +25,11 @@
|
||||
#include "set.h"
|
||||
#include "signal-util.h"
|
||||
#include "sort-util.h"
|
||||
#include "specifier.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "sysupdate.h"
|
||||
#include "sysupdate-feature.h"
|
||||
#include "sysupdate-transfer.h"
|
||||
#include "sysupdate-update-set.h"
|
||||
#include "sysupdate-util.h"
|
||||
@@ -57,10 +59,21 @@ STATIC_DESTRUCTOR_REGISTER(arg_component, freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
|
||||
STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep);
|
||||
|
||||
const Specifier specifier_table[] = {
|
||||
COMMON_SYSTEM_SPECIFIERS,
|
||||
COMMON_TMP_SPECIFIERS,
|
||||
{}
|
||||
};
|
||||
|
||||
typedef struct Context {
|
||||
Transfer **transfers;
|
||||
size_t n_transfers;
|
||||
|
||||
Transfer **disabled_transfers;
|
||||
size_t n_disabled_transfers;
|
||||
|
||||
Hashmap *features; /* Defined features, keyed by ID */
|
||||
|
||||
UpdateSet **update_sets;
|
||||
size_t n_update_sets;
|
||||
|
||||
@@ -77,6 +90,12 @@ static Context* context_free(Context *c) {
|
||||
transfer_free(*tr);
|
||||
free(c->transfers);
|
||||
|
||||
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers)
|
||||
transfer_free(*tr);
|
||||
free(c->disabled_transfers);
|
||||
|
||||
hashmap_free(c->features);
|
||||
|
||||
FOREACH_ARRAY(us, c->update_sets, c->n_update_sets)
|
||||
update_set_free(*us);
|
||||
free(c->update_sets);
|
||||
@@ -93,83 +112,145 @@ static Context* context_new(void) {
|
||||
return new0(Context, 1);
|
||||
}
|
||||
|
||||
static int context_read_definitions(
|
||||
static void free_transfers(Transfer **array, size_t n) {
|
||||
FOREACH_ARRAY(t, array, n)
|
||||
transfer_free(*t);
|
||||
free(array);
|
||||
}
|
||||
|
||||
static int read_definitions(
|
||||
Context *c,
|
||||
const char *directory,
|
||||
const char *component,
|
||||
const char *root,
|
||||
const char **dirs,
|
||||
const char *suffix,
|
||||
const char *node) {
|
||||
|
||||
_cleanup_strv_free_ char **files = NULL;
|
||||
Transfer **transfers = NULL, **disabled = NULL;
|
||||
size_t n_transfers = 0, n_disabled = 0;
|
||||
int r;
|
||||
|
||||
CLEANUP_ARRAY(transfers, n_transfers, free_transfers);
|
||||
CLEANUP_ARRAY(disabled, n_disabled, free_transfers);
|
||||
|
||||
assert(c);
|
||||
assert(dirs);
|
||||
assert(suffix);
|
||||
|
||||
if (directory)
|
||||
r = conf_files_list_strv(&files, ".conf", NULL, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) STRV_MAKE(directory));
|
||||
else if (component) {
|
||||
_cleanup_strv_free_ char **n = NULL;
|
||||
char **l = CONF_PATHS_STRV("");
|
||||
size_t k = 0;
|
||||
|
||||
n = new0(char*, strv_length(l) + 1);
|
||||
if (!n)
|
||||
return log_oom();
|
||||
|
||||
STRV_FOREACH(i, l) {
|
||||
char *j;
|
||||
|
||||
j = strjoin(*i, "sysupdate.", component, ".d");
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
n[k++] = j;
|
||||
}
|
||||
|
||||
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) n);
|
||||
} else
|
||||
r = conf_files_list_strv(&files, ".conf", root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char**) CONF_PATHS_STRV("sysupdate.d"));
|
||||
r = conf_files_list_strv(&files, suffix, arg_root, CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, dirs);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enumerate *.conf files: %m");
|
||||
return log_error_errno(r, "Failed to enumerate sysupdate.d/*%s definitions: %m", suffix);
|
||||
|
||||
STRV_FOREACH(f, files) {
|
||||
STRV_FOREACH(p, files) {
|
||||
_cleanup_(transfer_freep) Transfer *t = NULL;
|
||||
|
||||
if (!GREEDY_REALLOC(c->transfers, c->n_transfers + 1))
|
||||
return log_oom();
|
||||
Transfer **appended;
|
||||
|
||||
t = transfer_new(c);
|
||||
if (!t)
|
||||
return log_oom();
|
||||
|
||||
t->definition_path = strdup(*f);
|
||||
if (!t->definition_path)
|
||||
return log_oom();
|
||||
|
||||
r = transfer_read_definition(t, *f);
|
||||
r = transfer_read_definition(t, *p, dirs, c->features);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
c->transfers[c->n_transfers++] = TAKE_PTR(t);
|
||||
r = transfer_resolve_paths(t, arg_root, node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (t->enabled)
|
||||
appended = GREEDY_REALLOC_APPEND(transfers, n_transfers, &t, 1);
|
||||
else
|
||||
appended = GREEDY_REALLOC_APPEND(disabled, n_disabled, &t, 1);
|
||||
if (!appended)
|
||||
return log_oom();
|
||||
TAKE_PTR(t);
|
||||
}
|
||||
|
||||
c->transfers = TAKE_PTR(transfers);
|
||||
c->n_transfers = n_transfers;
|
||||
c->disabled_transfers = TAKE_PTR(disabled);
|
||||
c->n_disabled_transfers = n_disabled;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int context_read_definitions(Context *c, const char* node) {
|
||||
_cleanup_strv_free_ char **dirs = NULL, **files = NULL;
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
|
||||
if (arg_definitions)
|
||||
dirs = strv_new(arg_definitions);
|
||||
else if (arg_component) {
|
||||
char **l = CONF_PATHS_STRV("");
|
||||
size_t i = 0;
|
||||
|
||||
dirs = new0(char*, strv_length(l) + 1);
|
||||
if (!dirs)
|
||||
return log_oom();
|
||||
|
||||
STRV_FOREACH(dir, l) {
|
||||
char *j;
|
||||
|
||||
j = strjoin(*dir, "sysupdate.", arg_component, ".d");
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
dirs[i++] = j;
|
||||
}
|
||||
} else
|
||||
dirs = strv_new(CONF_PATHS("sysupdate.d"));
|
||||
if (!dirs)
|
||||
return log_oom();
|
||||
|
||||
r = conf_files_list_strv(&files,
|
||||
".feature",
|
||||
arg_root,
|
||||
CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED,
|
||||
(const char**) dirs);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enumerate sysupdate.d/*.feature definitions: %m");
|
||||
|
||||
STRV_FOREACH(p, files) {
|
||||
_cleanup_(feature_unrefp) Feature *f = NULL;
|
||||
|
||||
f = feature_new();
|
||||
if (!f)
|
||||
return log_oom();
|
||||
|
||||
r = feature_read_definition(f, *p, (const char**) dirs);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = hashmap_ensure_put(&c->features, &feature_hash_ops, f->id, f);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to insert feature '%s' into map: %m", f->id);
|
||||
TAKE_PTR(f);
|
||||
}
|
||||
|
||||
r = read_definitions(c, (const char**) dirs, ".transfer", node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (c->n_transfers + c->n_disabled_transfers == 0) {
|
||||
/* Backwards-compat: If no .transfer defs are found, fall back to trying .conf! */
|
||||
r = read_definitions(c, (const char**) dirs, ".conf", node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (c->n_transfers + c->n_disabled_transfers > 0)
|
||||
log_warning("As of v257, transfer definitions should have the '.transfer' extension.");
|
||||
}
|
||||
|
||||
if (c->n_transfers == 0) {
|
||||
if (arg_component)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
|
||||
"No transfer definitions for component '%s' found.", arg_component);
|
||||
"No transfer definitions for component '%s' found.",
|
||||
arg_component);
|
||||
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
|
||||
"No transfer definitions found.");
|
||||
}
|
||||
|
||||
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
|
||||
Transfer *t = *tr;
|
||||
|
||||
r = transfer_resolve_paths(t, root, node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -191,6 +272,17 @@ static int context_load_installed_instances(Context *c) {
|
||||
return r;
|
||||
}
|
||||
|
||||
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
|
||||
Transfer *t = *tr;
|
||||
|
||||
r = resource_load_instances(
|
||||
&t->target,
|
||||
arg_verify >= 0 ? arg_verify : t->verify,
|
||||
&c->web_cache);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -203,7 +295,6 @@ static int context_load_available_instances(Context *c) {
|
||||
|
||||
FOREACH_ARRAY(tr, c->transfers, c->n_transfers) {
|
||||
Transfer *t = *tr;
|
||||
assert(t);
|
||||
|
||||
r = resource_load_instances(
|
||||
&t->source,
|
||||
@@ -750,6 +841,7 @@ static int context_vacuum(
|
||||
uint64_t space,
|
||||
const char *extra_protected_version) {
|
||||
|
||||
size_t disabled_count = 0;
|
||||
int r, count = 0;
|
||||
|
||||
assert(c);
|
||||
@@ -773,15 +865,29 @@ static int context_vacuum(
|
||||
count = MAX(count, r);
|
||||
}
|
||||
|
||||
FOREACH_ARRAY(tr, c->disabled_transfers, c->n_disabled_transfers) {
|
||||
r = transfer_vacuum(*tr, UINT64_MAX /* wipe all instances */, NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
disabled_count++;
|
||||
}
|
||||
|
||||
if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
|
||||
if (count > 0)
|
||||
if (count > 0 && disabled_count > 0)
|
||||
log_info("Removed %i instances, and %zu disabled transfers.", count, disabled_count);
|
||||
else if (count > 0)
|
||||
log_info("Removed %i instances.", count);
|
||||
else if (disabled_count > 0)
|
||||
log_info("Removed %zu disabled transfers.", disabled_count);
|
||||
else
|
||||
log_info("Removed no instances.");
|
||||
log_info("Found nothing to remove.");
|
||||
} else {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
|
||||
|
||||
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_INTEGER("removed", count));
|
||||
r = sd_json_buildo(&json,
|
||||
SD_JSON_BUILD_PAIR_INTEGER("removed", count),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("disabledTransfers", disabled_count));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to create JSON: %m");
|
||||
|
||||
@@ -806,7 +912,7 @@ static int context_make_offline(Context **ret, const char *node) {
|
||||
if (!context)
|
||||
return log_oom();
|
||||
|
||||
r = context_read_definitions(context, arg_definitions, arg_component, arg_root, node);
|
||||
r = context_read_definitions(context, node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -1095,6 +1201,140 @@ static int verb_list(int argc, char **argv, void *userdata) {
|
||||
}
|
||||
}
|
||||
|
||||
static int verb_features(int argc, char **argv, void *userdata) {
|
||||
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
|
||||
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
|
||||
_cleanup_(context_freep) Context* context = NULL;
|
||||
_cleanup_(table_unrefp) Table *table = NULL;
|
||||
const char *feature_id;
|
||||
Feature *f;
|
||||
int r;
|
||||
|
||||
assert(argc <= 2);
|
||||
feature_id = argc >= 2 ? argv[1] : NULL;
|
||||
|
||||
r = process_image(/* ro= */ true, &mounted_dir, &loop_device);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = context_make_offline(&context, loop_device ? loop_device->node : NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (feature_id) {
|
||||
_cleanup_strv_free_ char **transfers = NULL;
|
||||
|
||||
f = hashmap_get(context->features, feature_id);
|
||||
if (!f)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
|
||||
"Optional feature not found: %s",
|
||||
feature_id);
|
||||
|
||||
table = table_new_vertical();
|
||||
if (!table)
|
||||
return log_oom();
|
||||
|
||||
FOREACH_ARRAY(tr, context->transfers, context->n_transfers) {
|
||||
Transfer *t = *tr;
|
||||
|
||||
if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
|
||||
continue;
|
||||
|
||||
r = strv_extend(&transfers, t->id);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
FOREACH_ARRAY(tr, context->disabled_transfers, context->n_disabled_transfers) {
|
||||
Transfer *t = *tr;
|
||||
|
||||
if (!strv_contains(t->features, f->id) && !strv_contains(t->requisite_features, f->id))
|
||||
continue;
|
||||
|
||||
r = strv_extend(&transfers, t->id);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = table_add_many(table,
|
||||
TABLE_FIELD, "Name",
|
||||
TABLE_STRING, f->id,
|
||||
TABLE_FIELD, "Enabled",
|
||||
TABLE_BOOLEAN, f->enabled);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
|
||||
if (f->description) {
|
||||
r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f->description);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
if (f->documentation) {
|
||||
r = table_add_many(table,
|
||||
TABLE_FIELD, "Documentation",
|
||||
TABLE_STRING, f->documentation,
|
||||
TABLE_SET_URL, f->documentation);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
if (f->appstream) {
|
||||
r = table_add_many(table,
|
||||
TABLE_FIELD, "AppStream",
|
||||
TABLE_STRING, f->appstream,
|
||||
TABLE_SET_URL, f->appstream);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
if (!strv_isempty(transfers)) {
|
||||
r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, transfers);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
|
||||
} else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
|
||||
table = table_new("", "feature", "description", "documentation");
|
||||
if (!table)
|
||||
return log_oom();
|
||||
|
||||
HASHMAP_FOREACH(f, context->features) {
|
||||
r = table_add_many(table,
|
||||
TABLE_BOOLEAN_CHECKMARK, f->enabled,
|
||||
TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled),
|
||||
TABLE_STRING, f->id,
|
||||
TABLE_STRING, f->description,
|
||||
TABLE_STRING, f->documentation,
|
||||
TABLE_SET_URL, f->documentation);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
|
||||
} else {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
|
||||
_cleanup_strv_free_ char **features = NULL;
|
||||
|
||||
HASHMAP_FOREACH(f, context->features) {
|
||||
r = strv_extend(&features, f->id);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_STRV("features", features));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to create JSON: %m");
|
||||
|
||||
r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to print JSON: %m");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int verb_check_new(int argc, char **argv, void *userdata) {
|
||||
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
|
||||
_cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL;
|
||||
@@ -1411,6 +1651,7 @@ static int verb_help(int argc, char **argv, void *userdata) {
|
||||
"\n%5$sUpdate OS images.%6$s\n"
|
||||
"\n%3$sCommands:%4$s\n"
|
||||
" list [VERSION] Show installed and available versions\n"
|
||||
" features [FEATURE] Show optional features\n"
|
||||
" check-new Check if there's a new version available\n"
|
||||
" update [VERSION] Install new version now\n"
|
||||
" vacuum Make room, by deleting old versions\n"
|
||||
@@ -1623,8 +1864,9 @@ static int sysupdate_main(int argc, char *argv[]) {
|
||||
static const Verb verbs[] = {
|
||||
{ "list", VERB_ANY, 2, VERB_DEFAULT, verb_list },
|
||||
{ "components", VERB_ANY, 1, 0, verb_components },
|
||||
{ "features", VERB_ANY, 2, 0, verb_features },
|
||||
{ "check-new", VERB_ANY, 1, 0, verb_check_new },
|
||||
{ "update", VERB_ANY, 2, 0, verb_update },
|
||||
{ "update", VERB_ANY, 2, 0, verb_update },
|
||||
{ "vacuum", VERB_ANY, 1, 0, verb_vacuum },
|
||||
{ "reboot", 1, 1, 0, verb_pending_or_reboot },
|
||||
{ "pending", 1, 1, 0, verb_pending_or_reboot },
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "specifier.h"
|
||||
|
||||
/* Forward declare this type so that Transfers can point at it */
|
||||
typedef struct Context Context;
|
||||
|
||||
@@ -11,3 +13,5 @@ extern bool arg_sync;
|
||||
extern uint64_t arg_instances_max;
|
||||
extern char *arg_root;
|
||||
extern char *arg_transfer_source;
|
||||
|
||||
extern const Specifier specifier_table[];
|
||||
|
||||
+228
-20
@@ -13,6 +13,7 @@
|
||||
#include "bus-util.h"
|
||||
#include "common-signal.h"
|
||||
#include "discover-image.h"
|
||||
#include "dropin.h"
|
||||
#include "env-util.h"
|
||||
#include "escape.h"
|
||||
#include "event-util.h"
|
||||
@@ -31,6 +32,8 @@
|
||||
#include "string-table.h"
|
||||
#include "sysupdate-util.h"
|
||||
|
||||
#define FEATURES_DROPIN_NAME "systemd-sysupdate-enabled"
|
||||
|
||||
typedef struct Manager {
|
||||
sd_event *event;
|
||||
sd_bus *bus;
|
||||
@@ -85,6 +88,7 @@ typedef enum JobType {
|
||||
JOB_CHECK_NEW,
|
||||
JOB_UPDATE,
|
||||
JOB_VACUUM,
|
||||
JOB_DESCRIBE_FEATURE,
|
||||
_JOB_TYPE_MAX,
|
||||
_JOB_TYPE_INVALID = -EINVAL,
|
||||
} JobType;
|
||||
@@ -104,6 +108,7 @@ struct Job {
|
||||
JobType type;
|
||||
bool offline;
|
||||
char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */
|
||||
char *feature; /* Passed into sysupdate for JOB_DESCRIBE_FEATURE */
|
||||
|
||||
unsigned progress_percent;
|
||||
|
||||
@@ -131,11 +136,12 @@ static const char* const target_class_table[_TARGET_CLASS_MAX] = {
|
||||
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass);
|
||||
|
||||
static const char* const job_type_table[_JOB_TYPE_MAX] = {
|
||||
[JOB_LIST] = "list",
|
||||
[JOB_DESCRIBE] = "describe",
|
||||
[JOB_CHECK_NEW] = "check-new",
|
||||
[JOB_UPDATE] = "update",
|
||||
[JOB_VACUUM] = "vacuum",
|
||||
[JOB_LIST] = "list",
|
||||
[JOB_DESCRIBE] = "describe",
|
||||
[JOB_CHECK_NEW] = "check-new",
|
||||
[JOB_UPDATE] = "update",
|
||||
[JOB_VACUUM] = "vacuum",
|
||||
[JOB_DESCRIBE_FEATURE] = "describe-feature",
|
||||
};
|
||||
|
||||
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType);
|
||||
@@ -149,6 +155,7 @@ static Job *job_free(Job *j) {
|
||||
|
||||
free(j->object_path);
|
||||
free(j->version);
|
||||
free(j->feature);
|
||||
|
||||
sd_json_variant_unref(j->json);
|
||||
|
||||
@@ -440,8 +447,8 @@ static int job_start(Job *j) {
|
||||
NULL, /* maybe --verify=no */
|
||||
NULL, /* maybe --component=, --root=, or --image= */
|
||||
NULL, /* maybe --offline */
|
||||
NULL, /* list, check-new, update, vacuum */
|
||||
NULL, /* maybe version (for list, update) */
|
||||
NULL, /* list, check-new, update, vacuum, features */
|
||||
NULL, /* maybe version (for list, update), maybe feature (features) */
|
||||
NULL
|
||||
};
|
||||
size_t k = 2;
|
||||
@@ -493,6 +500,12 @@ static int job_start(Job *j) {
|
||||
cmd[k++] = "vacuum";
|
||||
break;
|
||||
|
||||
case JOB_DESCRIBE_FEATURE:
|
||||
cmd[k++] = "features";
|
||||
assert(!isempty(j->feature));
|
||||
cmd[k++] = j->feature;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
@@ -573,20 +586,26 @@ static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *
|
||||
action = "org.freedesktop.sysupdate1.vacuum";
|
||||
break;
|
||||
|
||||
case JOB_DESCRIBE_FEATURE:
|
||||
action = NULL;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
||||
r = bus_verify_polkit_async(
|
||||
msg,
|
||||
action,
|
||||
/* details= */ NULL,
|
||||
&j->manager->polkit_registry,
|
||||
error);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return 1; /* Will call us back */
|
||||
if (action) {
|
||||
r = bus_verify_polkit_async(
|
||||
msg,
|
||||
action,
|
||||
/* details= */ NULL,
|
||||
&j->manager->polkit_registry,
|
||||
error);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return 1; /* Will call us back */
|
||||
}
|
||||
|
||||
r = job_cancel(j);
|
||||
if (r < 0)
|
||||
@@ -918,6 +937,8 @@ static int target_method_describe_finish(
|
||||
_cleanup_free_ char *text = NULL;
|
||||
int r;
|
||||
|
||||
/* NOTE: This is also reused by target_method_describe_feature */
|
||||
|
||||
assert(json);
|
||||
|
||||
r = sd_json_variant_format(json, 0, &text);
|
||||
@@ -1132,7 +1153,7 @@ static int target_method_vacuum_finish(
|
||||
sd_bus_error *error) {
|
||||
|
||||
sd_json_variant *v;
|
||||
uint64_t instances;
|
||||
uint64_t instances, disabled;
|
||||
|
||||
assert(json);
|
||||
|
||||
@@ -1144,7 +1165,15 @@ static int target_method_vacuum_finish(
|
||||
instances = sd_json_variant_unsigned(v);
|
||||
assert(instances <= UINT32_MAX);
|
||||
|
||||
return sd_bus_reply_method_return(msg, "u", (uint32_t) instances);
|
||||
v = sd_json_variant_by_key(json, "disabledTransfers");
|
||||
if (!v)
|
||||
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Missing key 'disabledTransfers'");
|
||||
if (!sd_json_variant_is_unsigned(v))
|
||||
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Key 'disabledTransfers' should be an unsigned int");
|
||||
disabled = sd_json_variant_unsigned(v);
|
||||
assert(disabled <= UINT32_MAX);
|
||||
|
||||
return sd_bus_reply_method_return(msg, "uu", (uint32_t) instances, (uint32_t) disabled);
|
||||
}
|
||||
|
||||
static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
|
||||
@@ -1247,6 +1276,167 @@ static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_b
|
||||
return sd_bus_send(NULL, reply, NULL);
|
||||
}
|
||||
|
||||
static int target_method_list_features(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
|
||||
_cleanup_strv_free_ char **features = NULL;
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
Target *t = ASSERT_PTR(userdata);
|
||||
sd_json_variant *v;
|
||||
uint64_t flags;
|
||||
int r;
|
||||
|
||||
assert(msg);
|
||||
|
||||
r = sd_bus_message_read(msg, "t", &flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (flags != 0)
|
||||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
|
||||
|
||||
r = sysupdate_run_simple(&json, t, "features", NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
v = sd_json_variant_by_key(json, "features");
|
||||
if (!v)
|
||||
return -EINVAL;
|
||||
r = sd_json_variant_strv(v, &features);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_bus_message_new_method_return(msg, &reply);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_bus_message_append_strv(reply, features);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_bus_send(NULL, reply, NULL);
|
||||
}
|
||||
|
||||
static int target_method_describe_feature(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
|
||||
Target *t = ASSERT_PTR(userdata);
|
||||
_cleanup_(job_freep) Job *j = NULL;
|
||||
const char *feature;
|
||||
uint64_t flags;
|
||||
int r;
|
||||
|
||||
assert(msg);
|
||||
|
||||
r = sd_bus_message_read(msg, "st", &feature, &flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (isempty(feature))
|
||||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Feature must be specified");
|
||||
|
||||
if (flags != 0)
|
||||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
|
||||
|
||||
r = job_new(JOB_DESCRIBE_FEATURE, t, msg, target_method_describe_finish, &j);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
j->feature = strdup(feature);
|
||||
if (!j->feature)
|
||||
return log_oom();
|
||||
|
||||
r = job_start(j);
|
||||
if (r < 0)
|
||||
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
|
||||
TAKE_PTR(j); /* Avoid job from being killed & freed */
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool feature_name_is_valid(const char *name) {
|
||||
if (isempty(name))
|
||||
return false;
|
||||
|
||||
if (!ascii_is_valid(name))
|
||||
return false;
|
||||
|
||||
if (!filename_is_valid(strjoina(name, ".feature.d")))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int target_method_set_feature_enabled(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
|
||||
_cleanup_free_ char *feature_ext = NULL;
|
||||
Target *t = ASSERT_PTR(userdata);
|
||||
const char *feature;
|
||||
uint64_t flags;
|
||||
int32_t enabled;
|
||||
int r;
|
||||
|
||||
assert(msg);
|
||||
|
||||
if (t->class != TARGET_HOST)
|
||||
return sd_bus_reply_method_errorf(msg,
|
||||
SD_BUS_ERROR_NOT_SUPPORTED,
|
||||
"For now, features can only be managed on the host system.");
|
||||
|
||||
r = sd_bus_message_read(msg, "sit", &feature, &enabled, &flags);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (!feature_name_is_valid(feature))
|
||||
return sd_bus_reply_method_errorf(msg,
|
||||
SD_BUS_ERROR_INVALID_ARGS,
|
||||
"The specified feature is invalid");
|
||||
if (flags != 0)
|
||||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
|
||||
|
||||
if (!endswith(feature, ".feature")) {
|
||||
feature_ext = strjoin(feature, ".feature");
|
||||
if (!feature_ext)
|
||||
return -ENOMEM;
|
||||
feature = feature_ext;
|
||||
}
|
||||
|
||||
const char *details[] = {
|
||||
"class", target_class_to_string(t->class),
|
||||
"name", t->name,
|
||||
"feature", feature,
|
||||
"enabled", enabled >= 0 ? true_false(enabled) : "unset",
|
||||
NULL
|
||||
};
|
||||
|
||||
r = bus_verify_polkit_async(
|
||||
msg,
|
||||
"org.freedesktop.sysupdate1.manage-features",
|
||||
details,
|
||||
&t->manager->polkit_registry,
|
||||
error);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return 1; /* Will call us back */
|
||||
|
||||
/* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
|
||||
if (enabled < 0) { /* Reset -> delete the drop-in file */
|
||||
_cleanup_free_ char *path = NULL;
|
||||
|
||||
r = drop_in_file(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME, NULL, &path);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (unlink(path) < 0)
|
||||
return -errno;
|
||||
} else { /* otherwise, create the drop-in with the right settings */
|
||||
r = write_drop_in_format(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME,
|
||||
"# Generated via org.freedesktop.sysupdate1 D-Bus interface\n\n"
|
||||
"[Feature]\n"
|
||||
"Enabled=%s\n",
|
||||
yes_no(enabled));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return sd_bus_reply_method_return(msg, NULL);
|
||||
}
|
||||
|
||||
static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
|
||||
_cleanup_strv_free_ char **components = NULL;
|
||||
@@ -1397,7 +1587,7 @@ static const sd_bus_vtable target_vtable[] = {
|
||||
|
||||
SD_BUS_METHOD_WITH_ARGS("Vacuum",
|
||||
SD_BUS_NO_ARGS,
|
||||
SD_BUS_RESULT("u", count),
|
||||
SD_BUS_RESULT("u", instances, "u", disabled_transfers),
|
||||
target_method_vacuum,
|
||||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||||
|
||||
@@ -1413,6 +1603,24 @@ static const sd_bus_vtable target_vtable[] = {
|
||||
target_method_get_version,
|
||||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||||
|
||||
SD_BUS_METHOD_WITH_ARGS("ListFeatures",
|
||||
SD_BUS_ARGS("t", flags),
|
||||
SD_BUS_RESULT("as", features),
|
||||
target_method_list_features,
|
||||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||||
|
||||
SD_BUS_METHOD_WITH_ARGS("DescribeFeature",
|
||||
SD_BUS_ARGS("s", feature, "t", flags),
|
||||
SD_BUS_RESULT("s", json),
|
||||
target_method_describe_feature,
|
||||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||||
|
||||
SD_BUS_METHOD_WITH_ARGS("SetFeatureEnabled",
|
||||
SD_BUS_ARGS("s", feature, "i", enabled, "t", flags),
|
||||
SD_BUS_NO_RESULT,
|
||||
target_method_set_feature_enabled,
|
||||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||||
|
||||
SD_BUS_VTABLE_END
|
||||
};
|
||||
|
||||
|
||||
+330
-24
@@ -12,12 +12,18 @@
|
||||
#include "bus-locator.h"
|
||||
#include "bus-map-properties.h"
|
||||
#include "bus-util.h"
|
||||
#include "conf-files.h"
|
||||
#include "conf-parser.h"
|
||||
#include "errno-list.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "format-table.h"
|
||||
#include "fs-util.h"
|
||||
#include "json-util.h"
|
||||
#include "main-func.h"
|
||||
#include "os-util.h"
|
||||
#include "pager.h"
|
||||
#include "path-util.h"
|
||||
#include "pretty-print.h"
|
||||
#include "strv.h"
|
||||
#include "sysupdate-update-set-flags.h"
|
||||
@@ -29,9 +35,11 @@ static PagerFlags arg_pager_flags = 0;
|
||||
static bool arg_legend = true;
|
||||
static bool arg_reboot = false;
|
||||
static bool arg_offline = false;
|
||||
static bool arg_now = false;
|
||||
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
|
||||
static char *arg_host = NULL;
|
||||
|
||||
#define SYSUPDATE_HOST_PATH "/org/freedesktop/sysupdate1/target/host"
|
||||
#define SYSUPDATE_TARGET_INTERFACE "org.freedesktop.sysupdate1.Target"
|
||||
|
||||
typedef struct Version {
|
||||
@@ -1044,21 +1052,19 @@ static int update_started(sd_bus_message *reply, void *userdata, sd_bus_error *r
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int verb_update(int argc, char **argv, void *userdata) {
|
||||
sd_bus *bus = ASSERT_PTR(userdata);
|
||||
static int do_update(sd_bus *bus, char **targets) {
|
||||
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
|
||||
_cleanup_(sd_event_source_unrefp) sd_event_source *render_exit = NULL;
|
||||
_cleanup_ordered_hashmap_free_ OrderedHashmap *map = NULL;
|
||||
_cleanup_strv_free_ char **targets = NULL, **versions = NULL, **target_paths = NULL;
|
||||
_cleanup_strv_free_ char **versions = NULL, **target_paths = NULL;
|
||||
size_t n;
|
||||
unsigned remaining = 0;
|
||||
void *p;
|
||||
bool did_anything = false;
|
||||
int r;
|
||||
|
||||
r = ensure_targets(bus, argv + 1, &targets);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Could not find targets: %m");
|
||||
assert(bus);
|
||||
assert(targets);
|
||||
|
||||
r = parse_targets(targets, &n, &target_paths, &versions);
|
||||
if (r < 0)
|
||||
@@ -1140,18 +1146,64 @@ static int verb_update(int argc, char **argv, void *userdata) {
|
||||
did_anything = true;
|
||||
}
|
||||
|
||||
if (arg_reboot) {
|
||||
if (did_anything)
|
||||
return reboot_now();
|
||||
log_info("Nothing was updated... skipping reboot.");
|
||||
}
|
||||
return did_anything ? 1 : 0;
|
||||
}
|
||||
|
||||
static int verb_update(int argc, char **argv, void *userdata) {
|
||||
sd_bus *bus = ASSERT_PTR(userdata);
|
||||
_cleanup_strv_free_ char **targets = NULL;
|
||||
bool did_anything = false;
|
||||
int r;
|
||||
|
||||
r = ensure_targets(bus, argv + 1, &targets);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Could not find targets: %m");
|
||||
|
||||
r = do_update(bus, targets);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
did_anything = true;
|
||||
|
||||
if (!arg_reboot)
|
||||
return 0;
|
||||
|
||||
if (did_anything)
|
||||
return reboot_now();
|
||||
|
||||
log_info("Nothing was updated... skipping reboot.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_vacuum(sd_bus *bus, const char *target, const char *path) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
uint32_t count, disabled;
|
||||
int r;
|
||||
|
||||
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, path, SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, target, "call Vacuum");
|
||||
|
||||
r = sd_bus_message_read(reply, "uu", &count, &disabled);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
if (count > 0 && disabled > 0)
|
||||
log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
|
||||
count, disabled, target);
|
||||
else if (count > 0)
|
||||
log_info("Deleted %u instance(s) of %s.", count, target);
|
||||
else if (disabled > 0)
|
||||
log_info("Deleted %u disabled transfer(s) of %s.", disabled, target);
|
||||
else
|
||||
log_info("Found nothing to delete for %s.", target);
|
||||
|
||||
return count + disabled > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static int verb_vacuum(int argc, char **argv, void *userdata) {
|
||||
sd_bus *bus = ASSERT_PTR(userdata);
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_strv_free_ char **targets = NULL, **target_paths = NULL;
|
||||
size_t n;
|
||||
int r;
|
||||
@@ -1165,19 +1217,260 @@ static int verb_vacuum(int argc, char **argv, void *userdata) {
|
||||
return log_error_errno(r, "Failed to parse targets: %m");
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
uint32_t count;
|
||||
|
||||
r = sd_bus_call_method(bus, bus_sysupdate_mgr->destination, target_paths[i], SYSUPDATE_TARGET_INTERFACE, "Vacuum", &error, &reply, NULL);
|
||||
r = do_vacuum(bus, targets[i], target_paths[i]);
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, targets[i], "call Vacuum");
|
||||
return r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
r = sd_bus_message_read(reply, "u", &count);
|
||||
typedef struct Feature {
|
||||
char *name;
|
||||
char *description;
|
||||
bool enabled;
|
||||
char *documentation;
|
||||
char **transfers;
|
||||
} Feature;
|
||||
|
||||
static void feature_done(Feature *f) {
|
||||
assert(f);
|
||||
f->name = mfree(f->name);
|
||||
f->description = mfree(f->description);
|
||||
f->documentation = mfree(f->documentation);
|
||||
f->transfers = strv_free(f->transfers);
|
||||
}
|
||||
|
||||
static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
_cleanup_(feature_done) Feature f = {};
|
||||
char *json;
|
||||
int r;
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, name), SD_JSON_MANDATORY },
|
||||
{ "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, description), 0 },
|
||||
{ "enabled", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled), SD_JSON_MANDATORY },
|
||||
{ "documentationUrl", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, documentation), 0 },
|
||||
{ "transfers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, transfers), 0 },
|
||||
{}
|
||||
};
|
||||
|
||||
assert(bus);
|
||||
assert(feature);
|
||||
assert(ret);
|
||||
|
||||
r = sd_bus_call_method(bus,
|
||||
bus_sysupdate_mgr->destination,
|
||||
SYSUPDATE_HOST_PATH,
|
||||
SYSUPDATE_TARGET_INTERFACE,
|
||||
"DescribeFeature",
|
||||
&error,
|
||||
&reply,
|
||||
"st",
|
||||
feature,
|
||||
UINT64_C(0));
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, "host", "lookup feature");
|
||||
|
||||
r = sd_bus_message_read_basic(reply, 's', &json);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
r = sd_json_parse(json, 0, &v, NULL, NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse JSON: %m");
|
||||
|
||||
r = sd_json_dispatch(v, dispatch_table, 0, &f);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to dispatch JSON: %m");
|
||||
|
||||
*ret = TAKE_STRUCT(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int list_features(sd_bus *bus) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
_cleanup_strv_free_ char **features = NULL;
|
||||
_cleanup_(table_unrefp) Table *table = NULL;
|
||||
int r;
|
||||
|
||||
assert(bus);
|
||||
|
||||
table = table_new("", "feature", "description");
|
||||
if (!table)
|
||||
return log_oom();
|
||||
|
||||
r = sd_bus_call_method(bus,
|
||||
bus_sysupdate_mgr->destination,
|
||||
SYSUPDATE_HOST_PATH,
|
||||
SYSUPDATE_TARGET_INTERFACE,
|
||||
"ListFeatures",
|
||||
&error,
|
||||
&reply,
|
||||
"t",
|
||||
UINT64_C(0));
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, "host", "lookup feature");
|
||||
|
||||
r = sd_bus_message_read_strv(reply, &features);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
STRV_FOREACH(feature, features) {
|
||||
_cleanup_(feature_done) Feature f = {};
|
||||
_cleanup_free_ char *name_link = NULL;
|
||||
|
||||
r = describe_feature(bus, *feature, &f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (urlify_enabled() && f.documentation) {
|
||||
name_link = strjoin(f.name, special_glyph(SPECIAL_GLYPH_EXTERNAL_LINK));
|
||||
if (!name_link)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
r = table_add_many(table,
|
||||
TABLE_BOOLEAN_CHECKMARK, f.enabled,
|
||||
TABLE_SET_COLOR, ansi_highlight_green_red(f.enabled),
|
||||
TABLE_STRING, name_link ?: f.name,
|
||||
TABLE_SET_URL, f.documentation,
|
||||
TABLE_STRING, f.description);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, arg_legend);
|
||||
}
|
||||
|
||||
static int verb_features(int argc, char **argv, void *userdata) {
|
||||
sd_bus *bus = ASSERT_PTR(userdata);
|
||||
_cleanup_(table_unrefp) Table *table = NULL;
|
||||
_cleanup_(feature_done) Feature f = {};
|
||||
int r;
|
||||
|
||||
if (argc == 1)
|
||||
return list_features(bus);
|
||||
|
||||
table = table_new_vertical();
|
||||
if (!table)
|
||||
return log_oom();
|
||||
|
||||
r = describe_feature(bus, argv[1], &f);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = table_add_many(table,
|
||||
TABLE_FIELD, "Name",
|
||||
TABLE_STRING, f.name,
|
||||
TABLE_FIELD, "Enabled",
|
||||
TABLE_BOOLEAN, f.enabled);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
|
||||
if (f.description) {
|
||||
r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f.description);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
if (f.documentation) {
|
||||
r = table_add_many(table,
|
||||
TABLE_FIELD, "Documentation",
|
||||
TABLE_STRING, f.documentation,
|
||||
TABLE_SET_URL, f.documentation);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
if (!strv_isempty(f.transfers)) {
|
||||
r = table_add_many(table, TABLE_FIELD, "Transfers", TABLE_STRV_WRAPPED, f.transfers);
|
||||
if (r < 0)
|
||||
return table_log_add_error(r);
|
||||
}
|
||||
|
||||
return table_print_with_pager(table, SD_JSON_FORMAT_OFF, arg_pager_flags, false);
|
||||
}
|
||||
|
||||
static int verb_enable(int argc, char **argv, void *userdata) {
|
||||
sd_bus *bus = ASSERT_PTR(userdata);
|
||||
bool did_anything = false, enable;
|
||||
char **features;
|
||||
int r;
|
||||
|
||||
enable = streq(argv[0], "enable");
|
||||
features = strv_skip(argv, 1);
|
||||
|
||||
STRV_FOREACH(feature, features) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
|
||||
r = sd_bus_call_method(bus,
|
||||
bus_sysupdate_mgr->destination,
|
||||
SYSUPDATE_HOST_PATH,
|
||||
SYSUPDATE_TARGET_INTERFACE,
|
||||
"SetFeatureEnabled",
|
||||
&error,
|
||||
/* reply= */ NULL,
|
||||
"sbt",
|
||||
*feature,
|
||||
(int) enable,
|
||||
UINT64_C(0));
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, "host", "call SetFeatureEnabled");
|
||||
}
|
||||
|
||||
if (!arg_now) /* We weren't asked to apply the changes, so we're done! */
|
||||
return 0;
|
||||
|
||||
if (enable) {
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
||||
_cleanup_free_ char *target = NULL;
|
||||
char *version = NULL;
|
||||
|
||||
/* We're downloading the new feature into the "current" version, which is either going to be
|
||||
* the currently booted version or it's going to be a pending update that has already been
|
||||
* installed and is just waiting for us to reboot into it. */
|
||||
|
||||
r = sd_bus_call_method(bus,
|
||||
bus_sysupdate_mgr->destination,
|
||||
SYSUPDATE_HOST_PATH,
|
||||
SYSUPDATE_TARGET_INTERFACE,
|
||||
"GetVersion",
|
||||
&error,
|
||||
&reply,
|
||||
NULL);
|
||||
if (r < 0)
|
||||
return log_bus_error(r, &error, "host", "get current version");
|
||||
|
||||
r = sd_bus_message_read_basic(reply, 's', &version);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
log_info("Deleted %u instance(s) of %s.\n", count, targets[i]);
|
||||
}
|
||||
target = strjoin("host@", version);
|
||||
if (!target)
|
||||
return log_oom();
|
||||
|
||||
r = do_update(bus, STRV_MAKE(target));
|
||||
} else
|
||||
r = do_vacuum(bus, "host", SYSUPDATE_HOST_PATH);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0)
|
||||
did_anything = true;
|
||||
|
||||
if (arg_reboot && did_anything)
|
||||
return reboot_now();
|
||||
else if (did_anything)
|
||||
log_info("Feature(s) %s.", enable ? "downloaded" : "deleted");
|
||||
else
|
||||
log_info("Nothing %s%s.",
|
||||
enable ? "downloaded" : "deleted",
|
||||
arg_reboot ? ", skipping reboot" :"");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1196,11 +1489,15 @@ static int help(void) {
|
||||
" check [TARGET...] Check for updates\n"
|
||||
" update [TARGET[@VERSION]...] Install updates\n"
|
||||
" vacuum [TARGET...] Clean up old updates\n"
|
||||
" features [FEATURE] List and inspect optional features on host OS\n"
|
||||
" enable FEATURE... Enable optional feature on host OS\n"
|
||||
" disable FEATURE... Disable optional feature on host OS\n"
|
||||
" -h --help Show this help\n"
|
||||
" --version Show package version\n"
|
||||
"\n%3$sOptions:%4$s\n"
|
||||
" --reboot Reboot after updating to newer version\n"
|
||||
" --offline Do not fetch metadata from the network\n"
|
||||
" --now Download/delete resources immediately\n"
|
||||
" -H --host=[USER@]HOST Operate on remote host\n"
|
||||
" --no-pager Do not pipe output into a pager\n"
|
||||
" --no-legend Do not show the headers and footers\n"
|
||||
@@ -1222,6 +1519,7 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
ARG_NO_LEGEND,
|
||||
ARG_REBOOT,
|
||||
ARG_OFFLINE,
|
||||
ARG_NOW,
|
||||
};
|
||||
|
||||
static const struct option options[] = {
|
||||
@@ -1232,6 +1530,7 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
{ "host", required_argument, NULL, 'H' },
|
||||
{ "reboot", no_argument, NULL, ARG_REBOOT },
|
||||
{ "offline", no_argument, NULL, ARG_OFFLINE },
|
||||
{ "now", no_argument, NULL, ARG_NOW },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -1270,6 +1569,10 @@ static int parse_argv(int argc, char *argv[]) {
|
||||
arg_offline = true;
|
||||
break;
|
||||
|
||||
case ARG_NOW:
|
||||
arg_now = true;
|
||||
break;
|
||||
|
||||
case '?':
|
||||
return -EINVAL;
|
||||
|
||||
@@ -1286,10 +1589,13 @@ static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
static const Verb verbs[] = {
|
||||
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
|
||||
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
|
||||
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
|
||||
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
|
||||
{ "list", VERB_ANY, 2, VERB_DEFAULT|VERB_ONLINE_ONLY, verb_list },
|
||||
{ "check", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_check },
|
||||
{ "update", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_update },
|
||||
{ "vacuum", VERB_ANY, VERB_ANY, VERB_ONLINE_ONLY, verb_vacuum },
|
||||
{ "features", VERB_ANY, 2, VERB_ONLINE_ONLY, verb_features },
|
||||
{ "enable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
|
||||
{ "disable", 2, VERB_ANY, VERB_ONLINE_ONLY, verb_enable },
|
||||
{}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user