/* * 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 "config.h" #include #include "cockpitdbusinternal.h" #include "common/cockpitmachinesjson.h" #include "common/cockpitsystem.h" #define MACHINES_SIG "a{sa{sv}}" /* counts the number of file change events that have not yet gotten a PropertiesChanged signal */ static guint pending_updates; GFileMonitor *machines_monitor; /* returns a floating GVariant */ static GVariant * get_machines (void) { JsonNode *machines; GError *error = NULL; GVariant *variant; machines = read_machines_json (); variant = json_gvariant_deserialize (machines, MACHINES_SIG, &error); /* if the signature does not match, we screwed up in the parser already */ g_assert (variant != NULL); json_node_free (machines); return variant; } static GVariant * machines_get_property (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { g_return_val_if_fail (property_name != NULL, NULL); if (g_str_equal (property_name, "Machines")) return get_machines (); else g_return_val_if_reached (NULL); } static void machines_method_call (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { if (g_str_equal (method_name, "Update")) { const gchar *filename, *hostname; GVariant * info_v; JsonNode *info_json = NULL; GError *error = NULL; g_variant_get (parameters, "(&s&s@a{sv})", &filename, &hostname, &info_v); info_json = json_gvariant_serialize (info_v); g_debug ("Updating %s for machine %s", filename, hostname); g_variant_unref (info_v); if (update_machines_json (filename, hostname, info_json, &error)) g_dbus_method_invocation_return_value (invocation, NULL); else g_dbus_method_invocation_take_error (invocation, error); json_node_free (info_json); } else g_return_if_reached (); } /** * notify_properties: * @user_data: GDBusConnection to which updates get sent * * Send a PropertiesChanged signal for invalidating the Machines property. This * avoids parsing files and constructing the property when nobody is listening. * This gets called in reaction to changed *.json configuration files. */ static gboolean notify_properties (gpointer user_data) { GDBusConnection *connection = user_data; GVariant *signal_value; GVariantBuilder builder; GError *error = NULL; /* reset pending counter before we do any actual work, to avoid races */ pending_updates = 0; g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); g_variant_builder_add (&builder, "s", "Machines"); signal_value = g_variant_ref_sink (g_variant_new ("(sa{sv}as)", "cockpit.Machines", NULL, &builder)); g_dbus_connection_emit_signal (connection, NULL, "/machines", "org.freedesktop.DBus.Properties", "PropertiesChanged", signal_value, &error); if (error != NULL) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED)) g_critical ("failed to send PropertiesChanged signal: %s", error->message); g_error_free (error); } g_variant_unref (signal_value); return G_SOURCE_REMOVE; } static void on_machines_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { gchar *path; /* ignore uninteresting events; note that DELETED does not get a followup CHANGES_DONE_HINT */ if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT && event_type != G_FILE_MONITOR_EVENT_DELETED) return; path = g_file_get_path (file); if (g_str_has_suffix (path, ".json")) { g_debug ("on_machines_changed: event type %i on %s", event_type, path); /* change events tend to come in batches, so slightly delay the re-reading of * files and sending PropertiesChanged; if we already have queued up an * update, don't queue it again */ if (pending_updates++ == 0) g_timeout_add (100, notify_properties, user_data); } else { g_debug ("on_machines_changed: ignoring event type %i on non-.json file %s", event_type, path); } g_free (path); } static void migrate_var_config (void) { const gchar *var_path = "/var/lib/cockpit/machines.json"; GError *error = NULL; /* TOCTOU, but if we really miss this, we'll migrate it the next time */ if (!g_file_test (var_path, G_FILE_TEST_IS_REGULAR)) { g_debug ("%s does not exist, nothing to migrate", var_path); return; } /* the directory should already exist (shipped by the package), but let's make sure */ if (g_mkdir_with_parents (get_machines_json_dir (), 0755) < 0) { g_message ("failed to create %s, Cockpit will not work properly: %m", get_machines_json_dir ()); return; } /* common case is to move it to 99-webui.json */ gchar *etc_path = g_build_filename (get_machines_json_dir (), "99-webui.json", NULL); GFile *var_file = g_file_new_for_path (var_path); GFile *etc_file = g_file_new_for_path (etc_path); if (g_file_move (var_file, etc_file, G_FILE_COPY_NONE, NULL, NULL, NULL, &error)) { g_info ("migrated %s to %s", var_path, etc_path); } else { if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { GError *error2 = NULL; /* most likely an interrupted/failed previous transition attempt; * don't clobber the existing file but move it to 98-migrated.json * instead */ g_free (etc_path); g_object_unref (etc_file); etc_path = g_build_filename (get_machines_json_dir (), "98-migrated.json", NULL); etc_file = g_file_new_for_path (etc_path); if (g_file_move (var_file, etc_file, G_FILE_COPY_NONE, NULL, NULL, NULL, &error2)) { g_info ("migrated %s to %s (99-webui.json already exists)", var_path, etc_path); } else { g_message ("moving of %s to %s failed: %s", var_path, etc_path, error2->message); g_error_free (error2); } } else /* different g_file_move() error than EXISTS */ { g_message ("migration of %s to %s failed: %s", var_path, etc_path, error->message); } g_error_free (error); } g_object_unref (etc_file); g_object_unref (var_file); g_free (etc_path); } static GDBusInterfaceVTable machines_vtable = { .method_call = machines_method_call, .get_property = machines_get_property, }; static GDBusPropertyInfo machines_property = { -1, "Machines", MACHINES_SIG, G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL }; static GDBusPropertyInfo *machines_properties[] = { &machines_property, NULL }; static GDBusArgInfo machines_update_filename_arg = { -1, "filename", "s", NULL }; static GDBusArgInfo machines_update_hostname_arg = { -1, "hostname", "s", NULL }; static GDBusArgInfo machines_update_info_arg = { -1, "info", "a{sv}", NULL }; static GDBusArgInfo *machines_update_args[] = { &machines_update_filename_arg, &machines_update_hostname_arg, &machines_update_info_arg, NULL }; static GDBusMethodInfo machines_update_method = { -1, "Update", machines_update_args, NULL, NULL }; static GDBusMethodInfo *machines_methods[] = { &machines_update_method, NULL }; static GDBusInterfaceInfo machines_interface = { -1, "cockpit.Machines", machines_methods, NULL, machines_properties, NULL }; void cockpit_dbus_machines_startup (void) { GDBusConnection *connection; GFile *machines_monitor_file; GError *error = NULL; connection = cockpit_dbus_internal_server (); g_return_if_fail (connection != NULL); g_dbus_connection_register_object (connection, "/machines", &machines_interface, &machines_vtable, NULL, NULL, &error); if (error != NULL) { g_critical ("couldn't register DBus cockpit.Machines object: %s", error->message); g_error_free (error); return; } /* only attempt this in a privileged bridge, otherwise we get confusing failure messages */ if (g_access ("/etc/cockpit", W_OK) >= 0) migrate_var_config (); /* watch for file changes and send D-Bus signal for it */ machines_monitor_file = g_file_new_for_path (get_machines_json_dir ()); machines_monitor = g_file_monitor (machines_monitor_file, G_FILE_MONITOR_NONE, NULL, &error); g_object_unref (machines_monitor_file); if (machines_monitor == NULL) { g_critical ("couldn't set up file watch: %s", error->message); g_error_free (error); return; } g_signal_connect (machines_monitor, "changed", G_CALLBACK (on_machines_changed), connection); g_object_unref (connection); } void cockpit_dbus_machines_cleanup (void) { g_object_unref (machines_monitor); }