Don't hardcode interpreter path
[libvirt-sandbox.git] / bin / virt-sandbox.c
1 /*
2  * virt-sandbox.c: libvirt sandbox command
3  *
4  * Copyright (C) 2011 Red Hat, Inc.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19  *
20  * Author: Daniel P. Berrange <berrange@redhat.com>
21  */
22
23 #include <config.h>
24
25 #include <libvirt-sandbox/libvirt-sandbox.h>
26 #include <glib/gi18n.h>
27 #include <sys/types.h>
28 #include <pwd.h>
29
30 static gboolean do_close(GVirSandboxConsole *con G_GNUC_UNUSED,
31                          gboolean error G_GNUC_UNUSED,
32                          gpointer opaque)
33 {
34     GMainLoop *loop = opaque;
35     g_main_loop_quit(loop);
36     return FALSE;
37 }
38
39 static gboolean do_delayed_close(gpointer opaque)
40 {
41     GMainLoop *loop = opaque;
42     g_main_loop_quit(loop);
43     return FALSE;
44 }
45
46 static gboolean do_pending_close(GVirSandboxConsole *con G_GNUC_UNUSED,
47                                  gboolean error G_GNUC_UNUSED,
48                                  gpointer opaque)
49 {
50     GMainLoop *loop = opaque;
51     g_timeout_add(2000, do_delayed_close, loop);
52     return FALSE;
53 }
54
55 static gboolean do_exited(GVirSandboxConsole *con G_GNUC_UNUSED,
56                           int status,
57                           gpointer opaque)
58 {
59     int *ret = opaque;
60     *ret = WEXITSTATUS(status);
61     return FALSE;
62 }
63
64 static void libvirt_sandbox_version(void)
65 {
66     g_print(_("%s version %s\n"), PACKAGE, VERSION);
67
68     exit(EXIT_SUCCESS);
69 }
70
71
72 int main(int argc, char **argv) {
73     int ret = EXIT_FAILURE;
74     GVirConnection *hv = NULL;
75     GVirSandboxConfig *cfg = NULL;
76     GVirSandboxConfigInteractive *icfg = NULL;
77     GVirSandboxContext *ctx = NULL;
78     GVirSandboxContextInteractive *ictx = NULL;
79     GVirSandboxConsole *log = NULL;
80     GVirSandboxConsole *con = NULL;
81     GMainLoop *loop = NULL;
82     GError *error = NULL;
83     gchar *name = NULL;
84     gchar **disks = NULL;
85     gchar **envs = NULL;
86     gchar **mounts = NULL;
87     gchar **includes = NULL;
88     gchar *includefile = NULL;
89     gchar *uri = NULL;
90     gchar *security = NULL;
91     gchar **networks = NULL;
92     gchar **cmdargs = NULL;
93     gchar *root = NULL;
94     gchar *kernver = NULL;
95     gchar *kernpath = NULL;
96     gchar *kmodpath = NULL;
97     gchar *switchto = NULL;
98     gboolean verbose = FALSE;
99     gboolean debug = FALSE;
100     gboolean shell = FALSE;
101     gboolean privileged = FALSE;
102     GOptionContext *context;
103     GOptionEntry options[] = {
104         { "version", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK,
105           libvirt_sandbox_version, N_("display version information"), NULL },
106         { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose,
107           N_("display verbose information"), NULL },
108         { "debug", 'd', 0, G_OPTION_ARG_NONE, &debug,
109           N_("display debugging information"), NULL },
110         { "connect", 'c', 0, G_OPTION_ARG_STRING, &uri,
111           N_("connect to hypervisor"), "URI"},
112         { "name", 'n', 0, G_OPTION_ARG_STRING, &name,
113           N_("name of the sandbox"), "NAME" },
114         { "root", 'r', 0, G_OPTION_ARG_STRING, &root,
115           N_("root directory of the sandbox"), "DIR" },
116         { "disk", ' ', 0, G_OPTION_ARG_STRING_ARRAY, &disks,
117           N_("add a disk in the guest"), "TYPE:TAGNAME=SOURCE,format=FORMAT" },
118         { "env", 'e', 0, G_OPTION_ARG_STRING_ARRAY, &envs,
119           N_("add a environment variable for the sandbox"), "KEY=VALUE" },
120         { "mount", 'm', 0, G_OPTION_ARG_STRING_ARRAY, &mounts,
121           N_("mount a filesystem in the guest"), "TYPE:TARGET=SOURCE" },
122         { "include", 'i', 0, G_OPTION_ARG_STRING_ARRAY, &includes,
123           N_("file to copy into custom dir"), "GUEST-PATH=HOST-PATH", },
124         { "includefile", 'I', 0, G_OPTION_ARG_STRING, &includefile,
125           N_("file contain list of files to include"), "FILE" },
126         { "network", 'N', 0, G_OPTION_ARG_STRING_ARRAY, &networks,
127           N_("setup network interface properties"), "PATH", },
128         { "security", 's', 0, G_OPTION_ARG_STRING, &security,
129           N_("security properties"), "PATH", },
130         { "privileged", 'p', 0, G_OPTION_ARG_NONE, &privileged,
131           N_("run the command privileged"), NULL },
132         { "switchto", 'S', 0, G_OPTION_ARG_STRING, &switchto,
133           N_("switch to the given user"), "USER" },
134         { "shell", 'l', 0, G_OPTION_ARG_NONE, &shell,
135           N_("start a shell"), NULL, },
136         { "kernver", 0, 0, G_OPTION_ARG_STRING, &kernver,
137           N_("kernel version"), NULL, },
138         { "kernpath", 0, 0, G_OPTION_ARG_STRING, &kernpath,
139           N_("kernel binary path"), NULL, },
140         { "kmodpath", 0, 0, G_OPTION_ARG_STRING, &kmodpath,
141           N_("kernel module directory"), NULL, },
142         { G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_STRING_ARRAY, &cmdargs,
143           NULL, "COMMAND-PATH [ARGS...]" },
144         { NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
145     };
146     const char *help_msg = N_("Run 'virt-sandbox --help' to see a full list of available command line options");
147     struct passwd *pw;
148
149     setlocale(LC_ALL, "");
150     bindtextdomain(PACKAGE, LOCALEDIR);
151     bind_textdomain_codeset(PACKAGE, "UTF-8");
152     textdomain(PACKAGE);
153
154     if (!gvir_sandbox_init_check(&argc, &argv, &error))
155         exit(EXIT_FAILURE);
156
157     g_set_application_name(_("Libvirt Sandbox"));
158
159     context = g_option_context_new (_("- Libvirt Sandbox"));
160     g_option_context_add_main_entries (context, options, NULL);
161     g_option_context_parse (context, &argc, &argv, &error);
162     if (error) {
163         g_printerr("%s\n%s\n",
164                    error->message,
165                    gettext(help_msg));
166         goto cleanup;
167     }
168
169     g_option_context_free(context);
170
171     if (!cmdargs || (g_strv_length(cmdargs) < 1)) {
172         g_printerr(_("\nUsage: %s [OPTIONS] COMMAND-PATH [ARGS...]\n\n%s\n\n"), argv[0], help_msg);
173         goto cleanup;
174     }
175
176     if (debug) {
177         setenv("LIBVIRT_LOG_FILTERS", "1:libvirt", 1);
178         setenv("LIBVIRT_LOG_OUTPUTS", "stderr", 1);
179     } else if (verbose) {
180         setenv("LIBVIRT_LOG_FILTERS", "1:libvirt", 1);
181         setenv("LIBVIRT_LOG_OUTPUTS", "stderr", 1);
182     }
183
184     loop = g_main_loop_new(g_main_context_default(), FALSE);
185
186     hv = gvir_connection_new(uri);
187     if (!gvir_connection_open(hv, NULL, &error)) {
188         g_printerr(_("Unable to open connection: %s\n"),
189                    error && error->message ? error->message : _("Unknown failure"));
190         goto cleanup;
191     }
192
193     icfg = gvir_sandbox_config_interactive_new(name ? name : "sandbox");
194     cfg = GVIR_SANDBOX_CONFIG(icfg);
195     gvir_sandbox_config_interactive_set_command(icfg, cmdargs);
196
197     if (root)
198         gvir_sandbox_config_set_root(cfg, root);
199
200     if (kernver)
201         gvir_sandbox_config_set_kernrelease(cfg, kernver);
202     if (kernpath)
203         gvir_sandbox_config_set_kernpath(cfg, kernpath);
204     if (kmodpath)
205         gvir_sandbox_config_set_kmodpath(cfg, kmodpath);
206
207     if (privileged && switchto) {
208         g_printerr(_("'switchto' and 'privileged' are incompatible options\n"));
209         goto cleanup;
210     }
211
212     if (privileged) {
213         gvir_sandbox_config_set_userid(cfg, 0);
214         gvir_sandbox_config_set_groupid(cfg, 0);
215         gvir_sandbox_config_set_username(cfg, "root");
216     } else if (switchto) {
217         pw = getpwnam(switchto);
218         if (!pw) {
219             g_printerr(_("Failed to resolve user %s\n"), switchto);
220             goto cleanup;
221         }
222         gvir_sandbox_config_set_userid(cfg, pw->pw_uid);
223         gvir_sandbox_config_set_groupid(cfg, pw->pw_gid);
224         gvir_sandbox_config_set_username(cfg, pw->pw_name);
225         gvir_sandbox_config_set_homedir(cfg, pw->pw_dir);
226     }
227
228     if (envs &&
229         !gvir_sandbox_config_add_env_strv(cfg, envs, &error)) {
230         g_printerr(_("Unable to parse custom environment variables: %s\n"),
231                    error && error->message ? error->message : _("Unknown failure"));
232         goto cleanup;
233     }
234
235     if (disks &&
236         !gvir_sandbox_config_add_disk_strv(cfg, disks, &error)) {
237         g_printerr(_("Unable to parse disks: %s\n"),
238                    error && error->message ? error->message : _("Unknown failure"));
239         goto cleanup;
240     }
241
242     if (mounts &&
243         !gvir_sandbox_config_add_mount_strv(cfg, mounts, &error)) {
244         g_printerr(_("Unable to parse mounts: %s\n"),
245                    error && error->message ? error->message : _("Unknown failure"));
246         goto cleanup;
247     }
248     if (includes &&
249         !gvir_sandbox_config_add_host_include_strv(cfg, includes, &error)) {
250         g_printerr(_("Unable to parse includes: %s\n"),
251                    error && error->message ? error->message : _("Unknown failure"));
252         goto cleanup;
253     }
254     if (includefile &&
255         !gvir_sandbox_config_add_host_include_file(cfg, includefile, &error)) {
256         g_printerr(_("Unable to parse include file: %s\n"),
257                    error && error->message ? error->message : _("Unknown failure"));
258         goto cleanup;
259     }
260     if (networks &&
261         !gvir_sandbox_config_add_network_strv(cfg, networks, &error)) {
262         g_printerr(_("Unable to parse networks: %s\n"),
263                    error && error->message ? error->message : _("Unknown failure"));
264         goto cleanup;
265     }
266     if (security &&
267         !gvir_sandbox_config_set_security_opts(cfg, security, &error)) {
268         g_printerr(_("Unable to parse security: %s\n"),
269                    error && error->message ? error->message : _("Unknown failure"));
270         goto cleanup;
271     }
272
273     if (shell)
274         gvir_sandbox_config_set_shell(cfg, TRUE);
275
276     if (isatty(STDIN_FILENO))
277         gvir_sandbox_config_interactive_set_tty(icfg, TRUE);
278
279     ictx = gvir_sandbox_context_interactive_new(hv, icfg);
280     ctx = GVIR_SANDBOX_CONTEXT(ictx);
281
282     if (!gvir_sandbox_context_start(ctx, &error)) {
283         g_printerr(_("Unable to start sandbox: %s\n"),
284                    error && error->message ? error->message : _("Unknown failure"));
285         goto cleanup;
286     }
287
288     if (!(log = gvir_sandbox_context_get_log_console(ctx, &error))) {
289         g_printerr(_("Unable to get log console: %s\n"),
290                    error && error->message ? error->message : _("Unknown failure"));
291         goto cleanup;
292     }
293     g_signal_connect(log, "closed", (GCallback)do_close, loop);
294
295     if (!(gvir_sandbox_console_attach_stderr(log, &error))) {
296         g_printerr(_("Unable to attach sandbox console: %s\n"),
297                    error && error->message ? error->message : _("Unknown failure"));
298         goto cleanup;
299     }
300
301     if (!(con = gvir_sandbox_context_interactive_get_app_console(ictx, &error))) {
302         g_printerr(_("Unable to get app console: %s\n"),
303                    error && error->message ? error->message : _("Unknown failure"));
304         goto cleanup;
305     }
306     /* We don't close right away - we want to ensure we read any
307      * final debug info from the log console. We should get an
308      * EOF on that console which will trigger the real close,
309      * but we schedule a timer just in case.
310      */
311     g_signal_connect(con, "closed", (GCallback)do_pending_close, loop);
312     g_signal_connect(con, "exited", (GCallback)do_exited, &ret);
313
314     if (!(gvir_sandbox_console_attach_stdio(con, &error))) {
315         g_printerr(_("Unable to attach sandbox console: %s\n"),
316                    error && error->message ? error->message : _("Unknown failure"));
317         goto cleanup;
318     }
319
320     g_main_loop_run(loop);
321
322 cleanup:
323     if (error)
324         g_error_free(error);
325     if (con)
326         gvir_sandbox_console_detach(con, NULL);
327     if (ctx) {
328         gvir_sandbox_context_stop(ctx, NULL);
329         g_object_unref(ctx);
330     }
331     if (cfg)
332         g_object_unref(cfg);
333     if (loop)
334         g_main_loop_unref(loop);
335     if (hv)
336         g_object_unref(hv);
337
338     return ret;
339 }
340
341 /*
342 =head1 NAME
343
344 virt-sandbox - Run cmd under a virtual machine sandbox
345
346 =head1 SYNOPSIS
347
348 virt-sandbox [OPTIONS...] COMMAND
349
350 virt-sandbox [OPTIONS...] -- COMMAND [CMDARG1 [CMDARG2 [...]]]
351
352 =head1 DESCRIPTION
353
354 Run the C<cmd>  application within a tightly confined virtual machine. The
355 default sandbox domain only allows applications the ability to read and
356 write stdin, stdout and any other file descriptors handed to it. It is
357 not allowed to open any other files.
358
359 =head1 OPTIONS
360
361 =over 8
362
363 =item B<-c URI>, B<--connect=URI>
364
365 Set the libvirt connection URI, defaults to qemu:///session if
366 omitted. Alternatively the C<LIBVIRT_DEFAULT_URI> environment
367 variable can be set, or the config file C</etc/libvirt/libvirt.conf>
368 can have a default URI set.  Currently only the QEMU and LXC drivers
369 are supported.
370
371 =item B<-n NAME>, B<--name=NAME>
372
373 Set the unique name for the sandbox. This defaults to B<sandbox>
374 but this will need to be changed if more than one sandbox is to
375 be run concurrently. This is used as the name of the libvirt
376 virtual machine or container.
377
378 =item B<-r DIR>, B<--root DIR>
379
380 Use B<DIR> as the root directory of the sandbox, instead of
381 inheriting the host's root filesystem.
382
383 NB. C<DIR> must contain a matching install of the libvirt-sandbox
384 package. This restriction may be lifted in a future version.
385
386 =item B<--env key=value>
387
388 Sets up a custom environment variable on a running sandbox.
389
390 =item B<--disk TYPE:TAGNAME=SOURCE,format=FORMAT>
391
392 Sets up a disk inside the sandbox by using B<SOURCE> with a symlink named as B<TAGNAME>
393 and type B<TYPE> and format B<FORMAT>. Example: file:cache=/var/lib/sandbox/demo/tmp.qcow2,format=qcow2
394 Format is an optional parameter.
395
396 =over 4
397
398 =item B<TYPE>
399
400 Type parameter can be set to "file".
401
402 =item B<TAGNAME>
403
404 TAGNAME will be created under /dev/disk/by-tag/TAGNAME. It will be linked to the device under /dev
405
406 =item B<SOURCE>
407
408 Source parameter needs to point a file which must be a one of the valid domain disk formats supported by qemu.
409
410 =item B<FORMAT>
411
412 Format parameter must be set to the same disk format as the file passed on source parameter.
413 This parameter is optional and the format can be guessed from the image extension
414
415 =back
416
417 =item B<-m TYPE:DST=SRC>, B<--mount TYPE:DST=SRC>
418
419 Sets up a mount inside the sandbox at B<DST> backed by B<SRC>. The
420 meaning of B<SRC> depends on the value of C<TYPE> specified:
421
422 =over 4
423
424 =item B<host-bind>
425
426 If B<TYPE> is B<host-bind>, then B<SRC> is interpreted as the path
427 to a directory on the host filesystem. If C<SRC> is the empty string,
428 then a temporary (empty) directory is created on the host before
429 starting the sandbox and deleted afterwards. The C<--include> option
430 is useful for populating these temporary directories with copies of host
431 files.
432
433 =item B<host-image>
434
435 If B<TYPE> is B<host-image>, then B<SRC> is interpreted as the path
436 to a disk image file on the host filesystem. The image should be
437 formatted with a filesystem that can be auto-detected by the sandbox,
438 such as B<ext3>, B<ext4>, etc. The disk image itself should be a raw
439 file, not qcow2 or any other special format
440
441 =item B<guest-bind>
442
443 If B<TYPE> is B<guest-bind>, then B<SRC> is interpreted as the path
444 to another directory in the container filesystem.
445
446 =item B<ram>
447
448 If B<TYPE> is B<ram>, then B<SRC> is interpreted as specifying the
449 size of the RAM disk in bytes. The suffix B<K>, B<KiB>, B<M>,
450 B<MiB>, B<G>, B<GiB> can used to alter the units from bytes to a
451 coarser level.
452
453 =back
454
455 Some examples
456
457  -m host-bind:/tmp=/var/lib/sandbox/demo/tmp
458  -m host-image:/=/var/lib/sandbox/demo.img
459  -m guest-bind:/home=/tmp/home
460  -m ram:/tmp=500M
461
462 =item B<-I HOST-PATH>, B<--includefile=HOST-PATH>
463
464 Copy all files listed in inputfile into the
465 appropriate temporary sandbox directories.
466
467 =item B<-N NETWORK-OPTIONS>, B<--network NETWORK-OPTIONS>
468
469 Add a network interface to the sandbox. NETWORK-OPTIONS is a set of
470 key=val pairs, separated by commas. The following options are valid
471
472 =over 4
473
474 =item dhcp
475
476 Configure the network interface using dhcp. This key takes no value.
477 No other keys may be specified. eg
478
479   -N dhcp,source=default
480   --network dhcp,source=lan
481
482 where 'source' is the name of any libvirt virtual network.
483
484 =item source=NETWORK
485
486 Set the name of the network to connect the interface to. C<NETWORK>
487 is the name of any libvirt virtual network. See also B<virsh net-list>
488
489 =item mac=NN:NN:NN:NN:NN:NN
490
491 Set the MAC address of the network interface, where each NN is a pair
492 of hex digits.
493
494 =item address=IP-ADDRESS/PREFIX%BROADCAST
495
496 Configure the network interface with the static IPv4 or IPv6 address
497 B<IP-ADDRESS>. The B<PREFIX> value is the length of the network
498 prefix in B<IP-ADDRESS>. The optional B<BROADCAST> parameter
499 specifies the broadcast address. Some examples
500
501   address=192.168.122.1/24
502   address=192.168.122.1/24%192.168.122.255
503   address=2001:212::204:2/64
504
505 =item route=IP-NETWORK/PREFIX%GATEWAY
506
507 Configure the network interface with the static IPv4 or IPv6 route
508 B<IP-NETWORK>. The B<PREFIX> value is the length of the network
509 prefix in B<IP-NETWORK>. The B<GATEWAY> parameter specifies the
510 address of the gateway for the route. Some examples
511
512   route=192.168.122.255/24%192.168.1.1
513
514 =back
515
516 =item B<-s SECURITY-OPTIONS>, B<--security=SECURITY-OPTIONS>
517
518 Use alternative security options. SECURITY-OPTIONS is a set of key=val pairs,
519 separated by commas. The following options are valid for SELinux
520
521 =over 4
522
523 =item dynamic
524
525 Dynamically allocate an SELinux label, using the default base context.
526 The default base context is system_u:system_r:svirt_lxc_net_t:s0 for LXC,
527 system_u:system_r:svirt_t:s0 for KVM, system_u:system_r:svirt_tcg_t:s0
528 for QEMU.
529
530 =item dynamic,label=USER:ROLE:TYPE:LEVEL
531
532 Dynamically allocate an SELinux label, using the base context
533 USER:ROLE:TYPE:LEVEL, instead of the default base context.
534
535 =item static,label=USER:ROLE:TYPE:LEVEL
536
537 To set a completely static label. For example,
538 static,label=system_u:system_r:svirt_t:s0:c412,c355
539
540 =item inherit
541
542 Inherit the context from the process that is executing virt-sandbox.
543
544 =back
545
546 =item B<--kernver=VERSION>
547
548 Specify the kernel version to run for machine based sandboxes. If
549 omitted, defaults to match the current running host version.
550
551 =item B<--kernpath=FILE-PATH>
552
553 Specify the path to the kernel binary. If omitted, defaults
554 to C</boot/vmlinuz-$KERNEL-VERSION>.
555
556 =item B<--kmodpath=DIR-PATH>
557
558 Specify the path to the kernel module base directory. If omitted, defaults
559 to C</lib/modules>. The suffix C<$KERNEL-VERSION/kernel> will be appended
560 to this path to locate the modules.
561
562 =item B<-p>, B<--privileged>
563
564 Retain root privileges inside the sandbox, rather than dropping privileges
565 to match the current user identity.
566
567 =item B<-S USER>, B<--switchto=USER>
568
569 Switch to the given user inside the sandbox and setup $HOME
570 accordingly.
571
572 =item B<-l>, B<--shell>
573
574 Launch an interactive shell on a secondary console device
575
576 =item B<-V>, B<--version>
577
578 Display the version number and exit
579
580 =item B<-v>, B<--verbose>
581
582 Display verbose progress information
583
584 =item B<-d>, B<--debug>
585
586 Display debugging information
587
588 =item B<-h>, B<--help>
589
590 Display help information
591
592 =back
593
594 =head1 EXAMPLES
595
596 Run an interactive shell under LXC, replace $HOME with the contents
597 of $HOME/scratch
598
599   # mkdir $HOME/scratch
600   # echo "hello" > $HOME/scratch/foo
601   # echo "sandbox" > $HOME/scratch/bar
602   # virt-sandbox -c lxc:/// -m host-bind:$HOME=$HOME/scratch -i $HOME/scratch/foo -i $HOME/scratch/bar /bin/sh
603
604 Convert an OGG file to WAV inside QEMU
605
606   # virt-sandbox -c qemu:///session  -- /usr/bin/oggdec -Q -o - - < somefile.ogg > somefile.wav
607
608 =head1 SEE ALSO
609
610 C<sandbox(8)>, C<virsh(1)>
611
612 =head1 AUTHORS
613
614 Daniel P. Berrange <dan@berrange.com>
615
616 =head1 COPYRIGHT
617
618 Copyright (C) 2011 Daniel P. Berrange <dan@berrange.com>
619 Copyright (C) 2011-2012 Red Hat, Inc.
620
621 =head1 LICENSE
622
623 virt-sandbox is distributed under the terms of the GNU LGPL v2+.
624 This is free software; see the source for copying conditions.
625 There is NO warranty; not even for MERCHANTABILITY or FITNESS
626 FOR A PARTICULAR PURPOSE
627
628 =cut
629
630  */
631
632 /*
633  * Local variables:
634  *  c-indent-level: 4
635  *  c-basic-offset: 4
636  *  indent-tabs-mode: nil
637  *  tab-width: 8
638  * End:
639  */