mirror of
https://github.com/systemd/systemd.git
synced 2024-12-22 17:35:35 +03:00
updatectl: Introduce optional feature verbs
This introduces a nice UX for listing, inspecting, enabling, and disabling optional features from the command line.
This commit is contained in:
parent
e55e7a5a61
commit
5803efff44
@ -90,6 +90,35 @@
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><command>features</command> [<replaceable>FEATURE</replaceable>]</term>
|
||||
|
||||
<listitem><para>When no <replaceable>FEATURE</replaceable> is specified, this command lists all
|
||||
optional features.
|
||||
When a <replaceable>FEATURE</replaceable> is specified, this command lists all known information
|
||||
about that feature.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><command>enable</command> <replaceable>FEATURE</replaceable>…</term>
|
||||
<term><command>disable</command> <replaceable>FEATURE</replaceable>…</term>
|
||||
|
||||
<listitem><para>These commands enable or disable optional features.
|
||||
See <citerefentry><refentrytitle>sysupdate.features</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
|
||||
These commands always operate on the host system.</para>
|
||||
|
||||
<para>By default, these commands will only change the system's configuration by creating or deleting
|
||||
drop-in files; they will not immediately download the enabled features, or clean up after the
|
||||
disabled ones.
|
||||
Enabled features will be downloaded and installed the next time the target is updated, and disabled
|
||||
transfers will be cleaned up the next time the target is updated or vacuumed.
|
||||
Pass <option>--now</option> to immediately apply these changes.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<xi:include href="standard-options.xml" xpointer="help" />
|
||||
<xi:include href="standard-options.xml" xpointer="version" />
|
||||
</variablelist>
|
||||
@ -107,6 +136,9 @@
|
||||
<listitem><para>When used with the <command>update</command> command, reboots the system
|
||||
after updates finish applying. If any update fails, the system will not reboot.</para>
|
||||
|
||||
<para>When used with the <command>enable</command> or <command>disable</command> commands and the
|
||||
<option>--now</option> flag, reboots the system after download or clean-up finish applying.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
@ -121,6 +153,16 @@
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><option>--now</option></term>
|
||||
|
||||
<listitem><para>When used with the <command>enable</command> command, downloads and installs the
|
||||
enabled features. When used with the <command>disable</command> command, deletes all resources
|
||||
downloaded by the disabled features.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<xi:include href="user-system-options.xml" xpointer="host" />
|
||||
|
||||
<xi:include href="standard-options.xml" xpointer="no-pager" />
|
||||
|
@ -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,27 +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, disabled;
|
||||
|
||||
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, "uu", &count, &disabled);
|
||||
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);
|
||||
|
||||
if (count > 0 && disabled > 0)
|
||||
log_info("Deleted %u instance(s) and %u disabled transfer(s) of %s.",
|
||||
count, disabled, targets[i]);
|
||||
else if (count > 0)
|
||||
log_info("Deleted %u instance(s) of %s.", count, targets[i]);
|
||||
else if (disabled > 0)
|
||||
log_info("Deleted %u disabled transfer(s) of %s.", disabled, targets[i]);
|
||||
else
|
||||
log_info("Found nothing to delete for %s.", 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;
|
||||
}
|
||||
|
||||
@ -1204,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"
|
||||
@ -1230,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[] = {
|
||||
@ -1240,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 },
|
||||
{}
|
||||
};
|
||||
|
||||
@ -1278,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;
|
||||
|
||||
@ -1294,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 },
|
||||
{}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user