Permalink
Fetching contributors…
Cannot retrieve contributors at this time
428 lines (385 sloc) 11.3 KB
/*
* Copyright (c) 2014-2016 Red Hat.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "pmapi.h"
#include "impl.h"
#include "pmda.h"
#include "root.h"
#include "jsmn.h"
#include "docker.h"
/*
* JSMN helper interfaces for efficiently extracting JSON configs
*/
static int
jsmneq(const char *js, jsmntok_t *tok, const char *s)
{
if (tok->type != JSMN_STRING)
return -1;
if (strlen(s) == tok->end - tok->start &&
strncasecmp(js + tok->start, s, tok->end - tok->start) == 0)
return 0;
return -1;
}
static int
jsmnflag(const char *js, jsmntok_t *tok, int *bits, int flag)
{
if (tok->type != JSMN_PRIMITIVE)
return -1;
if (strncmp(js + tok->start, "true", sizeof("true")-1) == 0)
*bits |= flag;
else
*bits &= ~flag;
return 0;
}
static int
jsmnint(const char *js, jsmntok_t *tok, int *value)
{
char buffer[64];
if (tok->type != JSMN_PRIMITIVE)
return -1;
strncpy(buffer, js + tok->start, tok->end - tok->start);
buffer[tok->end - tok->start] = '\0';
*value = (int)strtol(buffer, NULL, 0);
return 0;
}
static int
jsmnstrdup(const char *js, jsmntok_t *tok, char **name)
{
char *s = *name;
if (tok->type != JSMN_STRING)
return -1;
if (s)
free(s);
s = strndup(js + tok->start, tok->end - tok->start);
return ((*name = s) == NULL) ? -1 : 0;
}
/*
* Heuristic to determine if systemd cgroup names will be used,
* or not. Uglee, but appears to be no better way (previously,
* we tested for /sys/fs/cgroup/systemd existance but turns out
* the partial systemd installations of some distributions also
* satisfy that test).
*/
static int
test_systemd_init(void)
{
FILE *fp = fopen("/proc/1/cmdline", "r");
char line[64];
int count;
if (!fp)
return 0;
count = fscanf(fp, "%63s", line);
fclose(fp);
if (count == 1 && strstr(line, "systemd") != NULL)
return 1;
return 0;
}
/*
* Container implementation for the Docker engine
*
* Currently uses direct access to the /var/lib/docker/container state
* to discover local containers. We may want to switch over to using
* the Docker daemon (Unix socket) JSON API at some point though if we
* can get everything needed there. That'd also mean the daemon must
* be running to get at the data, which also may be a problem (we can
* fallback to using the code below in that case though?). Anyway, it
* is early days still...
*/
void
docker_setup(container_engine_t *dp)
{
static const char *docker_default = "/var/lib/docker";
const char *systemd_cgroup = getenv("PCP_SYSTEMD_CGROUP");
const char *docker = getenv("PCP_DOCKER_DIR");
/* determine the location of docker container config.json files */
if (!docker)
docker = docker_default;
snprintf(dp->path, sizeof(dp->path), "%s/containers", docker);
dp->path[sizeof(dp->path)-1] = '\0';
/* heuristic to determine which cgroup naming convention in use */
if (systemd_cgroup || test_systemd_init())
dp->state |= CONTAINER_STATE_SYSTEMD;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_setup: path %s, %s suffix\n", dp->path,
(dp->state & CONTAINER_STATE_SYSTEMD)? "systemd" : "default");
}
int
docker_indom_changed(container_engine_t *dp)
{
static int lasterrno;
static struct stat lastsbuf;
struct stat statbuf;
if (stat(dp->path, &statbuf) != 0) {
if (oserror() == lasterrno)
return 0;
lasterrno = oserror();
return 1;
}
lasterrno = 0;
if (!root_stat_time_differs(&statbuf, &lastsbuf))
return 0;
lastsbuf = statbuf;
return 1;
}
void
docker_insts_refresh(container_engine_t *dp, pmInDom indom)
{
DIR *rundir;
char *path;
struct dirent *drp;
container_t *cp;
int sts;
if ((rundir = opendir(dp->path)) == NULL) {
if (pmDebug & DBG_TRACE_ATTR)
fprintf(stderr, "%s: skipping docker path %s\n",
pmProgname, dp->path);
return;
}
while ((drp = readdir(rundir)) != NULL) {
if (*(path = &drp->d_name[0]) == '.')
continue;
sts = pmdaCacheLookupName(indom, path, NULL, (void **)&cp);
if (sts == PMDA_CACHE_ACTIVE)
continue;
/* allocate space for values for this container and update indom */
if (sts != PMDA_CACHE_INACTIVE) {
if (pmDebug & DBG_TRACE_ATTR)
fprintf(stderr, "%s: adding docker container %s\n",
pmProgname, path);
if ((cp = calloc(1, sizeof(container_t))) == NULL)
continue;
cp->engine = dp;
snprintf(cp->cgroup, sizeof(cp->cgroup),
(dp->state & CONTAINER_STATE_SYSTEMD) ?
"/system.slice/docker-%s.scope" : "/docker/%s",
path);
}
pmdaCacheStore(indom, PMDA_CACHE_ADD, path, cp);
}
closedir(rundir);
}
static int
docker_values_changed(const char *path, container_t *values)
{
struct stat statbuf;
if (stat(path, &statbuf) != 0) {
memset(&values->stat, 0, sizeof(values->stat));
return 1;
}
if (!root_stat_time_differs(&statbuf, &values->stat))
return 0;
values->stat = statbuf;
return 1;
}
static int
docker_values_extract(const char *js, jsmntok_t *t, size_t count,
int key, container_t *values)
{
int i, j;
if (count == 0)
return 0;
switch (t->type) {
case JSMN_PRIMITIVE:
return 1;
case JSMN_STRING:
/*
* We're only interested in a handful of values:
* "Name": "/jolly_sinoussi",
* "State": { "Running": true, "Paused": false, "Restarting": false,
* "Pid": 32471 }
*/
if (key) {
jsmntok_t *value = t + 1;
if (t->parent == 0) { /* top-level: look for Name & State */
if (jsmneq(js, t, "Name") == 0) {
jsmnstrdup(js, value, &values->name);
values->uptodate++;
}
if (jsmneq(js, t, "State") == 0) {
values->state = (value->type == JSMN_OBJECT);
values->uptodate++;
}
}
else if (values->state) { /* pick out various stateful values */
int flag = values->flags;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_values_parse: state\n");
if (jsmneq(js, t, "Running") == 0)
jsmnflag(js, value, &flag, CONTAINER_FLAG_RUNNING);
else if (jsmneq(js, t, "Paused") == 0)
jsmnflag(js, value, &flag, CONTAINER_FLAG_PAUSED);
else if (jsmneq(js, t, "Restarting") == 0)
jsmnflag(js, value, &flag, CONTAINER_FLAG_RESTARTING);
else if (jsmneq(js, t, "Pid") == 0) {
if (jsmnint(js, value, &values->pid) < 0)
values->pid = -1;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_value PID=%d\n",
values->pid);
}
values->flags = flag;
}
}
return 1;
case JSMN_OBJECT:
for (i = j = 0; i < t->size; i++) {
j += docker_values_extract(js, t+1+j, count-j, 1, values); /* key */
j += docker_values_extract(js, t+1+j, count-j, 0, values); /*value*/
}
values->state = 0;
return j + 1;
case JSMN_ARRAY:
for (i = j = 0; i < t->size; i++)
j += docker_values_extract(js, t+1+j, count-j, 0, values);
return j + 1;
default:
return 0;
}
return 0;
}
static int
docker_values_parse(FILE *fp, const char *name, container_t *values)
{
static char *js;
static int jslen;
static int tokcount;
static jsmntok_t *tok;
jsmn_parser p;
char buf[BUFSIZ];
int n, sts = 0, eof_expected = 0;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_values_parse: name=%s\n", name);
if (!tok) {
tokcount = 128;
if ((tok = calloc(tokcount, sizeof(*tok))) == NULL)
return -ENOMEM;
}
if (jslen)
jslen = 0;
jsmn_init(&p);
values->uptodate = 0; /* values for this container not yet visible */
values->state = -1; /* reset State key marker for this iteration */
for (;;) {
/* Read another chunk */
n = fread(buf, 1, sizeof(buf), fp);
if (n < 0) {
if (pmDebug & DBG_TRACE_ATTR) {
fprintf(stderr, "%s: failed read on docker %s config: %s\n",
pmProgname, name, osstrerror());
sts = -oserror();
break;
}
}
if (n == 0) {
if (!eof_expected) {
if (pmDebug & DBG_TRACE_ATTR)
fprintf(stderr, "%s: unexpected EOF on %s config: %s\n",
pmProgname, name, osstrerror());
sts = -EINVAL;
break;
}
return 0;
}
if ((js = realloc(js, jslen + n + 1)) == NULL) {
sts = -ENOMEM;
break;
}
strncpy(js + jslen, buf, n);
jslen = jslen + n;
again:
n = jsmn_parse(&p, js, jslen, tok, tokcount);
if (n < 0) {
if (n == JSMN_ERROR_NOMEM) {
tokcount = tokcount * 2;
if ((tok = realloc(tok, sizeof(*tok) * tokcount)) == NULL) {
sts = -ENOMEM;
break;
}
goto again;
}
} else {
sts = docker_values_extract(js, tok, p.toknext, 0, values);
eof_expected = 1;
}
}
return sts;
}
/*
* Extract critical information (PID1, state) for a named container.
* Name here is the unique identifier we've chosen to use for Docker
* container external instance names (i.e. long hash names).
*/
int
docker_value_refresh(container_engine_t *dp,
const char *name, container_t *values)
{
int sts;
FILE *fp;
char path[MAXPATHLEN];
snprintf(path, sizeof(path), "%s/%s/config.json", dp->path, name);
if (!docker_values_changed(path, values))
return 0;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_value_refresh: file=%s\n", path);
if ((fp = fopen(path, "r")) == NULL)
return -oserror();
sts = docker_values_parse(fp, name, values);
fclose(fp);
if (sts < 0)
return sts;
if (pmDebug & DBG_TRACE_ATTR)
__pmNotifyErr(LOG_DEBUG, "docker_value_refresh: uptodate=%d of %d\n",
values->uptodate, NUM_UPTODATE);
return values->uptodate == NUM_UPTODATE ? 0 : PM_ERR_AGAIN;
}
/*
* Given two strings, determine if they identify the same container.
* This is called iteratively, passing over all container instances.
*
* For Docker we need to match user-supplied names (which Docker will
* have assigned itself, if none given - see 'docker ps'), optionally
* slash-prefixed (for some reason Docker does this? *shrug*). Also,
* we want to allow for matching on the container hash identifiers we
* use for the external names (or unique-prefix components thereof).
*
* Use a simple ranking scheme - the closer the match, the higher the
* return value, up to a maximum of 100% (zero => no match).
*
* 'query' - the name supplied by the PCP monitoring tool.
* 'username' - the name from the container_t -> name field.
* 'instname' - the external instance identifier, lengthy hash.
*/
int
docker_name_matching(struct container_engine *dp, const char *query,
const char *username, const char *instname)
{
unsigned int ilength, qlength, limit;
int i, fuzzy = 0;
if (strcmp(query, username) == 0)
return 100;
if (username[0] == '/' && strcmp(query, username+1) == 0)
return 99;
if (strcmp(query, instname) == 0)
return 98;
qlength = strlen(query);
ilength = strlen(instname);
/* find the shortest of the three boundary conditions */
if ((limit = (qlength < ilength) ? qlength : ilength) > 95)
limit = 95;
for (i = 0; i < limit; i++) {
if (query[i] != instname[i])
break;
fuzzy++; /* bump for each matching character */
}
return fuzzy;
}