/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include "cockpitmachinesjson.h"
#include
#include
static int
glob_err_func (const char *epath,
int eerrno)
{
/* Should Not Happen™ -- log the error for debugging */
if (eerrno != ENOENT)
g_warning ("%s: cannot read: %s", epath, g_strerror (eerrno));
return 0;
}
static JsonNode *
new_object_node (void)
{
JsonNode *n = json_node_new (JSON_NODE_OBJECT);
json_node_take_object (n, json_object_new ());
return n;
}
static JsonNode *
parse_json_file (const char *path)
{
JsonParser *parser = NULL;
GError *error = NULL;
gboolean success;
JsonNode *result = NULL;
parser = json_parser_new ();
success = json_parser_load_from_file (parser, path, &error);
if (success)
{
result = json_parser_get_root (parser);
/* root is NULL if the file is empty */
if (result != NULL)
{
if (JSON_NODE_HOLDS_OBJECT (result))
{
result = json_node_copy (result);
}
else
{
g_message ("%s: does not contain a JSON object, ignoring", path);
result = NULL;
}
}
}
else
{
if (error->code != G_FILE_ERROR_NOENT)
g_message ("%s: invalid JSON: %s", path, error->message);
g_error_free (error);
}
g_object_unref (parser);
return result;
}
static gboolean
write_json_file (JsonNode *config, const char *path, GError **error)
{
JsonGenerator *json_gen;
gboolean res;
json_gen = json_generator_new ();
json_generator_set_root (json_gen, config);
json_generator_set_pretty (json_gen, TRUE); /* bikeshed zone */
res = json_generator_to_file (json_gen, path, error);
g_object_unref (json_gen);
return res;
}
static void
merge_config (JsonObject *machines,
JsonObject *delta,
const char *path)
{
GList *hosts = json_object_get_members (delta);
for (GList *i = g_list_first (hosts); i; i = g_list_next (i))
{
const char *hostname = i->data;
JsonNode *delta_props = json_object_get_member (delta, hostname);
if (!JSON_NODE_HOLDS_OBJECT (delta_props))
{
g_message ("%s: host name definition %s does not contain a JSON object, ignoring", path, hostname);
continue;
}
/* merge delta properties info existing machines host */
if (!json_object_has_member (machines, hostname))
json_object_set_member (machines, hostname, new_object_node ());
JsonObject *machine_props = json_object_get_object_member (machines, hostname);
g_debug ("%s: merging updates to host name %s", path, hostname);
GList *proplist = json_object_get_members (json_node_get_object (delta_props));
for (GList *p = g_list_first (proplist); p; p = g_list_next (p))
{
const char *propname = p->data;
JsonNode *prop_node = json_object_get_member (json_node_get_object (delta_props), propname);
if (!JSON_NODE_HOLDS_VALUE (prop_node))
{
g_message ("%s: host name definition %s: property %s does not contain a simple value, ignoring", path, hostname, propname);
continue;
}
g_debug ("%s: host name %s: merging property %s", path, hostname, propname);
json_object_set_member (machine_props, propname, json_node_copy (prop_node));
}
g_list_free (proplist);
}
g_list_free (hosts);
}
const char *
get_machines_json_dir (void)
{
static gchar *path = NULL;
if (path == NULL)
path = g_strdup_printf ("%s/machines.d", g_getenv ("COCKPIT_TEST_CONFIG_DIR") ?: "/etc/cockpit");
return path;
}
JsonNode *
read_machines_json (void)
{
gchar *glob_str;
glob_t conf_glob = { .gl_offs = 1 };
int res;
JsonNode *machines = NULL;
/* find json config files */
glob_str = g_build_filename (get_machines_json_dir (), "*.json", NULL);
res = glob (glob_str, GLOB_DOOFFS, glob_err_func, &conf_glob);
if (G_UNLIKELY (res != 0 && res != GLOB_NOMATCH))
{
g_critical ("glob %s failed with return code %i", glob_str, res);
globfree (&conf_glob);
g_free (glob_str);
return NULL;
}
/* also read /var/lib/cockpit/machines.json for backwards compat; except when
* running unit tests, then disable this (this is covered by an integration test) */
conf_glob.gl_pathv[0] = g_getenv ("COCKPIT_TEST_CONFIG_DIR") ? "/dev/null" : "/var/lib/cockpit/machines.json";
/* start with an empty object */
machines = new_object_node ();
for (size_t i = 0; i < conf_glob.gl_pathc + 1; ++i)
{
JsonNode *j = parse_json_file (conf_glob.gl_pathv[i]);
if (j)
{
merge_config (json_node_get_object (machines), json_node_get_object (j), conf_glob.gl_pathv[i]);
json_node_free (j);
}
}
globfree (&conf_glob);
g_free (glob_str);
return machines;
}
/* iterator function for update_machines_json() */
static void
update_machine_property (JsonObject *object,
const gchar *member_name,
JsonNode *member_node,
gpointer user_data)
{
json_object_set_member ((JsonObject *) user_data, member_name, json_node_copy (member_node));
}
gboolean
update_machines_json (const char *filename,
const char *hostname,
JsonNode *info,
GError **error)
{
gchar *path;
JsonNode *cur_config;
JsonObject *cur_config_obj;
JsonNode *cur_props;
gboolean res;
g_assert (JSON_NODE_HOLDS_OBJECT (info));
path = g_build_filename (get_machines_json_dir (), filename, NULL);
cur_config = parse_json_file (path);
if (cur_config == NULL)
cur_config = new_object_node ();
cur_config_obj = json_node_get_object (cur_config);
cur_props = json_object_get_member (cur_config_obj, hostname);
if (cur_props)
{
/* update settings for hostname */
g_assert (JSON_NODE_HOLDS_OBJECT (cur_props));
json_object_foreach_member (json_node_get_object (info), update_machine_property, json_node_get_object (cur_props));
}
else
{
/* create new entry for host name */
json_object_set_member (cur_config_obj, hostname, json_node_copy (info));
}
res = write_json_file (cur_config, path, error);
g_free (path);
json_node_free (cur_config);
return res;
}