From patchwork Tue Oct 11 22:01:53 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Robin Roevens X-Patchwork-Id: 6056 Return-Path: Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by web04.haj.ipfire.org (Postfix) with ESMTPS id 4Mn8tX2Hyjz3wgN for ; Tue, 11 Oct 2022 22:03:32 +0000 (UTC) Received: from mail02.haj.ipfire.org (mail02.haj.ipfire.org [172.28.1.201]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail02.haj.ipfire.org", Issuer "R3" (verified OK)) by mail01.ipfire.org (Postfix) with ESMTPS id 4Mn8tQ1rpYz2b7; Tue, 11 Oct 2022 22:03:26 +0000 (UTC) Received: from mail02.haj.ipfire.org (localhost [127.0.0.1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4Mn8tP0Vnbz303n; Tue, 11 Oct 2022 22:03:25 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4Mn8tM0HQrz2xmP for ; Tue, 11 Oct 2022 22:03:23 +0000 (UTC) Received: from knopi.disroot.org (knopi.disroot.org [178.21.23.139]) (using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPS id 4Mn8tL68M8z1gg for ; Tue, 11 Oct 2022 22:03:22 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by disroot.org (Postfix) with ESMTP id 9259A4DB07 for ; Wed, 12 Oct 2022 00:03:22 +0200 (CEST) X-Virus-Scanned: SPAM Filter at disroot.org Received: from knopi.disroot.org ([127.0.0.1]) by localhost (disroot.org [127.0.0.1]) (amavisd-new, port 10024) with UTF8SMTP id RFMT-Ive1Ac4 for ; Wed, 12 Oct 2022 00:03:20 +0200 (CEST) Received: from chojin.sicho.home (amaterasu.sicho.home [192.168.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) (no client certificate requested) (Authenticated sender) by hachiman (MailScanner Milter) with SMTP id EC8B2751F1; Wed, 12 Oct 2022 00:02:08 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail; t=1665525747; bh=JBuOl8ZP3GB9iUxpKHg+3oBq+nLP4uWCgKKfjT82pdg=; h=From:To:Cc:Subject:Date:In-Reply-To:References; b=ZmRxGNAeb90QOUOPrKiRk4LZCQMSBiWaPlw5b3ff0eBrPTFQ/cuhlyefiswfWuYOE HqsDk3cw6/zgDwa8x0FBr8h027NOChFgODzf3m2hacXJqq1TfDxJAumMXwZZg9wPvC nOiNPkj9WqYe+4BzusaEYAoH5Ay/1ziToiJsN6B7ZYcjin65dFQ/9XdEPZQ9I/YGI/ 2sg7Eio4GkVQwDmzfOhNfI0dqM5/k8xB4GpEGMm2jpeX6gXH/KjYDR35cUCrVeyRBf lu9qavtHRKqGBSpruaOjwlF9p1xPCk0+mdQc30/J1Pf1pFFZvr2chw+1G2kXfEUXDr FlobTj/bGN0Aw== From: Robin Roevens To: development@lists.ipfire.org Subject: [PATCH v3 1/5] misc-progs: addonctrl: Add support for 'Services' metadata Date: Wed, 12 Oct 2022 00:01:53 +0200 Message-Id: <20221011220157.17385-2-robin.roevens@disroot.org> In-Reply-To: <20221011220157.17385-1-robin.roevens@disroot.org> References: <20221011220157.17385-1-robin.roevens@disroot.org> Mime-Version: 1.0 X-sicho-MailScanner-ID: EC8B2751F1.A8A80 X-sicho-MailScanner: Found to be clean X-sicho-MailScanner-From: robin.roevens@disroot.org X-sicho-MailScanner-Watermark: 1666130530.86395@lQ9s3VYNF9d/XxtsoUqnqg ARC-Seal: i=1; s=202003rsa; d=lists.ipfire.org; t=1665525802; a=rsa-sha256; cv=none; b=A/06Tu/+oMshsLwvSm15V0yMk2bhy95qC+qbPwGFcWy/n/nmaliYOx/Y3vBLlKoxBhMDCK 2KMYSZBSzrwP0U+TSu0PhR265cOXjio0sLVEREZAm+w7hHH98sheOMEHuWF9DQuhOgFTJ1 4MDh9HpM4dtWIY9NKPB9n9AlcvYrlqy6V7dsQXOWAoRY7zWYbQ9tym97qhx4zrvuMOJYAF Hgo9aj+ItCLhmzU5jHdCEhJI6CNybfe2GZrK6kRSbfBQqOZFhsYu0LQQhgW49RjwLqBmH6 xWJbWO8jImXanpVUeeIndB31FtMIkmzjHG/eR58ZJ+p1RQgWIiKPctcTKX30lw== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=pass header.d=disroot.org header.s=mail header.b=ZmRxGNAe; spf=pass (mail01.ipfire.org: domain of robin.roevens@disroot.org designates 178.21.23.139 as permitted sender) smtp.mailfrom=robin.roevens@disroot.org; dmarc=pass (policy=quarantine) header.from=disroot.org ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1665525802; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:dkim-signature; bh=WLN1tSKJSumT03FBdxv0mO1cWXaKEpWzg2cVK/xo/H4=; b=VhUu1VmwcIkxW6Bych9VcNng/RRrbZooEY1cT07dzAPUD/bbJDhjn/helyuO19J0OR18oZ OOLfqoJ+8OuaDudDDsNgDLXOLNvptiBDj/R62aY8SLKx4cU2bb4DYyVZWYO/GNGYpuOL90 c7GiMUONmQ28nFGYg6KIe6KWzdLYU3Hd/C40tLRpmTI8Z54C8ztfOdBmZ8shvTkE4DwPG6 tiXINIP+jI/xi2s1KEqeXUrTd06n1qvEpaVOUKe/Z/EG1YvVeWGIcCp1a40BnHAjPLa8/4 Sx4vxITE/JwkUxXOIEoyIPd+1JFKWTySlMzGJQ0tQODpIoNBCzh7cqATEu+c1Q== Authentication-Results: mail01.ipfire.org; dkim=pass header.d=disroot.org header.s=mail header.b=ZmRxGNAe; spf=pass (mail01.ipfire.org: domain of robin.roevens@disroot.org designates 178.21.23.139 as permitted sender) smtp.mailfrom=robin.roevens@disroot.org; dmarc=pass (policy=quarantine) header.from=disroot.org X-Rspamd-Server: mail01.haj.ipfire.org X-Spamd-Result: default: False [-5.11 / 11.00]; BAYES_HAM(-3.00)[99.99%]; IP_REPUTATION_HAM(-1.13)[asn: 50673(-0.32), country: NL(-0.01), ip: 178.21.23.139(-0.81)]; MID_CONTAINS_FROM(1.00)[]; NEURAL_HAM(-1.00)[-1.000]; DKIM_REPUTATION(-0.82)[-0.81568261952269]; SPF_REPUTATION_HAM(-0.61)[-0.61451727240354]; DMARC_POLICY_ALLOW(-0.50)[disroot.org,quarantine]; R_MISSING_CHARSET(0.50)[]; MV_CASE(0.50)[]; R_DKIM_ALLOW(0.27)[disroot.org:s=mail]; R_SPF_ALLOW(-0.20)[+a:c]; MIME_GOOD(-0.10)[text/plain]; MX_GOOD(-0.01)[]; ARC_NA(0.00)[]; PREVIOUSLY_DELIVERED(0.00)[development@lists.ipfire.org]; FROM_HAS_DN(0.00)[]; TO_MATCH_ENVRCPT_SOME(0.00)[]; FROM_EQ_ENVFROM(0.00)[]; RCVD_TLS_LAST(0.00)[]; RCVD_COUNT_THREE(0.00)[4]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1]; ASN(0.00)[asn:50673, ipnet:178.21.23.0/24, country:NL]; DKIM_TRACE(0.00)[disroot.org:+]; MIME_TRACE(0.00)[0:+]; RCPT_COUNT_TWO(0.00)[2]; TO_DN_SOME(0.00)[] X-Rspamd-Queue-Id: 4Mn8tL68M8z1gg X-BeenThere: development@lists.ipfire.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: IPFire development talk List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: development-bounces@lists.ipfire.org Sender: "Development" * Addonctrl will now check in addon metadata for the exact initscript names (Services). If more than one initscript is defined for an addon, the requested action will be performed on all listed initscripts. * Added posibility to perform action on a specific initscript of an addon instead of on all initscripts of the addon. * New action 'list-services' to display a list of services related to an addon. * New action 'boot-status' to display wether service(s) are enabled to start on boot or not. * More error checking and cleaner error reporting to user * General cleanup and code restructuring to avoid code duplication * Updated and made usage instructions more verbose. Fixes: Bug#12935 Signed-off-by: Robin Roevens Reviewed-by: Michael Tremer --- src/misc-progs/addonctrl.c | 459 ++++++++++++++++++++++++++++++++----- 1 file changed, 398 insertions(+), 61 deletions(-) diff --git a/src/misc-progs/addonctrl.c b/src/misc-progs/addonctrl.c index 14b4b1325..8eb7fbfa5 100644 --- a/src/misc-progs/addonctrl.c +++ b/src/misc-progs/addonctrl.c @@ -10,71 +10,408 @@ #include #include #include +#include #include +#include +#include +#include #include "setuid.h" #define BUFFER_SIZE 1024 +const char *initd_path = "/etc/rc.d/init.d"; +const char *enabled_path = "/etc/rc.d/rc3.d"; +const char *disabled_path = "/etc/rc.d/rc3.d/off"; + +const char *usage = + "Usage\n" + " addonctrl (start|stop|restart|reload|enable|disable|status|boot-status|list-services) []\n" + "\n" + "Options:\n" + " \t\tName of the addon to control\n" + " \t\tSpecific service of the addon to control (optional)\n" + " \t\t\tBy default the requested action is performed on all related services. See also 'list-services'.\n" + " start\t\t\tStart service(s) of the addon\n" + " stop\t\t\tStop service(s) of the addon\n" + " restart\t\tRestart service(s) of the addon\n" + " enable\t\tEnable service(s) of the addon to start at boot\n" + " disable\t\tDisable service(s) of the addon to start at boot\n" + " status\t\tDisplay current state of addon service(s)\n" + " boot-status\t\tDisplay wether service(s) is enabled on boot or not\n" + " list-services\t\tDisplay a list of services related to the addon"; + +// Find a file using as glob pattern. +// Returns the found filename or NULL if not found +char *find_file_in_dir(const char *path, const char *filepattern) +{ + struct dirent *entry; + DIR *dp; + char *found = NULL; + + dp = opendir(path); + if (dp) { + entry = readdir(dp); + while(!found && entry) { + if (fnmatch(filepattern, entry->d_name, FNM_PATHNAME) == 0) + found = strdup(entry->d_name); + else + entry = readdir(dp); + } + + closedir(dp); + } + + return found; +} + +// Reads Services metadata for . +// Returns pointer to array of strings containing the services for , +// sets to the number of found services and +// sets to +// -1 - system error occured, check errno +// 0 - success - if returned array is NULL, there are no services for +// 1 - addon was not found +char **get_addon_services(const char *addon, int *servicescnt, const char *filter, int *returncode) { + const char *metafile_prefix = "/opt/pakfire/db/installed/meta-"; + const char *metadata_key = "Services"; + const char *keyvalue_delim = ":"; + const char *service_delim = " "; + char *token; + char **services = NULL; + char *service; + char *line = NULL; + size_t line_len = 0; + int i = 0; + char *metafile = NULL; + + *returncode = 0; + + if (!addon) { + errno = EINVAL; + *returncode = 1; + return NULL; + } + + *returncode = asprintf(&metafile, "%s%s", metafile_prefix, addon); + if (*returncode == -1) + return NULL; + + FILE *fp = fopen(metafile,"r"); + if (!fp) { + if (errno == ENOENT) { + *returncode = 1; + } else { + *returncode = -1; + } + return NULL; + } + + // Get initscript(s) for addon from meta-file + while (getline(&line, &line_len, fp) != -1 && !services) { + // Strip newline + char *newline = strchr(line, '\n'); + if (newline) *newline = 0; + + // Split line in key and values; Check for required key. + token = strtok(line, keyvalue_delim); + if (!token || strcmp(token, metadata_key) != 0) + continue; + + // Get values for matched key. Stop if no values are present. + token = strtok(NULL, keyvalue_delim); + if (!token) + break; + + // Split values and put each service in services array + service = strtok(token, service_delim); + while (service) { + if (!filter || strcmp(filter, service) == 0) { + services = reallocarray(services, i+1 ,sizeof (char *)); + if (!services) { + *returncode = -1; + break; + } + + services[i] = strdup(service); + if (!services[i++]) { + *returncode = -1; + break; + } + } + + service = strtok(NULL, service_delim); + } + } + + if (line) free(line); + fclose(fp); + free(metafile); + + *servicescnt = i; + + return services; +} + +// Calls initscript with parameter +int initscript_action(const char *service, const char *action) { + char *initscript = NULL; + char *argv[] = { + action, + NULL + }; + int r = 0; + + r = asprintf(&initscript, "%s/%s", initd_path, service); + if (r != -1) + r = run(initscript, argv); + + if (initscript) free(initscript); + + return r; +} + +// Move an initscript with filepattern from to +// Returns: +// -1: Error during move or memory allocation. Details in errno +// 0: Success +// 1: file was not moved, but is already in +// 2: file does not exist in either in or +int move_initscript_by_pattern(const char *src_path, const char *dest_path, const char *filepattern) { + char *src = NULL; + char *dest = NULL; + int r = 2; + char *filename = NULL; + + filename = find_file_in_dir(src_path, filepattern); + if (filename) { + // Move file + r = asprintf(&src, "%s/%s", src_path, filename); + if (r != -1) { + r = asprintf(&dest, "%s/%s", dest_path, filename); + if (r != -1) + r = rename(src, dest); + } + + if (src) free(src); + if (dest) free(dest); + } else { + // check if file is already in dest + filename = find_file_in_dir(dest_path, filepattern); + if (filename) + r = 1; + } + + if (filename) free(filename); + + return r; +} + +// Enable/Disable addon service(s) by moving initscript symlink from/to disabled_path +// Returns: +// -1 - System error occured. Check errno. +// 0 - Success +// 1 - Service was already enabled/disabled +// 2 - Service has no valid runlevel symlink +int toggle_service(const char *service, const char *action) { + const char *src_path, *dest_path; + char *filepattern = NULL; + int r = 0; + + if (asprintf(&filepattern, "S??%s", service) == -1) + return -1; + + if (strcmp(action, "enable") == 0) { + src_path = disabled_path; + dest_path = enabled_path; + } else { + src_path = enabled_path; + dest_path = disabled_path; + } + + // Ensure disabled_path exists + r = mkdir(disabled_path, S_IRWXU + S_IRGRP + S_IXGRP + S_IROTH + S_IXOTH); + if (r != -1 || errno == EEXIST) + r = move_initscript_by_pattern(src_path, dest_path, filepattern); + + free(filepattern); + + return r; +} + +// Return whether is enabled or disabled on boot +// Returns: +// -1 - System error occured. Check errno. +// 0 - is disabled on boot +// 1 - is enabled on boot +// 2 - Runlevel suymlink for was not found +int get_boot_status(char *service) { + char *filepattern = NULL; + char *filename = NULL; + int r = 2; + + if (asprintf(&filepattern, "S??%s", service) == -1) + return -1; + + filename = find_file_in_dir(enabled_path, filepattern); + if (filename) + r = 1; + else { + filename = find_file_in_dir(disabled_path, filepattern); + if (filename) + r = 0; + else + r = 2; + } + + if (filename) free(filename); + free(filepattern); + + return r; +} + int main(int argc, char *argv[]) { - char command[BUFFER_SIZE]; - - if (!(initsetuid())) - exit(1); - - if (argc < 3) { - fprintf(stderr, "\nMissing arguments.\n\naddonctrl addon (start|stop|restart|reload|enable|disable)\n\n"); - exit(1); - } - - const char* name = argv[1]; - - if (strlen(name) > 32) { - fprintf(stderr, "\nString to large.\n\naddonctrl addon (start|stop|restart|reload|enable|disable)\n\n"); - exit(1); - } - - // Check if the input argument is valid - if (!is_valid_argument_alnum(name)) { - fprintf(stderr, "Invalid add-on name: %s\n", name); - exit(2); - } - - sprintf(command, "/opt/pakfire/db/installed/meta-%s", name); - FILE *fp = fopen(command,"r"); - if ( fp ) { - fclose(fp); - } else { - fprintf(stderr, "\nAddon '%s' not found.\n\naddonctrl addon (start|stop|restart|reload|status|enable|disable)\n\n", name); - exit(1); - } - - if (strcmp(argv[2], "start") == 0) { - snprintf(command, BUFFER_SIZE - 1, "/etc/rc.d/init.d/%s start", name); - safe_system(command); - } else if (strcmp(argv[2], "stop") == 0) { - snprintf(command, BUFFER_SIZE - 1, "/etc/rc.d/init.d/%s stop", name); - safe_system(command); - } else if (strcmp(argv[2], "restart") == 0) { - snprintf(command, BUFFER_SIZE - 1, "/etc/rc.d/init.d/%s restart", name); - safe_system(command); - } else if (strcmp(argv[2], "reload") == 0) { - snprintf(command, BUFFER_SIZE - 1, "/etc/rc.d/init.d/%s reload", name); - safe_system(command); - } else if (strcmp(argv[2], "status") == 0) { - snprintf(command, BUFFER_SIZE - 1, "/etc/rc.d/init.d/%s status", name); - safe_system(command); - } else if (strcmp(argv[2], "enable") == 0) { - snprintf(command, BUFFER_SIZE - 1, "mv -f /etc/rc.d/rc3.d/off/S??%s /etc/rc.d/rc3.d" , name); - safe_system(command); - } else if (strcmp(argv[2], "disable") == 0) { - snprintf(command, BUFFER_SIZE - 1, "mkdir -p /etc/rc.d/rc3.d/off"); - safe_system(command); - snprintf(command, BUFFER_SIZE - 1, "mv -f /etc/rc.d/rc3.d/S??%s /etc/rc.d/rc3.d/off" , name); - safe_system(command); - } else { - fprintf(stderr, "\nBad argument given.\n\naddonctrl addon (start|stop|restart|reload|enable|disable)\n\n"); - exit(1); - } - - return 0; + char **services = NULL; + int servicescnt = 0; + char *addon = argv[1]; + char *action = argv[2]; + char *service_filter = NULL; + int r = 0; + + if (!(initsetuid())) + exit(1); + + if (argc < 3) { + fprintf(stderr, "\nMissing arguments.\n\n%s\n\n", usage); + exit(1); + } + + // Ignore filter when list of services is requested + if (argc == 4 && strcmp(action, "list-services") != 0) + service_filter = argv[3]; + + if (strlen(addon) > 32) { + fprintf(stderr, "\nString too large.\n\n%s\n\n", usage); + exit(1); + } + + // Check if the input argument is valid + if (!is_valid_argument_alnum(addon)) { + fprintf(stderr, "Invalid add-on name: %s.\n", addon); + exit(2); + } + + // Get initscript name(s) from addon metadata + int rc = 0; + services = get_addon_services(addon, &servicescnt, service_filter, &rc); + if (!services) { + switch (rc) { + case -1: + fprintf(stderr, "\nSystem error occured. (Error: %m)\n\n"); + break; + + case 0: + if (service_filter) + fprintf(stderr, "\nNo service '%s' found for addon '%s'. Use 'list-services' to get a list of available services\n\n%s\n\n", service_filter, addon, usage); + else + fprintf(stderr, "\nAddon '%s' has no services.\n\n", addon); + break; + + case 1: + fprintf(stderr, "\nAddon '%s' not found.\n\n%s\n\n", addon, usage); + break; + } + exit(1); + } + + // Handle requested action + if (strcmp(action, "start") == 0 || + strcmp(action, "stop") == 0 || + strcmp(action, "restart") == 0 || + strcmp(action, "reload") == 0 || + strcmp(action, "status") == 0) { + + for(int i = 0; i < servicescnt; i++) { + if (initscript_action(services[i], action) < 0) { + r = 1; + fprintf(stderr, "\nSystem error occured. (Error: %m)\n\n"); + break; + } + } + + } else if (strcmp(action, "enable") == 0 || + strcmp(action, "disable") == 0) { + + for(int i = 0; i < servicescnt; i++) { + switch (r = toggle_service(services[i], action)) { + case -1: + fprintf(stderr, "\nSystem error occured. (Error: %m)\n\n"); + break; + + case 0: + printf("%sd service %s\n", action, services[i]); + break; + + case 1: + fprintf(stderr, "Service %s is already %sd. Skipping...\n", services[i], action); + break; + + case 2: + fprintf(stderr, "\nUnable to %s service %s. (Service has no valid runlevel symlink).\n\n", action, services[i]); + break; + } + + // Break from for loop in case of a system error. + if (r == -1) { + r = 1; + break; + } + } + + } else if (strcmp(action, "boot-status") == 0) { + // Print boot status for each service + for(int i = 0; i < servicescnt; i++) { + switch (get_boot_status(services[i])) { + case -1: + r = 1; + fprintf(stderr, "\nSystem error occured. (Error: %m)\n\n"); + break; + + case 0: + printf("%s is disabled on boot.\n", services[i]); + break; + + case 1: + printf("%s is enabled on boot.\n", services[i]); + break; + + case 2: + printf("%s is not available for boot. (Service has no valid symlink in either %s or %s).\n", services[i], enabled_path, disabled_path); + break; + } + + // Break from for loop in case of an error + if (r == 1) { + break; + } + } + + } else if (strcmp(action, "list-services") == 0) { + // List all services for addon + printf("\nServices for addon %s:\n", addon); + for(int i = 0; i < servicescnt; i++) { + printf(" %s\n", services[i]); + } + printf("\n"); + + } else { + fprintf(stderr, "\nBad argument given.\n\n%s\n\n", usage); + r = 1; + } + + // Cleanup + for(int i = 0; i < servicescnt; i++) + free(services[i]); + free(services); + + return r; }