diff --git a/20241202_berrange_qga_implement_a_guest_get_load_command.mbx b/20241202_berrange_qga_implement_a_guest_get_load_command.mbx new file mode 100644 index 0000000..72aefd5 --- /dev/null +++ b/20241202_berrange_qga_implement_a_guest_get_load_command.mbx @@ -0,0 +1,131 @@ +From git@z Thu Jan 1 00:00:00 1970 +Subject: [PATCH] qga: implement a 'guest-get-load' command +From: "Daniel P. Berrangé" +Date: Mon, 02 Dec 2024 12:19:27 +0000 +Message-Id: <20241202121927.864335-1-berrange@redhat.com> +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Provide a way to report the process load average, via a new +'guest-get-load' command. + +This is only implemented for POSIX platforms providing 'getloadavg'. + +Example illustrated with qmp-shell: + +(QEMU) guest-get-load +{ + "return": { + "load15m": 1.546875, + "load1m": 1.669921875, + "load5m": 1.9306640625 + } +} + +Windows has no native equivalent API, but it would be possible to +simulate it as illustrated here (BSD-3-Clause): + + https://github.com/giampaolo/psutil/pull/1485 + +This is left as an exercise for future contributors. + +Signed-off-by: Daniel P. Berrangé +Reviewed-by: Konstantin Kostiuk +--- + meson.build | 1 + + qga/commands-posix.c | 20 ++++++++++++++++++++ + qga/qapi-schema.json | 37 +++++++++++++++++++++++++++++++++++++ + 3 files changed, 58 insertions(+) + +diff --git a/meson.build b/meson.build +index a290dbfa33..9c65e56fff 100644 +--- a/meson.build ++++ b/meson.build +@@ -2619,6 +2619,7 @@ config_host_data.set('CONFIG_SETNS', cc.has_function('setns') and cc.has_functio + config_host_data.set('CONFIG_SYNCFS', cc.has_function('syncfs')) + config_host_data.set('CONFIG_SYNC_FILE_RANGE', cc.has_function('sync_file_range')) + config_host_data.set('CONFIG_TIMERFD', cc.has_function('timerfd_create')) ++config_host_data.set('CONFIG_GETLOADAVG', cc.has_function('getloadavg')) + config_host_data.set('HAVE_COPY_FILE_RANGE', cc.has_function('copy_file_range')) + config_host_data.set('HAVE_GETIFADDRS', cc.has_function('getifaddrs')) + config_host_data.set('HAVE_GLIB_WITH_SLICE_ALLOCATOR', glib_has_gslice) +diff --git a/qga/commands-posix.c b/qga/commands-posix.c +index 636307bedf..6e3c15f539 100644 +--- a/qga/commands-posix.c ++++ b/qga/commands-posix.c +@@ -1368,3 +1368,23 @@ char *qga_get_host_name(Error **errp) + + return g_steal_pointer(&hostname); + } ++ ++#ifdef CONFIG_GETLOADAVG ++GuestLoadAverage *qmp_guest_get_load(Error **errp) ++{ ++ double loadavg[3]; ++ GuestLoadAverage *ret = NULL; ++ ++ if (getloadavg(loadavg, G_N_ELEMENTS(loadavg)) < 0) { ++ error_setg_errno(errp, errno, ++ "cannot query load average"); ++ return NULL; ++ } ++ ++ ret = g_new0(GuestLoadAverage, 1); ++ ret->load1m = loadavg[0]; ++ ret->load5m = loadavg[1]; ++ ret->load15m = loadavg[2]; ++ return ret; ++} ++#endif +diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json +index 0537bb7886..995594aaf4 100644 +--- a/qga/qapi-schema.json ++++ b/qga/qapi-schema.json +@@ -1843,6 +1843,43 @@ + 'if': 'CONFIG_LINUX' + } + ++ ++## ++# @GuestLoadAverage: ++# ++# Statistics about process load information ++# ++# @load1m: 1-minute load avage ++# ++# @load5m: 5-minute load avage ++# ++# @load15m: 15-minute load avage ++# ++# Since: 10.0 ++## ++{ 'struct': 'GuestLoadAverage', ++ 'data': { ++ 'load1m': 'number', ++ 'load5m': 'number', ++ 'load15m': 'number' ++ }, ++ 'if': 'CONFIG_GETLOADAVG' ++} ++ ++## ++# @guest-get-load: ++# ++# Retrieve CPU process load information ++# ++# Returns: load information ++# ++# Since: 10.0 ++## ++{ 'command': 'guest-get-load', ++ 'returns': 'GuestLoadAverage', ++ 'if': 'CONFIG_GETLOADAVG' ++} ++ + ## + # @GuestNetworkRoute: + # +-- +2.46.0 + diff --git a/mingw-qemu-ga-win.spec b/mingw-qemu-ga-win.spec index 992f1ea..07b55cd 100644 --- a/mingw-qemu-ga-win.spec +++ b/mingw-qemu-ga-win.spec @@ -7,7 +7,7 @@ Name: mingw-qemu-ga-win Version: 109.1.0 -Release: 7%{?dist} +Release: 8%{?dist} Summary: Qemus Guest agent for Windows Group: System Environment/Daemons @@ -20,6 +20,8 @@ Source0: https://gitlab.com/qemu-project/qemu/-/archive/v%{qemu_version}/qemu-v% Patch0001: 0001-Change-Version.patch Patch0002: 20241210_demeng_qemu_ga_win_fix_a_typo_error.mbx +Patch0003: 20241202_berrange_qga_implement_a_guest_get_load_command.mbx +Patch0004: v2_20250324_kkostiuk_qga_add_guest_get_load_command.mbx BuildArch: noarch # RHEL-57753 - mingw-qemu-ga-win failed to build on s390x @@ -70,6 +72,8 @@ This package does not need to be installed on the host OS. %setup -q -n qemu-v%{qemu_version} %patch0001 -p1 %patch0002 -p1 +%patch0003 -p1 +%patch0004 -p1 %build @@ -126,6 +130,9 @@ cp build/qga/qemu-ga-x86_64.msi $RPM_BUILD_ROOT%{mingw64_bindir} %{mingw64_bindir}/qemu-ga* %changelog +* Mon Mar 17 2025 Konstantin Kostiuk 109.1.0-8 +- RHEL-71884 - [qemu-guest-agent][RFE] Report CPU load average for Windows VMs + * Mon Jan 20 2025 Konstantin Kostiuk 109.1.0-7 - RHEL-74469 - Rebuild mingw-qemu-ga-win package due to deps update diff --git a/v2_20250324_kkostiuk_qga_add_guest_get_load_command.mbx b/v2_20250324_kkostiuk_qga_add_guest_get_load_command.mbx new file mode 100644 index 0000000..79a216e --- /dev/null +++ b/v2_20250324_kkostiuk_qga_add_guest_get_load_command.mbx @@ -0,0 +1,405 @@ +From git@z Thu Jan 1 00:00:00 1970 +Subject: [PATCH v2 1/2] qga-win: implement a 'guest-get-load' command +From: Konstantin Kostiuk +Date: Mon, 24 Mar 2025 15:17:28 +0200 +Message-Id: <20250324131729.70992-2-kkostiuk@redhat.com> +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Windows has no native equivalent API, but it would be possible to +simulate it as illustrated here (BSD-3-Clause): + + https://github.com/giampaolo/psutil/pull/1485 + +Reviewed-by: Daniel P. Berrangé +Tested-by: Dehan Meng +Reviewed-by: Yan Vugenfirer +Signed-off-by: Konstantin Kostiuk +--- + qga/commands-win32.c | 148 +++++++++++++++++++++++++++++++++++++++++ + qga/guest-agent-core.h | 10 +++ + qga/main.c | 39 +++++++++++ + qga/meson.build | 2 +- + qga/qapi-schema.json | 9 ++- + 5 files changed, 205 insertions(+), 3 deletions(-) + +diff --git a/qga/commands-win32.c b/qga/commands-win32.c +index 749fdf8895..ba72bface7 100644 +--- a/qga/commands-win32.c ++++ b/qga/commands-win32.c +@@ -27,6 +27,7 @@ + #include + #include + #include ++#include + + #include "guest-agent-core.h" + #include "vss-win32.h" +@@ -119,6 +120,28 @@ static OpenFlags guest_file_open_modes[] = { + {"a+b", FILE_GENERIC_APPEND | GENERIC_READ, OPEN_ALWAYS } + }; + ++/* ++ * We use an exponentially weighted moving average, just like Unix systems do ++ * https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation ++ * ++ * These constants serve as the damping factor and are calculated with ++ * 1 / exp(sampling interval in seconds / window size in seconds) ++ * ++ * This formula comes from linux's include/linux/sched/loadavg.h ++ * https://github.com/torvalds/linux/blob/345671ea0f9258f410eb057b9ced9cefbbe5dc78/include/linux/sched/loadavg.h#L20-L23 ++ */ ++#define LOADAVG_FACTOR_1F 0.9200444146293232478931553241 ++#define LOADAVG_FACTOR_5F 0.9834714538216174894737477501 ++#define LOADAVG_FACTOR_15F 0.9944598480048967508795473394 ++/* ++ * The time interval in seconds between taking load counts, same as Linux ++ */ ++#define LOADAVG_SAMPLING_INTERVAL 5 ++ ++double load_avg_1m; ++double load_avg_5m; ++double load_avg_15m; ++ + #define debug_error(msg) do { \ + char *suffix = g_win32_error_message(GetLastError()); \ + g_debug("%s: %s", (msg), suffix); \ +@@ -2448,3 +2471,128 @@ char *qga_get_host_name(Error **errp) + + return g_utf16_to_utf8(tmp, size, NULL, NULL, NULL); + } ++ ++ ++static VOID CALLBACK load_avg_callback(PVOID hCounter, BOOLEAN timedOut) ++{ ++ PDH_FMT_COUNTERVALUE displayValue; ++ double currentLoad; ++ PDH_STATUS err; ++ ++ err = PdhGetFormattedCounterValue( ++ (PDH_HCOUNTER)hCounter, PDH_FMT_DOUBLE, 0, &displayValue); ++ /* Skip updating the load if we can't get the value successfully */ ++ if (err != ERROR_SUCCESS) { ++ slog("PdhGetFormattedCounterValue failed to get load value with 0x%lx", ++ err); ++ return; ++ } ++ currentLoad = displayValue.doubleValue; ++ ++ load_avg_1m = load_avg_1m * LOADAVG_FACTOR_1F + currentLoad * \ ++ (1.0 - LOADAVG_FACTOR_1F); ++ load_avg_5m = load_avg_5m * LOADAVG_FACTOR_5F + currentLoad * \ ++ (1.0 - LOADAVG_FACTOR_5F); ++ load_avg_15m = load_avg_15m * LOADAVG_FACTOR_15F + currentLoad * \ ++ (1.0 - LOADAVG_FACTOR_15F); ++} ++ ++static BOOL init_load_avg_counter(Error **errp) ++{ ++ CONST WCHAR *szCounterPath = L"\\System\\Processor Queue Length"; ++ PDH_STATUS status; ++ BOOL ret; ++ HQUERY hQuery; ++ HCOUNTER hCounter; ++ HANDLE event; ++ HANDLE waitHandle; ++ ++ status = PdhOpenQueryW(NULL, 0, &hQuery); ++ if (status != ERROR_SUCCESS) { ++ /* ++ * If the function fails, the return value is a system error code or ++ * a PDH error code. error_setg_win32 cant translate PDH error code ++ * properly, so just report it as is. ++ */ ++ error_setg_win32(errp, (DWORD)status, ++ "PdhOpenQueryW failed with 0x%lx", status); ++ return FALSE; ++ } ++ ++ status = PdhAddEnglishCounterW(hQuery, szCounterPath, 0, &hCounter); ++ if (status != ERROR_SUCCESS) { ++ error_setg_win32(errp, (DWORD)status, ++ "PdhAddEnglishCounterW failed with 0x%lx. Performance counters may be disabled.", ++ status); ++ PdhCloseQuery(hQuery); ++ return FALSE; ++ } ++ ++ event = CreateEventW(NULL, FALSE, FALSE, L"LoadUpdateEvent"); ++ if (event == NULL) { ++ error_setg_win32(errp, GetLastError(), "Create LoadUpdateEvent failed"); ++ PdhCloseQuery(hQuery); ++ return FALSE; ++ } ++ ++ status = PdhCollectQueryDataEx(hQuery, LOADAVG_SAMPLING_INTERVAL, event); ++ if (status != ERROR_SUCCESS) { ++ error_setg_win32(errp, (DWORD)status, ++ "PdhCollectQueryDataEx failed with 0x%lx", status); ++ CloseHandle(event); ++ PdhCloseQuery(hQuery); ++ return FALSE; ++ } ++ ++ ret = RegisterWaitForSingleObject( ++ &waitHandle, ++ event, ++ (WAITORTIMERCALLBACK)load_avg_callback, ++ (PVOID)hCounter, ++ INFINITE, ++ WT_EXECUTEDEFAULT); ++ ++ if (ret == 0) { ++ error_setg_win32(errp, GetLastError(), ++ "RegisterWaitForSingleObject failed"); ++ CloseHandle(event); ++ PdhCloseQuery(hQuery); ++ return FALSE; ++ } ++ ++ ga_set_load_avg_wait_handle(ga_state, waitHandle); ++ ga_set_load_avg_event(ga_state, event); ++ ga_set_load_avg_pdh_query(ga_state, hQuery); ++ ++ return TRUE; ++} ++ ++GuestLoadAverage *qmp_guest_get_load(Error **errp) ++{ ++ /* ++ * The load average logic calls PerformaceCounterAPI, which can result ++ * in a performance penalty. This avoids running the load average logic ++ * until a management application actually requests it. The load average ++ * will not initially be very accurate, but assuming that any interested ++ * management application will request it repeatedly throughout the lifetime ++ * of the VM, this seems like a good mitigation. ++ */ ++ if (ga_get_load_avg_pdh_query(ga_state) == NULL) { ++ /* set initial values */ ++ load_avg_1m = 0; ++ load_avg_5m = 0; ++ load_avg_15m = 0; ++ ++ if (init_load_avg_counter(errp) == false) { ++ return NULL; ++ } ++ } ++ ++ GuestLoadAverage *ret = NULL; ++ ++ ret = g_new0(GuestLoadAverage, 1); ++ ret->load1m = load_avg_1m; ++ ret->load5m = load_avg_5m; ++ ret->load15m = load_avg_15m; ++ return ret; ++} +diff --git a/qga/guest-agent-core.h b/qga/guest-agent-core.h +index a536d07d0d..d9f3922adf 100644 +--- a/qga/guest-agent-core.h ++++ b/qga/guest-agent-core.h +@@ -13,6 +13,10 @@ + #ifndef GUEST_AGENT_CORE_H + #define GUEST_AGENT_CORE_H + ++#ifdef _WIN32 ++#include ++#endif ++ + #include "qapi/qmp/dispatch.h" + #include "qga-qapi-types.h" + +@@ -41,6 +45,12 @@ void ga_set_response_delimited(GAState *s); + bool ga_is_frozen(GAState *s); + void ga_set_frozen(GAState *s); + void ga_unset_frozen(GAState *s); ++#ifdef _WIN32 ++void ga_set_load_avg_event(GAState *s, HANDLE event); ++void ga_set_load_avg_wait_handle(GAState *s, HANDLE wait_handle); ++void ga_set_load_avg_pdh_query(GAState *s, HQUERY query); ++HQUERY ga_get_load_avg_pdh_query(GAState *s); ++#endif + const char *ga_fsfreeze_hook(GAState *s); + int64_t ga_get_fd_handle(GAState *s, Error **errp); + int ga_parse_whence(GuestFileWhence *whence, Error **errp); +diff --git a/qga/main.c b/qga/main.c +index 72c39b042f..6c02f3ec38 100644 +--- a/qga/main.c ++++ b/qga/main.c +@@ -33,6 +33,7 @@ + #include "qemu-version.h" + #ifdef _WIN32 + #include ++#include + #include "qga/service-win32.h" + #include "qga/vss-win32.h" + #endif +@@ -105,6 +106,9 @@ struct GAState { + GAService service; + HANDLE wakeup_event; + HANDLE event_log; ++ HANDLE load_avg_wait_handle; ++ HANDLE load_avg_event; ++ HQUERY load_avg_pdh_query; + #endif + bool delimit_response; + bool frozen; +@@ -582,6 +586,25 @@ const char *ga_fsfreeze_hook(GAState *s) + } + #endif + ++#ifdef _WIN32 ++void ga_set_load_avg_wait_handle(GAState *s, HANDLE wait_handle) ++{ ++ s->load_avg_wait_handle = wait_handle; ++} ++void ga_set_load_avg_event(GAState *s, HANDLE event) ++{ ++ s->load_avg_event = event; ++} ++void ga_set_load_avg_pdh_query(GAState *s, HQUERY query) ++{ ++ s->load_avg_pdh_query = query; ++} ++HQUERY ga_get_load_avg_pdh_query(GAState *s) ++{ ++ return s->load_avg_pdh_query; ++} ++#endif ++ + static void become_daemon(const char *pidfile) + { + #ifndef _WIN32 +@@ -1402,6 +1425,10 @@ static GAState *initialize_agent(GAConfig *config, int socket_activation) + g_debug("Guest agent version %s started", QEMU_FULL_VERSION); + + #ifdef _WIN32 ++ s->load_avg_wait_handle = INVALID_HANDLE_VALUE; ++ s->load_avg_event = INVALID_HANDLE_VALUE; ++ s->load_avg_pdh_query = NULL; ++ + s->event_log = RegisterEventSource(NULL, "qemu-ga"); + if (!s->event_log) { + g_autofree gchar *errmsg = g_win32_error_message(GetLastError()); +@@ -1506,6 +1533,18 @@ static void cleanup_agent(GAState *s) + #ifdef _WIN32 + CloseHandle(s->wakeup_event); + CloseHandle(s->event_log); ++ ++ if (s->load_avg_wait_handle != INVALID_HANDLE_VALUE) { ++ UnregisterWait(s->load_avg_wait_handle); ++ } ++ ++ if (s->load_avg_event != INVALID_HANDLE_VALUE) { ++ CloseHandle(s->load_avg_event); ++ } ++ ++ if (s->load_avg_pdh_query) { ++ PdhCloseQuery(s->load_avg_pdh_query); ++ } + #endif + if (s->command_state) { + ga_command_state_cleanup_all(s->command_state); +diff --git a/qga/meson.build b/qga/meson.build +index 587ec4e5e8..89a4a8f713 100644 +--- a/qga/meson.build ++++ b/qga/meson.build +@@ -95,7 +95,7 @@ gen_tlb = [] + qga_libs = [] + if host_os == 'windows' + qga_libs += ['-lws2_32', '-lwinmm', '-lpowrprof', '-lwtsapi32', '-lwininet', '-liphlpapi', '-lnetapi32', +- '-lsetupapi', '-lcfgmgr32', '-luserenv'] ++ '-lsetupapi', '-lcfgmgr32', '-luserenv', '-lpdh' ] + if have_qga_vss + qga_libs += ['-lole32', '-loleaut32', '-lshlwapi', '-lstdc++', '-Wl,--enable-stdcall-fixup'] + subdir('vss-win32') +diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json +index 995594aaf4..e96a4b7957 100644 +--- a/qga/qapi-schema.json ++++ b/qga/qapi-schema.json +@@ -1863,7 +1863,7 @@ + 'load5m': 'number', + 'load15m': 'number' + }, +- 'if': 'CONFIG_GETLOADAVG' ++ 'if': { 'any': ['CONFIG_WIN32', 'CONFIG_GETLOADAVG'] } + } + + ## +@@ -1871,13 +1871,18 @@ + # + # Retrieve CPU process load information + # ++# .. note:: Windows does not have load average API, so QGA emulates it by ++# calculating the average CPU usage in the last 1, 5, 15 minutes ++# similar as Linux does this. ++# Calculation starts from the first time this command is called. ++# + # Returns: load information + # + # Since: 10.0 + ## + { 'command': 'guest-get-load', + 'returns': 'GuestLoadAverage', +- 'if': 'CONFIG_GETLOADAVG' ++ 'if': { 'any': ['CONFIG_WIN32', 'CONFIG_GETLOADAVG'] } + } + + ## +-- +2.48.1 + +From git@z Thu Jan 1 00:00:00 1970 +Subject: [PATCH v2 2/2] qga: Add tests for guest-get-load command +From: Konstantin Kostiuk +Date: Mon, 24 Mar 2025 15:17:29 +0200 +Message-Id: <20250324131729.70992-3-kkostiuk@redhat.com> +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 7bit + +Tested-by: Dehan Meng +Reviewed-by: Yan Vugenfirer +Signed-off-by: Konstantin Kostiuk +--- + tests/unit/test-qga.c | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/tests/unit/test-qga.c b/tests/unit/test-qga.c +index 541b08a5e7..587e30c7e4 100644 +--- a/tests/unit/test-qga.c ++++ b/tests/unit/test-qga.c +@@ -332,6 +332,22 @@ static void test_qga_get_fsinfo(gconstpointer fix) + } + } + ++static void test_qga_get_load(gconstpointer fix) ++{ ++ const TestFixture *fixture = fix; ++ g_autoptr(QDict) ret = NULL; ++ QDict *load; ++ ++ ret = qmp_fd(fixture->fd, "{'execute': 'guest-get-load'}"); ++ g_assert_nonnull(ret); ++ qmp_assert_no_error(ret); ++ ++ load = qdict_get_qdict(ret, "return"); ++ g_assert(qdict_haskey(load, "load1m")); ++ g_assert(qdict_haskey(load, "load5m")); ++ g_assert(qdict_haskey(load, "load15m")); ++} ++ + static void test_qga_get_memory_block_info(gconstpointer fix) + { + const TestFixture *fixture = fix; +@@ -1105,6 +1121,7 @@ int main(int argc, char **argv) + g_test_add_data_func("/qga/get-vcpus", &fix, test_qga_get_vcpus); + } + g_test_add_data_func("/qga/get-fsinfo", &fix, test_qga_get_fsinfo); ++ g_test_add_data_func("/qga/get-load", &fix, test_qga_get_load); + g_test_add_data_func("/qga/get-memory-block-info", &fix, + test_qga_get_memory_block_info); + g_test_add_data_func("/qga/get-memory-blocks", &fix, +-- +2.48.1 +