At this point, the only NBD server type virt-p2v supports is nbdkit with
socket activation; therefore, outputting "localhost" from
start_nbd_server(), and tracking it via variables / parameters from there
on is needless generality. Remove the affected variables and parameters,
such as "ipaddr", "hostname", and "nbd_local_ipaddr".
Ref:
https://listman.redhat.com/archives/libguestfs/2022-March/028475.html
Suggested-by: Richard W.M. Jones <rjones(a)redhat.com>
Signed-off-by: Laszlo Ersek <lersek(a)redhat.com>
Reviewed-by: Richard W.M. Jones <rjones(a)redhat.com>
---
Notes:
v2:
- pick up Rich's R-b
p2v.h | 6 +--
conversion.c | 13 ++---
nbd.c | 51 +++++++++-----------
ssh.c | 6 +--
4 files changed, 33 insertions(+), 43 deletions(-)
diff --git a/p2v.h b/p2v.h
index a14edc52ef74..4d2d20648467 100644
--- a/p2v.h
+++ b/p2v.h
@@ -105,7 +105,7 @@ extern int inhibit_power_saving (void);
/* ssh.c */
extern int test_connection (struct config *);
-extern mexp_h *open_data_connection (struct config *, const char *local_ipaddr, int
local_port, int *remote_port);
+extern mexp_h *open_data_connection (struct config *, int local_port, int *remote_port);
extern mexp_h *start_remote_connection (struct config *, const char *remote_dir);
extern const char *get_ssh_error (void);
extern int scp_file (struct config *config, const char *target, const char *local, ...)
__attribute__((sentinel));
@@ -113,8 +113,8 @@ extern int scp_file (struct config *config, const char *target, const
char *loca
/* nbd.c */
extern void set_nbd_option (const char *opt);
extern void test_nbd_servers (void);
-extern pid_t start_nbd_server (const char **ipaddr, int *port, const char *device);
-extern int wait_for_nbd_server_to_start (const char *ipaddr, int port);
+extern pid_t start_nbd_server (int *port, const char *device);
+extern int wait_for_nbd_server_to_start (int port);
const char *get_nbd_error (void);
/* utils.c */
diff --git a/conversion.c b/conversion.c
index 8daa1d9f39fa..8bc4119fb47c 100644
--- a/conversion.c
+++ b/conversion.c
@@ -155,270 +155,267 @@ int
start_conversion (struct config *config,
void (*notify_ui) (int type, const char *data))
{
int ret = -1;
int status;
size_t i, len;
const size_t nr_disks = guestfs_int_count_strings (config->disks);
time_t now;
struct tm tm;
CLEANUP_FREE struct data_conn *data_conns = NULL;
CLEANUP_FREE char *remote_dir = NULL;
char tmpdir[] = "/tmp/p2v.XXXXXX";
char name_file[] = "/tmp/p2v.XXXXXX/name";
char physical_xml_file[] = "/tmp/p2v.XXXXXX/physical.xml";
char wrapper_script[] = "/tmp/p2v.XXXXXX/virt-v2v-wrapper.sh";
char dmesg_file[] = "/tmp/p2v.XXXXXX/dmesg";
char lscpu_file[] = "/tmp/p2v.XXXXXX/lscpu";
char lspci_file[] = "/tmp/p2v.XXXXXX/lspci";
char lsscsi_file[] = "/tmp/p2v.XXXXXX/lsscsi";
char lsusb_file[] = "/tmp/p2v.XXXXXX/lsusb";
char p2v_version_file[] = "/tmp/p2v.XXXXXX/p2v-version";
int inhibit_fd = -1;
#if DEBUG_STDERR
print_config (config, stderr);
fprintf (stderr, "\n");
#endif
set_control_h (NULL);
set_running (1);
set_cancel_requested (0);
inhibit_fd = inhibit_power_saving ();
#ifdef DEBUG_STDERR
if (inhibit_fd == -1)
fprintf (stderr, "warning: virt-p2v cannot inhibit power saving during
conversion.\n");
#endif
data_conns = malloc (sizeof (struct data_conn) * nr_disks);
if (data_conns == NULL)
error (EXIT_FAILURE, errno, "malloc");
for (i = 0; config->disks[i] != NULL; ++i) {
data_conns[i].h = NULL;
data_conns[i].nbd_pid = 0;
data_conns[i].nbd_remote_port = -1;
}
/* Start the data connections and NBD server processes, one per disk. */
for (i = 0; config->disks[i] != NULL; ++i) {
- const char *nbd_local_ipaddr;
int nbd_local_port;
CLEANUP_FREE char *device = NULL;
if (config->disks[i][0] == '/') {
device = strdup (config->disks[i]);
if (!device) {
perror ("strdup");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
}
else if (asprintf (&device, "/dev/%s", config->disks[i]) == -1) {
perror ("asprintf");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
if (notify_ui) {
CLEANUP_FREE char *msg;
if (asprintf (&msg,
_("Starting local NBD server for %s ..."),
config->disks[i]) == -1)
error (EXIT_FAILURE, errno, "asprintf");
notify_ui (NOTIFY_STATUS, msg);
}
/* Start NBD server listening on the given port number. */
- data_conns[i].nbd_pid =
- start_nbd_server (&nbd_local_ipaddr, &nbd_local_port, device);
+ data_conns[i].nbd_pid = start_nbd_server (&nbd_local_port, device);
if (data_conns[i].nbd_pid == 0) {
set_conversion_error ("NBD server error: %s", get_nbd_error ());
goto out;
}
/* Wait for NBD server to start up and listen. */
- if (wait_for_nbd_server_to_start (nbd_local_ipaddr, nbd_local_port) == -1) {
+ if (wait_for_nbd_server_to_start (nbd_local_port) == -1) {
set_conversion_error ("NBD server error: %s", get_nbd_error ());
goto out;
}
if (notify_ui) {
CLEANUP_FREE char *msg;
if (asprintf (&msg,
_("Opening data connection for %s ..."),
config->disks[i]) == -1)
error (EXIT_FAILURE, errno, "asprintf");
notify_ui (NOTIFY_STATUS, msg);
}
/* Open the SSH data connection, with reverse port forwarding
* back to the NBD server.
*/
- data_conns[i].h = open_data_connection (config,
- nbd_local_ipaddr, nbd_local_port,
+ data_conns[i].h = open_data_connection (config, nbd_local_port,
&data_conns[i].nbd_remote_port);
if (data_conns[i].h == NULL) {
const char *err = get_ssh_error ();
set_conversion_error ("could not open data connection over SSH to the
conversion server: %s", err);
goto out;
}
#if DEBUG_STDERR
fprintf (stderr,
- "%s: data connection for %s: SSH remote port %d, local port
%s:%d\n",
+ "%s: data connection for %s: SSH remote port %d, local port
%d\n",
g_get_prgname (), device,
data_conns[i].nbd_remote_port,
- nbd_local_ipaddr, nbd_local_port);
+ nbd_local_port);
#endif
}
/* Create a remote directory name which will be used for libvirt
* XML, log files and other stuff. We don't delete this directory
* after the run because (a) it's useful for debugging and (b) it
* only contains small files.
*
* NB: This path MUST NOT require shell quoting.
*/
time (&now);
gmtime_r (&now, &tm);
if (asprintf (&remote_dir,
"/tmp/virt-p2v-%04d%02d%02d-XXXXXXXX",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday) == -1) {
perror ("asprintf");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
len = strlen (remote_dir);
guestfs_int_random_string (&remote_dir[len-8], 8);
if (notify_ui)
notify_ui (NOTIFY_LOG_DIR, remote_dir);
/* Generate the local temporary directory. */
if (mkdtemp (tmpdir) == NULL) {
perror ("mkdtemp");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
memcpy (name_file, tmpdir, strlen (tmpdir));
memcpy (physical_xml_file, tmpdir, strlen (tmpdir));
memcpy (wrapper_script, tmpdir, strlen (tmpdir));
memcpy (dmesg_file, tmpdir, strlen (tmpdir));
memcpy (lscpu_file, tmpdir, strlen (tmpdir));
memcpy (lspci_file, tmpdir, strlen (tmpdir));
memcpy (lsscsi_file, tmpdir, strlen (tmpdir));
memcpy (lsusb_file, tmpdir, strlen (tmpdir));
memcpy (p2v_version_file, tmpdir, strlen (tmpdir));
/* Generate the static files. */
generate_name (config, name_file);
generate_physical_xml (config, data_conns, physical_xml_file);
generate_wrapper_script (config, remote_dir, wrapper_script);
generate_system_data (dmesg_file,
lscpu_file, lspci_file, lsscsi_file, lsusb_file);
generate_p2v_version_file (p2v_version_file);
/* Open the control connection. This also creates remote_dir. */
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Setting up the control connection ..."));
set_control_h (start_remote_connection (config, remote_dir));
if (control_h == NULL) {
set_conversion_error ("could not open control connection over SSH to the
conversion server: %s",
get_ssh_error ());
goto out;
}
/* Copy the static files to the remote dir. */
/* These three files must not fail, so check for errors here. */
if (scp_file (config, remote_dir,
name_file, physical_xml_file, wrapper_script, NULL) == -1) {
set_conversion_error ("scp: %s: %s",
remote_dir, get_ssh_error ());
goto out;
}
/* It's not essential that these files are copied, so ignore errors. */
ignore_value (scp_file (config, remote_dir,
dmesg_file, lscpu_file, lspci_file, lsscsi_file,
lsusb_file, p2v_version_file, NULL));
/* Do the conversion. This runs until virt-v2v exits. */
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Doing conversion ..."));
if (mexp_printf (control_h,
/* To simplify things in the wrapper script, it
* writes virt-v2v's exit status to
* /remote_dir/status, and here we read that and
* exit the ssh shell with the same status.
*/
"%s/virt-v2v-wrapper.sh; "
"exit $(< %s/status)\n",
remote_dir, remote_dir) == -1) {
set_conversion_error ("mexp_printf: virt-v2v: %m");
goto out;
}
/* Read output from the virt-v2v process and echo it through the
* notify function, until virt-v2v closes the connection.
*/
while (!is_cancel_requested ()) {
char buf[257];
ssize_t r;
r = read (mexp_get_fd (control_h), buf, sizeof buf - 1);
if (r == -1) {
/* See comment about this in miniexpect.c. */
if (errno == EIO)
break; /* EOF */
set_conversion_error ("read: %m");
goto out;
}
if (r == 0)
break; /* EOF */
buf[r] = '\0';
if (notify_ui)
notify_ui (NOTIFY_REMOTE_MESSAGE, buf);
}
if (is_cancel_requested ()) {
set_conversion_error ("cancelled by user");
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Conversion cancelled by user."));
goto out;
}
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Control connection closed by remote."));
ret = 0;
out:
if (control_h) {
mexp_h *h = control_h;
set_control_h (NULL);
status = mexp_close (h);
if (status == -1) {
set_conversion_error ("mexp_close: %m");
ret = -1;
}
else if (ret == 0 &&
WIFEXITED (status) &&
WEXITSTATUS (status) != 0) {
set_conversion_error ("virt-v2v exited with status %d",
WEXITSTATUS (status));
ret = -1;
}
}
cleanup_data_conns (data_conns, nr_disks);
if (inhibit_fd >= 0)
close (inhibit_fd);
set_running (0);
return ret;
}
diff --git a/nbd.c b/nbd.c
index cda962d03bdd..cb849e75dc84 100644
--- a/nbd.c
+++ b/nbd.c
@@ -81,9 +81,9 @@ static const enum nbd_server standard_servers[] =
static enum nbd_server use_server;
static pid_t start_nbdkit (const char *device, int *fds, size_t nr_fds);
-static int open_listening_socket (const char *ipaddr, int **fds, size_t *nr_fds);
-static int bind_tcpip_socket (const char *ipaddr, const char *port, int **fds, size_t
*nr_fds);
-static int connect_with_source_port (const char *hostname, int dest_port, int
source_port);
+static int open_listening_socket (int **fds, size_t *nr_fds);
+static int bind_tcpip_socket (const char *port, int **fds, size_t *nr_fds);
+static int connect_with_source_port (int dest_port, int source_port);
static int bind_source_port (int sockfd, int family, int source_port);
static char *nbd_error;
@@ -235,32 +235,31 @@ test_nbd_servers (void)
* Returns the process ID (E<gt> 0) or C<0> if there is an error.
*/
pid_t
-start_nbd_server (const char **ipaddr, int *port, const char *device)
+start_nbd_server (int *port, const char *device)
{
int *fds = NULL;
size_t i, nr_fds;
pid_t pid;
switch (use_server) {
case NBDKIT: /* nbdkit with socket activation */
- *ipaddr = "localhost";
- *port = open_listening_socket (*ipaddr, &fds, &nr_fds);
+ *port = open_listening_socket (&fds, &nr_fds);
if (*port == -1) return -1;
pid = start_nbdkit (device, fds, nr_fds);
for (i = 0; i < nr_fds; ++i)
close (fds[i]);
free (fds);
return pid;
}
abort ();
}
#define FIRST_SOCKET_ACTIVATION_FD 3
/**
* Set up file descriptors and environment variables for
* socket activation.
*
* Note this function runs in the child between fork and exec.
*/
@@ -350,261 +349,257 @@ start_nbdkit (const char *device, int *fds, size_t nr_fds)
* The caller must free the array.
*/
static int
-open_listening_socket (const char *ipaddr, int **fds, size_t *nr_fds)
+open_listening_socket (int **fds, size_t *nr_fds)
{
int port;
char port_str[16];
/* This just ensures we don't try the port we previously bound to. */
port = nbd_local_port;
/* Search for a free port. */
for (; port < 60000; ++port) {
snprintf (port_str, sizeof port_str, "%d", port);
- if (bind_tcpip_socket (ipaddr, port_str, fds, nr_fds) == 0) {
+ if (bind_tcpip_socket (port_str, fds, nr_fds) == 0) {
/* See above. */
nbd_local_port = port + 1;
return port;
}
}
set_nbd_error ("cannot find a free local port");
return -1;
}
static int
-bind_tcpip_socket (const char *ipaddr, const char *port,
- int **fds_rtn, size_t *nr_fds_rtn)
+bind_tcpip_socket (const char *port, int **fds_rtn, size_t *nr_fds_rtn)
{
struct addrinfo *ai = NULL;
struct addrinfo hints;
struct addrinfo *a;
int err;
int *fds = NULL;
size_t nr_fds;
int addr_in_use = 0;
memset (&hints, 0, sizeof hints);
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
hints.ai_socktype = SOCK_STREAM;
- err = getaddrinfo (ipaddr, port, &hints, &ai);
+ err = getaddrinfo ("localhost", port, &hints, &ai);
if (err != 0) {
#if DEBUG_STDERR
- fprintf (stderr, "%s: getaddrinfo: %s: %s: %s",
- g_get_prgname (), ipaddr ? ipaddr : "<any>", port,
- gai_strerror (err));
+ fprintf (stderr, "%s: getaddrinfo: localhost: %s: %s", g_get_prgname (),
+ port, gai_strerror (err));
#endif
return -1;
}
nr_fds = 0;
for (a = ai; a != NULL; a = a->ai_next) {
int sock, opt;
sock = socket (a->ai_family, a->ai_socktype, a->ai_protocol);
if (sock == -1)
error (EXIT_FAILURE, errno, "socket");
opt = 1;
if (setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) == -1)
perror ("setsockopt: SO_REUSEADDR");
#ifdef IPV6_V6ONLY
if (a->ai_family == PF_INET6) {
if (setsockopt (sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof opt) == -1)
perror ("setsockopt: IPv6 only");
}
#endif
if (bind (sock, a->ai_addr, a->ai_addrlen) == -1) {
if (errno == EADDRINUSE) {
addr_in_use = 1;
close (sock);
continue;
}
perror ("bind");
close (sock);
continue;
}
if (listen (sock, SOMAXCONN) == -1) {
perror ("listen");
close (sock);
continue;
}
nr_fds++;
fds = realloc (fds, sizeof (int) * nr_fds);
if (!fds)
error (EXIT_FAILURE, errno, "realloc");
fds[nr_fds-1] = sock;
}
freeaddrinfo (ai);
if (nr_fds == 0 && addr_in_use) {
#if DEBUG_STDERR
- fprintf (stderr, "%s: unable to bind to %s:%s: %s\n",
- g_get_prgname (), ipaddr ? ipaddr : "<any>", port,
- strerror (EADDRINUSE));
+ fprintf (stderr, "%s: unable to bind to localhost:%s: %s\n",
+ g_get_prgname (), port, strerror (EADDRINUSE));
#endif
return -1;
}
#if DEBUG_STDERR
- fprintf (stderr, "%s: bound to IP address %s:%s (%zu socket(s))\n",
- g_get_prgname (), ipaddr ? ipaddr : "<any>", port, nr_fds);
+ fprintf (stderr, "%s: bound to localhost:%s (%zu socket(s))\n",
+ g_get_prgname (), port, nr_fds);
#endif
*fds_rtn = fds;
*nr_fds_rtn = nr_fds;
return 0;
}
/**
* Wait for a local NBD server to start and be listening for
* connections.
*/
int
-wait_for_nbd_server_to_start (const char *ipaddr, int port)
+wait_for_nbd_server_to_start (int port)
{
int sockfd = -1;
int result = -1;
time_t start_t, now_t;
struct timespec half_sec = { .tv_sec = 0, .tv_nsec = 500000000 };
struct timeval timeout = { .tv_usec = 0 };
char magic[8]; /* NBDMAGIC */
size_t bytes_read = 0;
ssize_t recvd;
time (&start_t);
for (;;) {
time (&now_t);
if (now_t - start_t >= WAIT_NBD_TIMEOUT) {
set_nbd_error ("timed out waiting for NBD server to start");
goto cleanup;
}
/* Source port for probing NBD server should be one greater than
* port. It's not guaranteed to always bind to this port, but it
* will hint the kernel to start there and try incrementally
* higher ports if needed. This avoids the case where the kernel
* selects port as our source port, and we immediately connect to
* ourself. See:
*
https://bugzilla.redhat.com/show_bug.cgi?id=1167774#c9
*/
- sockfd = connect_with_source_port (ipaddr, port, port+1);
+ sockfd = connect_with_source_port (port, port+1);
if (sockfd >= 0)
break;
nanosleep (&half_sec, NULL);
}
time (&now_t);
timeout.tv_sec = (start_t + WAIT_NBD_TIMEOUT) - now_t;
if (setsockopt (sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout) == -1)
{
set_nbd_error ("waiting for NBD server to start: "
"setsockopt(SO_RCVTIMEO): %m");
goto cleanup;
}
do {
recvd = recv (sockfd, magic, sizeof magic - bytes_read, 0);
if (recvd == -1) {
set_nbd_error ("waiting for NBD server to start: recv: %m");
goto cleanup;
}
bytes_read += recvd;
} while (bytes_read < sizeof magic);
if (memcmp (magic, "NBDMAGIC", sizeof magic) != 0) {
set_nbd_error ("waiting for NBD server to start: "
"'NBDMAGIC' was not received from NBD server");
goto cleanup;
}
result = 0;
cleanup:
if (sockfd >= 0)
close (sockfd);
return result;
}
/**
- * Connect to C<hostname:dest_port>, resolving the address using
+ * Connect to C<localhost:dest_port>, resolving the address using
* L<getaddrinfo(3)>.
*
* This also sets the source port of the connection to the first free
* port number E<ge> C<source_port>.
*
* This may involve multiple connections - to IPv4 and IPv6 for
* instance.
*/
static int
-connect_with_source_port (const char *hostname, int dest_port, int source_port)
+connect_with_source_port (int dest_port, int source_port)
{
struct addrinfo hints;
struct addrinfo *results, *rp;
char dest_port_str[16];
int r, sockfd = -1;
int reuseaddr = 1;
snprintf (dest_port_str, sizeof dest_port_str, "%d", dest_port);
memset (&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; /* allow IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV; /* numeric dest port number */
hints.ai_protocol = 0; /* any protocol */
- r = getaddrinfo (hostname, dest_port_str, &hints, &results);
+ r = getaddrinfo ("localhost", dest_port_str, &hints, &results);
if (r != 0) {
- set_nbd_error ("getaddrinfo: %s/%s: %s",
- hostname, dest_port_str, gai_strerror (r));
+ set_nbd_error ("getaddrinfo: localhost/%s: %s",
+ dest_port_str, gai_strerror (r));
return -1;
}
for (rp = results; rp != NULL; rp = rp->ai_next) {
sockfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sockfd == -1)
continue;
/* If we run p2v repeatedly (say, running the tests in a loop),
* there's a decent chance we'll end up trying to bind() to a port
* that is in TIME_WAIT from a prior run. Handle that gracefully
* with SO_REUSEADDR.
*/
if (setsockopt (sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuseaddr, sizeof reuseaddr) == -1)
perror ("warning: setsockopt");
/* Need to bind the source port. */
if (bind_source_port (sockfd, rp->ai_family, source_port) == -1) {
close (sockfd);
sockfd = -1;
continue;
}
/* Connect. */
if (connect (sockfd, rp->ai_addr, rp->ai_addrlen) == -1) {
set_nbd_error ("waiting for NBD server to start: "
- "connect to %s/%s: %m",
- hostname, dest_port_str);
+ "connect to localhost/%s: %m", dest_port_str);
close (sockfd);
sockfd = -1;
continue;
}
break;
}
freeaddrinfo (results);
return sockfd;
}
diff --git a/ssh.c b/ssh.c
index 42701433f0cc..5e1d60b8a2e7 100644
--- a/ssh.c
+++ b/ssh.c
@@ -1062,69 +1062,67 @@ compatible_version (const char *v2v_version)
}
mexp_h *
-open_data_connection (struct config *config,
- const char *local_ipaddr, int local_port,
- int *remote_port)
+open_data_connection (struct config *config, int local_port, int *remote_port)
{
mexp_h *h;
char remote_arg[32];
const char *extra_args[] = {
"-R", remote_arg,
"-N",
NULL
};
CLEANUP_FREE char *port_str = NULL;
const int ovecsize = 12;
int ovector[ovecsize];
- snprintf (remote_arg, sizeof remote_arg, "0:%s:%d", local_ipaddr,
local_port);
+ snprintf (remote_arg, sizeof remote_arg, "0:localhost:%d", local_port);
h = start_ssh (0, config, (char **) extra_args, 0);
if (h == NULL)
return NULL;
switch (mexp_expect (h,
(mexp_regexp[]) {
{ 100, .re = portfwd_re },
{ 0 }
}, ovector, ovecsize)) {
case 100: /* Ephemeral port. */
port_str = strndup (&h->buffer[ovector[2]], ovector[3]-ovector[2]);
if (port_str == NULL) {
set_ssh_internal_error ("strndup: %m");
mexp_close (h);
return NULL;
}
if (sscanf (port_str, "%d", remote_port) != 1) {
set_ssh_internal_error ("cannot extract the port number from
'%s'",
port_str);
mexp_close (h);
return NULL;
}
break;
case MEXP_EOF:
set_ssh_unexpected_eof ("\"ssh -R\" output");
mexp_close (h);
return NULL;
case MEXP_TIMEOUT:
set_ssh_unexpected_timeout ("\"ssh -R\" output");
mexp_close (h);
return NULL;
case MEXP_ERROR:
set_ssh_mexp_error ("mexp_expect");
mexp_close (h);
return NULL;
case MEXP_PCRE_ERROR:
set_ssh_pcre_error ();
mexp_close (h);
return NULL;
}
return h;
}
/* Wait for the prompt. */
--
2.19.1.3.g30247aa5d201