diff options
author | Sergey Poznyakoff <gray@gnu.org> | 2020-12-07 10:53:05 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org> | 2020-12-07 10:53:05 +0200 |
commit | 217efcb974309cd373e9ab9e11573de8270e74e0 (patch) | |
tree | e291e493565e52c86a57d31d4401dc939f0bf581 | |
parent | b3a58380b93512b86fe02749425410d89a5655d4 (diff) | |
download | pies-217efcb974309cd373e9ab9e11573de8270e74e0.tar.gz pies-217efcb974309cd373e9ab9e11573de8270e74e0.tar.bz2 |
New API endpoint /alive. Improve documentation.
* doc/Makefile.am: Add ctl.texi
* doc/ctl.texi: New file.
* doc/pies.texi: Add anchors for ctl.texi
* src/ctl.c (json_error_reply_create): Set status depending on
the HTTP response code.
Use "message", instead of "error_message".
New endpoint: /alive
* src/inetd.c: Use grecs_getline instead of getline.
* src/sysvinit.c: Likewise.
* src/piesctl.c: Inspect "message", instead of "error_message".
-rw-r--r-- | doc/Makefile.am | 1 | ||||
-rw-r--r-- | doc/ctl.texi | 448 | ||||
-rw-r--r-- | doc/pies.texi | 10 | ||||
-rw-r--r-- | src/ctl.c | 65 | ||||
-rw-r--r-- | src/inetd.c | 2 | ||||
-rw-r--r-- | src/piesctl.c | 14 | ||||
-rw-r--r-- | src/sysvinit.c | 2 |
7 files changed, 528 insertions, 14 deletions
diff --git a/doc/Makefile.am b/doc/Makefile.am index 33ae9cf..5283a88 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -16,6 +16,7 @@ info_TEXINFOS=pies.texi pies_TEXINFOS=\ + ctl.texi\ fdl.texi\ inetd.texi\ macros.texi\ diff --git a/doc/ctl.texi b/doc/ctl.texi new file mode 100644 index 0000000..588cebe --- /dev/null +++ b/doc/ctl.texi @@ -0,0 +1,448 @@ +@c This is part of the GNU Pies manual. +@c Copyright (C) 2009--2020 Sergey Poznyakoff +@c This file is distributed under GFDL 1.3 or any later version +@c published by the Free Software Foundation. + + This appendix describes @dfn{control API} used to communicate with the +running @command{pies} daemon via the control interface +(@pxref{control}). This API is used by @command{piesctl} (@pxref{piesctl}). + +The API is designed as a REST service and uses HTTP. Queries are sent to +pies @dfn{endpoints}, each of which serves a distinct purpose. Data +are serialized using the JSON format. + +The sections below describe in detail each endpoint and associated +with it request types. + +@node /instance +@appendixsec /instance + This endpoint controls the state of the running @command{pies} +instance and accepts the following HTTP requests: @code{GET}, +@code{DELETE}, @code{POST} (or @code{PUT}). + +@deffn {Request} GET /instance + Retrieves information about the current instance. The response body +is a JSON object with the following attributes: + +@table @samp +@item PID +PID of the running daemon. + +@item argv +Array of the command line arguments. @samp{argv[0]} is the program +name. + +@item binary +Name of the @command{pies} binary. + +@item instance +The instance name. @xref{instances}. + +@item package +Package name (the string @samp{GNU Pies}). + +@item version +Package version +@end table + +Any of these can be used in the URI to request the information about +that particular attribute, e.g.: + +@example +GET /instance/argv @result{} @{"argv":["pies", "-x2"]@} +@end example +@end deffn + +@deffn {Request} DELETE /instance/PID +Stops the current @command{pies} instance. +@end deffn + +@deffn {Request} PUT /instance/PID +@deffnx {Request} POST /instance/PID +Restarts the current @command{pies} instance. +@end deffn + +@node /conf +@appendixsec /conf + +@node /conf/files +@appendixsubsec /conf/files + +@deffn {Request} GET /conf/files +Return list of configuration files. On success, a JSON array is +returned. Each array element is an object with two attributes: + +@defvr {Attr} string file +Pathname of the configuration file. +@end defvr + +@defvr {Attr} string syntax +Configuration file syntax (@pxref{Syntax}). +@end defvr + +For example: + +@example +GET /conf/files @result{} +[@{"file":"/etc/pies.conf", "syntax":"pies"@}, + @{"file":"/etc/inetd.conf", "syntax":"inetd"@}] +@end example +@end deffn + +@deffn {Request} POST /conf/files +Adds a new configuration file. The body must be a JSON object with +@samp{file} and @samp{syntax} attributes, as described above. The +@samp{file} value must contain a pathname of a configuration file +written in a syntax supplied by the @samp{syntax} attribute +(@pxref{Syntax}). + +This request returns 201 code on success. To actually parse and load +the added configuration file, send a @samp{PUT} request to +@samp{/conf/runtime} (@pxref{/conf/runtime}). +@end deffn + +@deffn {Request} DELETE /conf/files/true +Clears all previously configured configuration files. Responds with: + +@example +@{ "message":"file list cleared", "status":"OK" @} +@end example +@end deffn + +@deffn {Request} DELETE /conf/files/[@var{list}] +Removes files named in the @var{list} from the list of configuration files. + +The @samp{DELETE} response is 200 on success. To actually update the +configuration of the running process, send a @samp{PUT} request to +@samp{/conf/runtime} (@pxref{/conf/runtime}). +@end deffn + +@node /conf/runtime +@appendixsubsec /conf/runime + +This is a write-only URI. The only request supported is +@samp{PUT /conf/runtime}. It initiates reloading of the +@command{pies} configuration. Usually, this request is sent after one +or more @samp{POST} and/or @samp{DELETE} requests to +@samp{/conf/files}, in order to finalize the changes applied to the +configuration. + +@node /programs +@appendixsec /programs + + A request sent to this URI selects one or more components and +applies operation defined by the request type to all of them. + + Components are selected using a query in the form of JSON object +(a @dfn{selector}). Valid selectors are: + +@deffn {Selector} null +@deffnx {Selector} false +Matches nothing. +@end deffn + +@deffn {Selector} true +Matches all components. +@end deffn + +@deffn {Selector} @{ "op": "component", "arg": @var{tag} @} +Matches component with the given @var{tag} (@pxref{tag}). +@end deffn + +@deffn {Selector} @{ "op": "type", "arg": "component" @} +Matches all components. +@end deffn + +@deffn {Selector} @{ "op": "type", "arg": "command" @} +Matches all commands. +@end deffn + +@deffn {Selector} @{ "op": "mode", "arg": @var{mode} @} +Matches all components with the given @var{mode}. @xref{component +mode}. +@end deffn + +@deffn {Selector} @{ "op": "active" @} +Matches all active components. +@end deffn + +@deffn {Selector} @{ "op": "status", "arg": @var{status} @} +Matches all components with the given @var{status} (one of +@samp{stopped}, @samp{running}, @samp{listener}, @samp{sleeping}, +@samp{stopping}, @samp{finished}). @xref{component status} for a +discussion of these values. +@end deffn + +@deffn {Selector} @{ "op: "not", "arg": @var{condition} @} +Negates @var{condition}, which is any valid selector. +@end deffn + +@deffn {Selector} @{ "op": "and", "arg": @var{array} @} +Returns the result of logical conjunction on the @var{array} of selectors. +@end deffn + +@deffn {Selector} @{ "op": "or", "arg": @var{array} @} +Returns the result of logical disjunction on the @var{array} of selectors. +@end deffn + + For example, the following selector matches all components that are +in @samp{running} state, excepting components of @samp{inetd} mode: + +@example +@{ "op": "and", + "arg": [ @{ "op": "type", "arg": "component" @}, + @{ "op": "not", "arg": @{ "op": "mode", "arg": "inetd" @} + ] +@} +@end example + +The following requests are supported: + +@deffn {Request} GET /programs?@var{selector} +@deffnx {Request} GET /programs/@var{tag} + This request returns information about components matched by +@var{selector} (see below for the @samp{/programs/@var{tag} variant}. +The response is a JSON array of descriptions. If no component matches +the @var{selector}, empty array is returned. Each description is a +JSON object with the following attributes: + +@deftypevr {Attr} string type +Type of the described entity: @samp{component} for an instance of a +configured component, and @samp{command} for a command run as a part +of exit action (@pxref{Exit Actions}), including mailer invocations +(@pxref{Notification}). +@end deftypevr + +@deftypevr {Attr} string mode +Mode of the entity. @xref{component mode}. +@end deftypevr + +@anchor{component status} +@deftypevr {Attr} string status +Entity status. Possible values are: + +@table @code +@item finished +A @samp{once} or @samp{startup} component has finished. + +@item listener +Component is an inetd listener. + +@item running +Component is running. + +@item sleeping +Component has been put to sleep because of excessive number of +failures (@pxref{respawn}). + +@item stopped +Component is stopped. + +@item stopping +Component is being stopped (a @code{SIGTERM} was sent). +@end table +@end deftypevr + +@deftypevr {Attr} boolean active +Whether this component is active. By default, all components are +active, unless marked with a @samp{disable} flag (@pxref{flags}) or +administratively stopped. +@end deftypevr + +@deftypevr {Attr} integer PID +PID of the running process. +@end deftypevr + +@deftypevr {Attr} string URL +(for @samp{inetd} components) URL of the socket the component is +listening on. +@end deftypevr + +@deftypevr {Attr} string service +(for @samp{tcpmux} components) TCPMUX service name. @xref{TCPMUX}. +@end deftypevr + +@deftypevr {Attr} string master +(for @samp{tcpmux} components) Tag of master TCPMUX component. +@xref{TCPMUX}. +@end deftypevr + +@deftypevr {Attr} string runlevels +For inittab components, the string of runlevels this component is +configured to run in. @xref{Init Process}. +@end deftypevr + +@deftypevr {Attr} integer wakeup-time +If component is in the @samp{sleeping} state, this attribute gives the +number of seconds after which an attempt will be made to restart it. +@end deftypevr + +@deftypevr {Attr} array argv +Component command line split into words. +@end deftypevr + +@deftypevr {Attr} string command +Component command. +@end deftypevr +@end deffn + +@deffn {Request} DELETE /programs?@var{selector} +@deffnx {Request} DELETE /programs/@var{tag} +Stop components matched by the @var{selector}. On success returns: + +@example +@{ "status":"OK" @} +@end example + +@noindent +On failure, returns + +@example +@{ "status":"ER", "error_message": @var{text} @} +@end example + +@noindent +where @var{text} is a textual human-readable description of the failure. +@end deffn + +@deffn {Request} PUT /programs?@var{selector} +@deffnx {Request} PUT /programs/@var{tag} +Start components matched by @var{selector}. +@end deffn + +@deffn {Request} POST /programs +Restart components. The selector is supplied in the request content. +@end deffn + +Wherever a selector is passed via query parameters, a simplified form +with component tag passed as query path is also allowed. For example: + +@example + GET /programs/@var{tag} +@end example + +@noindent +is a shortcut for: + +@example +@{ "op":"and", + "arg":[ @{"op":"type", "arg":"component"@}, + @{"op":"component", "arg":@var{tag} @} ] @} +@end example + +@node /alive +@appendixsec /alive + + This entry point accepts only @samp{GET} requests. The URI must not +be empty and must not include sub-directories (parts separated with +slashes). It is treated as the name of the component to return the +status of. E.g. querying @samp{/alive/foo} returns the status of the +component named @samp{foo}. The status is returned as HTTP status +code: + +@table @asis +@item 200 +The component is up and running. For regular components that means +that the corresponding program is running. For @samp{inetd} +components that means that the listener is listening on the configured +socket. + +@item 403 +No component specified. + +@item 404 +There is no such component. + +@item 503 +The component is not running. This means that it has failed, or has +been stopped administratively or (for @samp{once} and @samp{startup} +components) that it has run once and finished. + +If the component has failed, the @samp{Retry-After:} HTTP header +contains the number of seconds after which @command{pies} will retry +starting this component. +@end table + +@node /runlevel +@appendixsec /runlevel + This URI is active when @command{pies} runs as init process +(@pxref{Init Process}). It supports two requests: + +@deffn {Request} GET /runlevel +Returns the current state of the program as a JSON object with the +following attributes: + +@defvr {Attr} string runlevel +Current runlevel. @xref{Runlevels}. +@end defvr + +@defvr {Attr} string prevlevel +Previous runlevel (@samp{N} if none). +@end defvr + +@defvr {Attr} string bootstate +Boot state. @xref{startup states}. +@end defvr + +@defvr {Attr} string initdefault +Default runlevel. +@end defvr +@end deffn + +@deffn {Request} PUT /runlevel/@{"runlevel":@var{L}@} +Initiates transition from the current runlevel to runlevel @var{L} +(@pxref{Runlevels}). +@end deffn + +@node /environ +@appendixsec /environ + This URI is active when @command{pies} runs as init process +(@pxref{Init Process}). It manipulates the program initial +environment, i.e. the environment that all programs inherit. +@xref{Init Environment}. + +@deffn {Request} GET /environ/ +Returns entire environment formatted as a JSON array of strings. On +success, the 200 response is returned: + +@example +["RUNLEVEL=3", "CONSOLE=/dev/tty", ...] +@end example +@end deffn + +@deffn {Request} GET /environ/@var{var} +Returns the value of the environment variable @var{var}, if such is +defined. On success, the 200 response carries the object: + +@example +@{ "status":"OK", "value":@var{string} @} +@end example + +@noindent +If the variable @var{var} is not defined, a 404 response is returned. +On error, a 403 response is returned. In both cases, the response +body is the usual @command{pies} diagnostics object: + +@example +@{ "status":"ER", "error_message":@var{text} @} +@end example +@end deffn + +@deffn {Request} DELETE /environ/@var{var} +Deletes from the environment the variable @var{var}. On success, +responds with HTTP 200: + +@example +@{ "status":"OK" @} +@end example + +Error responses are the same as for @samp{GET}. +@end deffn + +@deffn {Request} PUT /environ/@var{name}=@var{value} +Initializes environment variable @var{name} to @var{value}. See +@samp{GET} for the possible responses. +@end deffn + + + diff --git a/doc/pies.texi b/doc/pies.texi index 1d65cf4..502b513 100644 --- a/doc/pies.texi +++ b/doc/pies.texi @@ -93,6 +93,7 @@ Appendices * inetd configuration:: @file{Inetd.conf} Format. * User-Group ACLs:: +* REST API:: * Copying This Manual:: The GNU Free Documentation License. * Concept Index:: Index of Concepts. @@ -729,6 +730,9 @@ component @var{tag} @{ @} @end example +@cindex component tag +@cindex tag (component) +@anchor{tag} The component is identified by its @dfn{tag}, which is given as argument to the @code{component} keyword. Component declarations with the same tags are merged into a single declaration. @@ -736,6 +740,7 @@ the same tags are merged into a single declaration. The following are the basic statements which are allowed within the @code{component} block: +@anchor{component mode} @deffn {Config: component} mode @var{mode} Declare the type (style) of the component. Following are the basic values for @var{mode}: @@ -3458,6 +3463,7 @@ with another @samp{init} daemons, and the latter is in native @command{pies} format (@pxref{Syntax}). Either of the files or even both of them can be missing. +@anchor{startup states} The startup process passes through several states. Transition between states is controlled by @dfn{runlevel}, which also defines the set of components that must be executed. Startup states are: @@ -4514,6 +4520,10 @@ line options used). @appendix User-Group ACLs @include usr-acl.texi +@node REST API +@appendix REST API +@include ctl.texi + @node Copying This Manual @appendix GNU Free Documentation License @include fdl.texi @@ -479,13 +479,13 @@ json_object_set_bool (struct json_value *obj, char const *name, int val) } static struct json_value * -json_error_reply_create (const char *msg) +json_error_reply_create (int http_code, const char *msg) { struct json_value *val; val = json_reply_create (); - json_object_set_string (val, "status", "ER"); - json_object_set_string (val, "error_message", "%s", msg); + json_object_set_string (val, "status", http_code < 300 ? "OK" : "ER"); + json_object_set_string (val, "message", "%s", msg); return val; } @@ -607,11 +607,11 @@ ctlio_reply (struct ctlio *io, int code, const char *fmt, ...) grecs_vasprintf (&str, &len, fmt, ap); va_end (ap); - io->output.reply = json_error_reply_create (str); + io->output.reply = json_error_reply_create (code, str); grecs_free (str); } else - io->output.reply = json_error_reply_create (http_text (code)); + io->output.reply = json_error_reply_create (code, http_text (code)); } static void @@ -918,6 +918,8 @@ static void res_instance (struct ctlio *, enum http_method, char const *, struct json_value *); static void res_programs (struct ctlio *, enum http_method, char const *, struct json_value *); +static void res_alive (struct ctlio *, enum http_method, char const *, + struct json_value *); static void res_conf (struct ctlio *, enum http_method, char const *, struct json_value *); @@ -946,6 +948,8 @@ static struct ctlio_resource restab[] = { { S(/conf), CTL_ADMIN_STATE, NULL, res_conf }, { S(/programs), CTL_ADMIN_STATE|CTL_USER_STATE, NULL, res_programs }, + { S(/alive), CTL_ADMIN_STATE|CTL_USER_STATE, NULL, + res_alive }, #if PIES_SYSVINIT_ENABLED { S(/runlevel), CTL_ADMIN_STATE, pred_sysvinit, res_runlevel }, { S(/environ), CTL_ADMIN_STATE, pred_sysvinit, res_environ }, @@ -2113,6 +2117,57 @@ res_programs (struct ctlio *io, enum http_method meth, else res_programs_select (io, meth, uri, json); } + +static void +res_alive (struct ctlio *io, enum http_method meth, char const *uri, + struct json_value *json) +{ + struct prog *prog; + + if (!uri) + { + ctlio_reply (io, 403, NULL); + return; + } + + prog = progman_locate (uri + 1); + + if (prog && IS_COMPONENT (prog)) + { + switch (prog->v.p.status) + { + case status_running: + case status_listener: + /* FIXME: Presumably these three fall into that category too: + status_stopped -- Component is stopped administratively + status_stopping -- Component is being stopped + status_finished -- A "once" or "startup" component has finished + */ + ctlio_reply (io, 200, NULL); + break; + + case status_sleeping: + { + time_t t; + char buf[80]; + + t = time (NULL) - prog->v.p.timestamp; + if (t <= SLEEPTIME) + { + snprintf (buf, sizeof buf, "%lu", SLEEPTIME - t); + output_set_header (&io->output, "Retry-After", buf); + } + } + /* fall through */ + + default: + ctlio_reply (io, 503, NULL); + break; + } + } + else + ctlio_reply (io, 404, NULL); +} #if PIES_SYSVINIT_ENABLED static int diff --git a/src/inetd.c b/src/inetd.c index 55bc2f1..b7ae0eb 100644 --- a/src/inetd.c +++ b/src/inetd.c @@ -96,7 +96,7 @@ inetd_conf_file (const char *file) } wsflags = WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_WS | WRDSF_SQUEEZE_DELIMS; - while (getline (&buf, &size, fp) >= 0) + while (grecs_getline (&buf, &size, fp) >= 0) { char *p; struct component *comp; diff --git a/src/piesctl.c b/src/piesctl.c index 58f9ad2..969d0e5 100644 --- a/src/piesctl.c +++ b/src/piesctl.c @@ -744,7 +744,7 @@ shttp_print_error (struct shttp_connection *conn) { struct json_value *jv; - if (conn->result && (jv = json_value_lookup (conn->result, "error_message"))) + if (conn->result && (jv = json_value_lookup (conn->result, "message"))) { if (jv->type == json_string) grecs_error (NULL, 0, "%s", jv->v.s); @@ -1477,7 +1477,7 @@ shttp_print_response_status (struct shttp_connection *conn) { if (strcmp (v->v.s, "OK") == 0) fputs (v->v.s, stdout); - else if (json_object_get_type (elt, "error_message", + else if (json_object_get_type (elt, "message", json_string, &v) == 0) fputs (v->v.s, stdout); else @@ -1756,7 +1756,7 @@ telinit_format_environ (struct shttp_connection *conn) } else if (strcmp (val->v.s, "ER") == 0) { - if (json_object_get_type (conn->result, "error_message", + if (json_object_get_type (conn->result, "message", json_string, &val) == 0) fputs (val->v.s, stderr); else @@ -1957,7 +1957,7 @@ conf_reload (struct cmdline_parser_state *state) val = json_object_require_type (conn->result, "status", json_string); if (strcmp (val->v.s, "ER") == 0) { - if (json_object_get_type (conn->result, "error_message", + if (json_object_get_type (conn->result, "message", json_string, &val) == 0) fputs (val->v.s, stdout); else @@ -2032,7 +2032,7 @@ conf_file_clear (struct cmdline_parser_state *state) val = json_object_require_type (conn->result, "status", json_string); if (strcmp (val->v.s, "OK")) { - if (json_object_get_type (conn->result, "error_message", + if (json_object_get_type (conn->result, "message", json_string, &val) == 0) fputs (val->v.s, stderr); else @@ -2084,7 +2084,7 @@ conf_file_del (struct cmdline_parser_state *state) val = json_object_require_type (conn->result, "status", json_string); if (strcmp (val->v.s, "OK") == 0) { - if (json_object_get_type (conn->result, "error_message", + if (json_object_get_type (conn->result, "message", json_string, &val) == 0) { fputs (val->v.s, stderr); @@ -2109,7 +2109,7 @@ conf_file_del (struct cmdline_parser_state *state) } else { - if (json_object_get_type (conn->result, "error_message", + if (json_object_get_type (conn->result, "message", json_string, &val) == 0) fputs (val->v.s, stderr); else diff --git a/src/sysvinit.c b/src/sysvinit.c index f63687e..a9681e0 100644 --- a/src/sysvinit.c +++ b/src/sysvinit.c @@ -1167,7 +1167,7 @@ inittab_parse (const char *file) fp = fopen (file, "r"); if (fp) { - while (getline (&ctx.buf, &ctx.size, fp) >= 0) + while (grecs_getline (&ctx.buf, &ctx.size, fp) >= 0) { enum inittab_status st = inittab_parse_line (&ctx); if (st == inittab_err) |