From: Marc-André Lureau <marcandre.lureau(a)redhat.com>
In QEMU 5.2, the guest agent learned to manipulate a user
~/.ssh/authorized_keys. Bind the JSON API to libvirt.
https://wiki.qemu.org/ChangeLog/5.2#Guest_agent
Signed-off-by: Marc-André Lureau <marcandre.lureau(a)redhat.com>
Signed-off-by: Michal Privoznik <mprivozn(a)redhat.com>
---
src/qemu/qemu_agent.c | 140 ++++++++++++++++++++++++++++++++++++++++++
src/qemu/qemu_agent.h | 15 +++++
tests/qemuagenttest.c | 79 ++++++++++++++++++++++++
3 files changed, 234 insertions(+)
diff --git a/src/qemu/qemu_agent.c b/src/qemu/qemu_agent.c
index 7fbb4a9431..9fa79338bc 100644
--- a/src/qemu/qemu_agent.c
+++ b/src/qemu/qemu_agent.c
@@ -2496,3 +2496,143 @@ qemuAgentSetResponseTimeout(qemuAgentPtr agent,
{
agent->timeout = timeout;
}
+
+/**
+ * qemuAgentSSHGetAuthorizedKeys:
+ * @agent: agent object
+ * @user: user to get authorized keys for
+ * @keys: Array of authorized keys
+ *
+ * Fetch the public keys from @user's $HOME/.ssh/authorized_keys.
+ *
+ * Returns: number of keys returned on success,
+ * -1 otherwise (error is reported)
+ */
+int
+qemuAgentSSHGetAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ char ***keys)
+{
+ g_autoptr(virJSONValue) cmd = NULL;
+ g_autoptr(virJSONValue) reply = NULL;
+ virJSONValuePtr data = NULL;
+ size_t ndata;
+ size_t i;
+ char **keys_ret = NULL;
+
+ if (!(cmd = qemuAgentMakeCommand("guest-ssh-get-authorized-keys",
+ "s:username", user,
+ NULL)))
+ return -1;
+
+ if (qemuAgentCommand(agent, cmd, &reply, agent->timeout) < 0)
+ return -1;
+
+ if (!(data = virJSONValueObjectGetObject(reply, "return")) ||
+ !(data = virJSONValueObjectGetArray(data, "keys"))) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("qemu agent didn't return an array of keys"));
+ return -1;
+ }
+
+ ndata = virJSONValueArraySize(data);
+
+ keys_ret = g_new0(char *, ndata);
+
+ for (i = 0; i < ndata; i++) {
+ virJSONValuePtr entry = virJSONValueArrayGet(data, i);
+
+ if (!entry) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("array element missing in guest-ssh-get-authorized-keys
return "
+ "value"));
+ goto error;
+ }
+
+ keys_ret[i] = g_strdup(virJSONValueGetString(entry));
+ }
+
+ *keys = g_steal_pointer(&keys_ret);
+ return ndata;
+
+ error:
+ virStringListFreeCount(keys_ret, ndata);
+ return -1;
+}
+
+
+/**
+ * qemuAgentSSHAddAuthorizedKeys:
+ * @agent: agent object
+ * @user: user to add authorized keys for
+ * @keys: Array of authorized keys
+ * @nkeys: number of items in @keys array
+ * @reset: whether to truncate authorized keys file before writing
+ *
+ * Append SSH @keys into the @user's authorized keys file. If
+ * @reset is true then the file is truncated before write and
+ * thus contains only newly added @keys.
+ *
+ * Returns: 0 on success,
+ * -1 otherwise (error is reported)
+ */
+int qemuAgentSSHAddAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys,
+ bool reset)
+{
+ g_autoptr(virJSONValue) cmd = NULL;
+ g_autoptr(virJSONValue) reply = NULL;
+ g_autoptr(virJSONValue) jkeys = NULL;
+
+ jkeys = qemuAgentMakeStringsArray(keys, nkeys);
+ if (jkeys == NULL)
+ return -1;
+
+ if (!(cmd = qemuAgentMakeCommand("guest-ssh-add-authorized-keys",
+ "s:username", user,
+ "a:keys", &jkeys,
+ "b:reset", reset,
+ NULL)))
+ return -1;
+
+ return qemuAgentCommand(agent, cmd, &reply, agent->timeout);
+}
+
+
+/**
+ * qemuAgentSSHRemoveAuthorizedKeys:
+ * @agent: agent object
+ * @user: user to remove authorized keys for
+ * @keys: Array of authorized keys
+ * @nkeys: number of items in @keys array
+ *
+ * Remove SSH @keys from the @user's authorized keys file. It's
+ * not considered an error when trying to remove a non-existent
+ * key.
+ *
+ * Returns: 0 on success,
+ * -1 otherwise (error is reported)
+ */
+int qemuAgentSSHRemoveAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys)
+{
+ g_autoptr(virJSONValue) cmd = NULL;
+ g_autoptr(virJSONValue) reply = NULL;
+ g_autoptr(virJSONValue) jkeys = NULL;
+
+ jkeys = qemuAgentMakeStringsArray(keys, nkeys);
+ if (jkeys == NULL)
+ return -1;
+
+ if (!(cmd = qemuAgentMakeCommand("guest-ssh-remove-authorized-keys",
+ "s:username", user,
+ "a:keys", &jkeys,
+ NULL)))
+ return -1;
+
+ return qemuAgentCommand(agent, cmd, &reply, agent->timeout);
+}
diff --git a/src/qemu/qemu_agent.h b/src/qemu/qemu_agent.h
index 2eeb376a68..7cbab489ec 100644
--- a/src/qemu/qemu_agent.h
+++ b/src/qemu/qemu_agent.h
@@ -170,3 +170,18 @@ int qemuAgentGetTimezone(qemuAgentPtr mon,
void qemuAgentSetResponseTimeout(qemuAgentPtr mon,
int timeout);
+
+int qemuAgentSSHGetAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ char ***keys);
+
+int qemuAgentSSHAddAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys,
+ bool reset);
+
+int qemuAgentSSHRemoveAuthorizedKeys(qemuAgentPtr agent,
+ const char *user,
+ const char **keys,
+ size_t nkeys);
diff --git a/tests/qemuagenttest.c b/tests/qemuagenttest.c
index 607bd97b5c..47ff92733a 100644
--- a/tests/qemuagenttest.c
+++ b/tests/qemuagenttest.c
@@ -35,6 +35,84 @@
virQEMUDriver driver;
+static int
+testQemuAgentSSHKeys(const void *data)
+{
+ virDomainXMLOptionPtr xmlopt = (virDomainXMLOptionPtr)data;
+ qemuMonitorTestPtr test = qemuMonitorTestNewAgent(xmlopt);
+ char **keys = NULL;
+ int nkeys = 0;
+ int ret = -1;
+
+ if (!test)
+ return -1;
+
+ if (qemuMonitorTestAddAgentSyncResponse(test) < 0)
+ goto cleanup;
+
+ if (qemuMonitorTestAddItem(test, "guest-ssh-get-authorized-keys",
+ "{\"return\": {"
+ " \"keys\": ["
+ " \"algo1 key1 comments1\","
+ " \"algo2 key2 comments2\""
+ " ]"
+ "}}") < 0)
+ goto cleanup;
+
+ if (qemuMonitorTestAddAgentSyncResponse(test) < 0)
+ goto cleanup;
+
+ if (qemuMonitorTestAddItem(test, "guest-ssh-add-authorized-keys",
+ "{ \"return\" : {} }") < 0)
+ goto cleanup;
+
+ if (qemuMonitorTestAddAgentSyncResponse(test) < 0)
+ goto cleanup;
+
+ if (qemuMonitorTestAddItem(test, "guest-ssh-remove-authorized-keys",
+ "{ \"return\" : {} }") < 0)
+ goto cleanup;
+
+ if ((nkeys = qemuAgentSSHGetAuthorizedKeys(qemuMonitorTestGetAgent(test),
+ "user",
+ &keys)) < 0)
+ goto cleanup;
+
+ if (nkeys != 2) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ "expected 2 keys, got %d", nkeys);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (STRNEQ(keys[1], "algo2 key2 comments2")) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "Unexpected key returned: %s",
keys[1]);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if ((ret = qemuAgentSSHAddAuthorizedKeys(qemuMonitorTestGetAgent(test),
+ "user",
+ (const char **) keys,
+ nkeys,
+ true)) < 0)
+ goto cleanup;
+
+ if ((ret = qemuAgentSSHRemoveAuthorizedKeys(qemuMonitorTestGetAgent(test),
+ "user",
+ (const char **) keys,
+ nkeys)) < 0)
+ goto cleanup;
+
+ ret = 0;
+
+ cleanup:
+ virStringListFreeCount(keys, nkeys);
+ qemuMonitorTestFree(test);
+ return ret;
+}
+
+
static int
testQemuAgentFSFreeze(const void *data)
{
@@ -1315,6 +1393,7 @@ mymain(void)
DO_TEST(Users);
DO_TEST(OSInfo);
DO_TEST(Timezone);
+ DO_TEST(SSHKeys);
DO_TEST(Timeout); /* Timeout should always be called last */
--
2.26.2