merge: implement nmcli con import/export for VPN connections (rh #1034105)

https://bugzilla.redhat.com/show_bug.cgi?id=1034105
This commit is contained in:
Jiří Klimeš 2015-12-07 09:17:48 +01:00
commit 4e9aa035c2
6 changed files with 435 additions and 22 deletions

View file

@ -40,6 +40,8 @@ nmcli_SOURCES = \
\
$(srcdir)/../common/nm-secret-agent-simple.c \
$(srcdir)/../common/nm-secret-agent-simple.h \
$(srcdir)/../common/nm-vpn-helpers.c \
$(srcdir)/../common/nm-vpn-helpers.h \
$(NULL)
nmcli_LDADD = \

View file

@ -36,6 +36,7 @@
#include "connections.h"
#include "nm-secret-agent-simple.h"
#include "polkit-agent.h"
#include "nm-vpn-helpers.h"
/* define some prompts for connection editor */
#define EDITOR_PROMPT_SETTING _("Setting name? ")
@ -47,6 +48,7 @@
#define PROMPT_VPN_TYPE _("VPN type: ")
#define PROMPT_MASTER _("Master: ")
#define PROMPT_CONNECTION _("Connection (name, UUID, or path): ")
#define PROMPT_VPN_CONNECTION _("VPN connection (name, UUID, or path): ")
#define PROMPT_CONNECTIONS _("Connection(s) (name, UUID, or path): ")
#define PROMPT_ACTIVE_CONNECTIONS _("Connection(s) (name, UUID, path or apath): ")
@ -274,7 +276,9 @@ usage (void)
" delete [id | uuid | path] <ID>\n\n"
" monitor [id | uuid | path] <ID> ...\n\n"
" reload\n\n"
" load <filename> [ <filename>... ]\n\n"));
" load <filename> [ <filename>... ]\n\n"
" import [--temporary] type <type> file <file to import>\n\n"
" export [id | uuid | path] <ID> [<output file>]\n\n"));
}
static void
@ -522,6 +526,30 @@ usage_connection_load (void)
"state.\n\n"));
}
static void
usage_connection_import (void)
{
g_printerr (_("Usage: nmcli connection import { ARGUMENTS | help }\n"
"\n"
"ARGUMENTS := [--temporary] type <type> file <file to import>\n"
"\n"
"Import an external/foreign configuration as a NetworkManager connection profile.\n"
"The type of the input file is specified by type option.\n"
"Only VPN configurations are supported at the moment. The configuration\n"
"is imported by NetworkManager VPN plugins.\n\n"));
}
static void
usage_connection_export (void)
{
g_printerr (_("Usage: nmcli connection export { ARGUMENTS | help }\n"
"\n"
"ARGUMENTS := [id | uuid | path] <ID> [<output file>]\n"
"\n"
"Export a connection. Only VPN connections are supported at the moment.\n"
"The data are directed to standard output or to a file if a name is given.\n\n"));
}
static gboolean
usage_connection_second_level (const char *cmd)
{
@ -549,6 +577,10 @@ usage_connection_second_level (const char *cmd)
usage_connection_reload ();
else if (matches (cmd, "load") == 0)
usage_connection_load ();
else if (matches (cmd, "import") == 0)
usage_connection_import ();
else if (matches (cmd, "export") == 0)
usage_connection_export ();
else
ret = FALSE;
return ret;
@ -6868,33 +6900,59 @@ gen_compat_devices (const char *text, int state)
return ret;
}
static const char **
_create_vpn_array (const GPtrArray *connections, gboolean uuid)
{
int c, idx = 0;
const char **array;
if (connections->len < 1)
return NULL;
array = g_new (const char *, connections->len + 1);
for (c = 0; c < connections->len; c++) {
NMConnection *connection = NM_CONNECTION (connections->pdata[c]);
const char *type = nm_connection_get_connection_type (connection);
if (g_strcmp0 (type, NM_SETTING_VPN_SETTING_NAME) == 0)
array[idx++] = uuid ? nm_connection_get_uuid (connection) : nm_connection_get_id (connection);
}
array[idx] = NULL;
return array;
}
static char *
gen_vpn_uuids (const char *text, int state)
{
const GPtrArray *connections = nmc_tab_completion.nmc->connections;
int c, u = 0;
const GPtrArray *connections = nm_cli.connections;
const char **uuids;
char *ret;
if (connections->len < 1)
return NULL;
uuids = g_new (const char *, connections->len + 1);
for (c = 0; c < connections->len; c++) {
NMConnection *connection = NM_CONNECTION (connections->pdata[c]);
const char *type = nm_connection_get_connection_type (connection);
if (g_strcmp0 (type, NM_SETTING_VPN_SETTING_NAME) == 0)
uuids[u++] = nm_connection_get_uuid (connection);
}
uuids[u] = NULL;
uuids = _create_vpn_array (connections, TRUE);
ret = nmc_rl_gen_func_basic (text, state, uuids);
g_free (uuids);
return ret;
}
static char *
gen_vpn_ids (const char *text, int state)
{
const GPtrArray *connections = nm_cli.connections;
const char **ids;
char *ret;
if (connections->len < 1)
return NULL;
ids = _create_vpn_array (connections, FALSE);
ret = nmc_rl_gen_func_basic (text, state, ids);
g_free (ids);
return ret;
}
static rl_compentry_func_t *
get_gen_func_cmd_nmcli (const char *str)
{
@ -9967,6 +10025,235 @@ do_connection_load (NmCli *nmc, int argc, char **argv)
return nmc->return_value;
}
// FIXME: change the text when non-VPN connection types are supported
#define PROMPT_IMPORT_TYPE PROMPT_VPN_TYPE
#define PROMPT_IMPORT_FILE _("File to import: ")
static NMCResultCode
do_connection_import (NmCli *nmc, gboolean temporary, int argc, char **argv)
{
GError *error = NULL;
const char *type = NULL, *filename = NULL;
char *type_ask = NULL, *filename_ask = NULL;
AddConnectionInfo *info;
NMConnection *connection = NULL;
NMVpnEditorPlugin *plugin;
if (argc == 0) {
if (nmc->ask) {
type_ask = nmc_readline (PROMPT_IMPORT_TYPE);
filename_ask = nmc_readline (PROMPT_IMPORT_FILE);
type = type_ask = type_ask ? g_strstrip (type_ask) : NULL;
filename = filename_ask = filename_ask ? g_strstrip (filename_ask) : NULL;
} else {
g_string_printf (nmc->return_text, _("Error: No arguments provided."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
}
while (argc > 0) {
if (strcmp (*argv, "type") == 0) {
if (next_arg (&argc, &argv) != 0) {
g_string_printf (nmc->return_text, _("Error: %s argument is missing."), *(argv-1));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
if (!type)
type = *argv;
else
g_printerr (_("Warning: 'type' already specified, ignoring extra one.\n"));
} else if (strcmp (*argv, "file") == 0) {
if (next_arg (&argc, &argv) != 0) {
g_string_printf (nmc->return_text, _("Error: %s argument is missing."), *(argv-1));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
if (!filename)
filename = *argv;
else
g_printerr (_("Warning: 'file' already specified, ignoring extra one.\n"));
} else {
g_string_printf (nmc->return_text, _("Unknown parameter: %s\n"), *argv);
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
argc--;
argv++;
}
if (!type) {
g_string_printf (nmc->return_text, _("Error: 'type' argument is required."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
if (!filename) {
g_string_printf (nmc->return_text, _("Error: 'file' argument is required."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
/* Import VPN configuration */
plugin = nm_vpn_get_plugin_by_service (type, &error);
if (!plugin) {
g_string_printf (nmc->return_text, _("Error: failed to load VPN plugin: %s."),
error->message);
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
connection = nm_vpn_editor_plugin_import (plugin, filename, &error);
if (!connection) {
g_string_printf (nmc->return_text, _("Error: failed to import '%s': %s."),
filename, error->message);
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
info = g_malloc0 (sizeof (AddConnectionInfo));
info->nmc = nmc;
info->con_name = g_strdup (nm_connection_get_id (connection));
/* Add the new imported connection to NetworkManager */
add_new_connection (!temporary,
nmc->client,
connection,
add_connection_cb,
info);
nmc->should_wait = TRUE;
finish:
if (connection)
g_object_unref (connection);
g_clear_error (&error);
g_free (type_ask);
g_free (filename_ask);
return nmc->return_value;
}
static NMCResultCode
do_connection_export (NmCli *nmc, int argc, char **argv)
{
NMConnection *connection = NULL;
const char *name;
const char *out_name = NULL;
char *name_ask = NULL;
char *out_name_ask = NULL;
const char *path = NULL;
const char *selector = NULL;
const char *type = NULL;
NMVpnEditorPlugin *plugin;
GError *error = NULL;
if (argc == 0) {
if (nmc->ask) {
name_ask = nmc_readline (PROMPT_VPN_CONNECTION);
name = name_ask = name_ask ? g_strstrip (name_ask) : NULL;
out_name = out_name_ask = nmc_readline (_("Output file name: "));
} else {
g_string_printf (nmc->return_text, _("Error: No arguments provided."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
} else {
if ( strcmp (*argv, "id") == 0
|| strcmp (*argv, "uuid") == 0
|| strcmp (*argv, "path") == 0) {
selector = *argv;
if (next_arg (&argc, &argv) != 0) {
g_string_printf (nmc->return_text, _("Error: %s argument is missing."),
selector);
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
}
name = *argv;
if (next_arg (&argc, &argv) == 0)
out_name = *argv;
if (next_arg (&argc, &argv) == 0) {
g_string_printf (nmc->return_text, _("Error: unknown extra argument: '%s'."), *argv);
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
}
if (!name) {
g_string_printf (nmc->return_text, _("Error: connection ID is missing."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
connection = nmc_find_connection (nmc->connections, selector, name, NULL);
if (!connection) {
g_string_printf (nmc->return_text, _("Error: Unknown connection '%s'."), name);
nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
goto finish;
}
type = nm_connection_get_connection_type (connection);
if (g_strcmp0 (type, NM_SETTING_VPN_SETTING_NAME) != 0) {
g_string_printf (nmc->return_text, _("Error: the connection is not VPN."));
nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
goto finish;
}
type = nm_setting_vpn_get_service_type (nm_connection_get_setting_vpn (connection));
/* Export VPN configuration */
plugin = nm_vpn_get_plugin_by_service (type, &error);
if (!plugin) {
g_string_printf (nmc->return_text, _("Error: failed to load VPN plugin."));
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
if (out_name)
path = out_name;
else {
int fd;
char tmpfile[] = "/tmp/nmcli-export-temp-XXXXXX";
fd = g_mkstemp (tmpfile);
if (fd == -1) {
g_string_printf (nmc->return_text, _("Error: failed to create temporary file %s."), tmpfile);
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
close (fd);
path = tmpfile;
}
if (!nm_vpn_editor_plugin_export (plugin, path, connection, &error)) {
g_string_printf (nmc->return_text, _("Error: failed to export '%s': %s."),
nm_connection_get_id (connection), error ? error->message : "(unknown)");
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
/* No output file -> copy data to stdout */
if (!out_name) {
char *contents = NULL;
gsize len = 0;
if (!g_file_get_contents (path, &contents, &len, &error)) {
g_string_printf (nmc->return_text, _("Error: failed to read temporary file '%s': %s."),
path, error->message);
nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
goto finish;
}
g_print ("%s", contents);
g_free (contents);
}
finish:
if (!out_name && path)
unlink (path);
g_clear_error (&error);
g_free (name_ask);
g_free (out_name_ask);
return nmc->return_value;
}
typedef struct {
NmCli *nmc;
@ -10067,6 +10354,13 @@ nmcli_con_tab_completion (const char *text, int start, int end)
generator_func = gen_func_connection_names;
} else if (g_strcmp0 (rl_prompt, PROMPT_ACTIVE_CONNECTIONS) == 0) {
generator_func = gen_func_active_connection_names;
} else if (g_strcmp0 (rl_prompt, PROMPT_IMPORT_TYPE) == 0) {
generator_func = gen_func_vpn_types;
} else if (g_strcmp0 (rl_prompt, PROMPT_IMPORT_FILE) == 0) {
rl_attempted_completion_over = 0;
rl_complete_with_tilde_expansion = 1;
} else if (g_strcmp0 (rl_prompt, PROMPT_VPN_CONNECTION) == 0) {
generator_func = gen_vpn_ids;
}
if (generator_func)
@ -10250,6 +10544,17 @@ do_connections (NmCli *nmc, int argc, char **argv)
next_arg (&argc, &argv);
}
nmc->return_value = do_connection_clone (nmc, temporary, argc, argv);
} else if (matches(*argv, "import") == 0) {
gboolean temporary = FALSE;
next_arg (&argc, &argv);
if (nmc_arg_is_option (*argv, "temporary")) {
temporary = TRUE;
next_arg (&argc, &argv);
}
nmc->return_value = do_connection_import (nmc, temporary, argc, argv);
} else if (matches(*argv, "export") == 0) {
nmc->return_value = do_connection_export (nmc, argc-1, argv+1);
} else {
usage ();
g_string_printf (nmc->return_text, _("Error: '%s' is not valid 'connection' command."), *argv);

View file

@ -282,6 +282,7 @@ _nmcli_compl_OPTIONS()
# expects several options with parameters. This function can parse them and remove them from the words array.
_nmcli_compl_ARGS()
{
local aliases=${@}
local OPTIONS_ALL N_REMOVE_WORDS REMOVE_OPTIONS OPTIONS_HAS_MANDATORY i
OPTIONS_ALL=("${OPTIONS[@]}")
OPTIONS_UNKNOWN_OPTION=
@ -317,7 +318,17 @@ _nmcli_compl_ARGS()
N_REMOVE_WORDS=2
REMOVE_OPTIONS=("${words[0]}")
case "${words[0]}" in
# change option name to alias
WORD0="${words[0]}"
for alias in "${aliases[@]}" ; do
if [[ "${WORD0}" == ${alias%%:*} ]]; then
WORD0=${alias#*:}
break
fi
done
case "${WORD0}" in
level)
if [[ "${#words[@]}" -eq 2 ]]; then
_nmcli_list "OFF ERR WARN INFO DEBUG TRACE"
@ -560,7 +571,8 @@ _nmcli_compl_ARGS()
username| \
service| \
password| \
passwd-file)
passwd-file| \
file)
if [[ "${#words[@]}" -eq 2 ]]; then
return 0
fi
@ -870,7 +882,7 @@ _nmcli()
;;
c|co|con|conn|conne|connec|connect|connecti|connectio|connection)
if [[ ${#words[@]} -eq 2 ]]; then
_nmcli_compl_COMMAND "$command" show up down add modify clone edit delete monitor reload load
_nmcli_compl_COMMAND "$command" show up down add modify clone edit delete monitor reload load import export
elif [[ ${#words[@]} -gt 2 ]]; then
case "$command" in
s|sh|sho|show)
@ -1315,6 +1327,61 @@ _nmcli()
COMPREPLY=()
fi
;;
i|im|imp|impo|impor|import)
if [[ ${#words[@]} -eq 3 ]]; then
_nmcli_compl_COMMAND "${words[2]}" type file --temporary
elif [[ ${#words[@]} -gt 3 ]]; then
_nmcli_array_delete_at words 0 1
LONG_OPTIONS=(help temporary)
HELP_ONLY_AS_FIRST=1
_nmcli_compl_OPTIONS
case $? in
0)
return 0
;;
1)
if [[ "$HELP_ONLY_AS_FIRST" == 1 ]]; then
_nmcli_compl_COMMAND "${words[2]}" type file
fi
return 0
;;
esac
OPTIONS=(type file)
OPTIONS_MANDATORY=(type file)
ALIASES=("type:vpn-type")
_nmcli_compl_ARGS ${ALIASES[@]}
return 0
fi
;;
e|ex|exp|expo|expor|export)
if [[ ${#words[@]} -eq 3 ]]; then
_nmcli_compl_COMMAND_nl "${words[2]}" "$(printf "id\nuuid\npath\n%s" "$(_nmcli_con_show NAME)")"
elif [[ ${#words[@]} -gt 3 ]]; then
_nmcli_array_delete_at words 0 1
LONG_OPTIONS=(help)
HELP_ONLY_AS_FIRST=1
_nmcli_compl_OPTIONS
case $? in
0)
return 0
;;
1)
if [[ "$HELP_ONLY_AS_FIRST" == 1 ]]; then
_nmcli_compl_COMMAND_nl "${words[2]}" "$(printf "id\nuuid\npath\n%s" "$(_nmcli_con_show NAME)")" "${LONG_OPTIONS[@]}"
fi
return 0
;;
esac
OPTIONS=(id uuid path)
_nmcli_compl_ARGS_CONNECTION && return 0
return 0
fi
;;
esac
fi
;;

View file

@ -37,22 +37,30 @@ static gboolean plugins_loaded;
static GSList *plugins = NULL;
NMVpnEditorPlugin *
nm_vpn_get_plugin_by_service (const char *service)
nm_vpn_get_plugin_by_service (const char *service, GError **error)
{
NMVpnEditorPlugin *plugin = NULL;
NMVpnPluginInfo *plugin_info;
char *type = NULL;
g_return_val_if_fail (service != NULL, NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
if (G_UNLIKELY (!plugins_loaded))
nm_vpn_get_plugins ();
if (!g_str_has_prefix (service, NM_DBUS_INTERFACE))
service = type = g_strdup_printf ("%s.%s", NM_DBUS_INTERFACE, service);
plugin_info = nm_vpn_plugin_info_list_find_by_service (plugins, service);
if (plugin_info) {
plugin = nm_vpn_plugin_info_get_editor_plugin (plugin_info);
if (!plugin)
plugin = nm_vpn_plugin_info_load_editor_plugin (plugin_info, NULL);
}
plugin = nm_vpn_plugin_info_load_editor_plugin (plugin_info, error);
} else
g_set_error_literal (error, NM_VPN_PLUGIN_ERROR, NM_VPN_PLUGIN_ERROR_FAILED,
_("could not get VPN plugin info"));
g_free (type);
return plugin;
}
@ -80,7 +88,7 @@ nm_vpn_supports_ipv6 (NMConnection *connection)
service_type = nm_setting_vpn_get_service_type (s_vpn);
g_return_val_if_fail (service_type != NULL, FALSE);
plugin = nm_vpn_get_plugin_by_service (service_type);
plugin = nm_vpn_get_plugin_by_service (service_type, NULL);
g_return_val_if_fail (plugin != NULL, FALSE);
capabilities = nm_vpn_editor_plugin_get_capabilities (plugin);

View file

@ -25,7 +25,7 @@
GSList *nm_vpn_get_plugins (void);
NMVpnEditorPlugin *nm_vpn_get_plugin_by_service (const char *service);
NMVpnEditorPlugin *nm_vpn_get_plugin_by_service (const char *service, GError **error);
gboolean nm_vpn_supports_ipv6 (NMConnection *connection);

View file

@ -816,6 +816,29 @@ then \fINetworkManager\fP will reload connection files any time they change
Load/reload one or more connection files from disk. Use this after manually
editing a connection file to ensure that \fBNetworkManager\fP is aware
of its latest state.
.TP
.B import [--temporary] type <type> file <file to import>
.br
Import an external/foreign configuration as a NetworkManager connection profile.
The type of the input file is specified by \fItype\fP option.
.br
Only VPN configurations are supported at the moment. The configuration
is imported by NetworkManager VPN plugins. \fItype\fP values are the same as for
\fIvpn-type\fP option in \fBnmcli connection add\fP. VPN configurations are
imported by VPN plugins. Therefore the proper VPN plugin has to be installed
so that nmcli could import the data.
.br
The imported connection profile will be saved as persistent unless \fI--temporary\fP
option is specified, in which case the new profile won't exist after NetworkManager
restart.
.TP
.B export [ id | uuid | path ] <ID> [<output file>]
.br
Export a connection.
.br
Only VPN connections are supported at the moment. A proper VPN plugin has to be
installed so that nmcli could export a connection. If no \fI<output file>\fP is
provided, the VPN configuration data will be printed to standard output.
.RE
.TP
@ -1187,6 +1210,14 @@ appends a Google public DNS server to DNS servers in ABC profile.
.IP
removes the specified IP address from (static) profile ABC.
.IP "\fB\f(CWnmcli con import type openvpn file ~/Downloads/frootvpn.ovpn\fP\fP"
.IP
imports an OpenVPN configuration to NetworkManager.
.IP "\fB\f(CWnmcli con export corp-vpnc /home/joe/corpvpn.conf\fP\fP"
.IP
exports NetworkManager VPN profile corp-vpnc as standard Cisco (vpnc) configuration.
.SH NOTES
\fInmcli\fP accepts abbreviations, as long as they are a unique prefix in the set
of possible options. As new options get added, these abbreviations are not guaranteed