chiark / gitweb /
machined: add logic to query IP addresses of containers
[elogind.git] / src / machine / machined-dbus.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4   This file is part of systemd.
5
6   Copyright 2011 Lennart Poettering
7
8   systemd is free software; you can redistribute it and/or modify it
9   under the terms of the GNU Lesser General Public License as published by
10   the Free Software Foundation; either version 2.1 of the License, or
11   (at your option) any later version.
12
13   systemd is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   Lesser General Public License for more details.
17
18   You should have received a copy of the GNU Lesser General Public License
19   along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <errno.h>
23 #include <string.h>
24 #include <unistd.h>
25 #include <pwd.h>
26 #include <sys/capability.h>
27
28 #include "sd-id128.h"
29 #include "sd-messages.h"
30 #include "strv.h"
31 #include "mkdir.h"
32 #include "path-util.h"
33 #include "special.h"
34 #include "fileio-label.h"
35 #include "label.h"
36 #include "utf8.h"
37 #include "unit-name.h"
38 #include "bus-util.h"
39 #include "bus-errors.h"
40 #include "time-util.h"
41 #include "cgroup-util.h"
42 #include "machined.h"
43
44 static bool valid_machine_name(const char *p) {
45         size_t l;
46
47         if (!filename_is_safe(p))
48                 return false;
49
50         if (!ascii_is_valid(p))
51                 return false;
52
53         l = strlen(p);
54
55         if (l < 1 || l> 64)
56                 return false;
57
58         return true;
59 }
60
61 static int method_get_machine(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
62         _cleanup_free_ char *p = NULL;
63         Manager *m = userdata;
64         Machine *machine;
65         const char *name;
66         int r;
67
68         assert(bus);
69         assert(message);
70         assert(m);
71
72         r = sd_bus_message_read(message, "s", &name);
73         if (r < 0)
74                 return r;
75
76         machine = hashmap_get(m->machines, name);
77         if (!machine)
78                 return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
79
80         p = machine_bus_path(machine);
81         if (!p)
82                 return -ENOMEM;
83
84         return sd_bus_reply_method_return(message, "o", p);
85 }
86
87 static int method_get_machine_by_pid(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
88         _cleanup_free_ char *p = NULL;
89         Manager *m = userdata;
90         Machine *machine = NULL;
91         pid_t pid;
92         int r;
93
94         assert(bus);
95         assert(message);
96         assert(m);
97
98         assert_cc(sizeof(pid_t) == sizeof(uint32_t));
99
100         r = sd_bus_message_read(message, "u", &pid);
101         if (r < 0)
102                 return r;
103
104         if (pid == 0) {
105                 _cleanup_bus_creds_unref_ sd_bus_creds *creds = NULL;
106
107                 r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_PID, &creds);
108                 if (r < 0)
109                         return r;
110
111                 r = sd_bus_creds_get_pid(creds, &pid);
112                 if (r < 0)
113                         return r;
114         }
115
116         r = manager_get_machine_by_pid(m, pid, &machine);
117         if (r < 0)
118                 return r;
119         if (!machine)
120                 return sd_bus_error_setf(error, BUS_ERROR_NO_MACHINE_FOR_PID, "PID "PID_FMT" does not belong to any known machine", pid);
121
122         p = machine_bus_path(machine);
123         if (!p)
124                 return -ENOMEM;
125
126         return sd_bus_reply_method_return(message, "o", p);
127 }
128
129 static int method_list_machines(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
130         _cleanup_bus_message_unref_ sd_bus_message *reply = NULL;
131         Manager *m = userdata;
132         Machine *machine;
133         Iterator i;
134         int r;
135
136         assert(bus);
137         assert(message);
138         assert(m);
139
140         r = sd_bus_message_new_method_return(message, &reply);
141         if (r < 0)
142                 return sd_bus_error_set_errno(error, r);
143
144         r = sd_bus_message_open_container(reply, 'a', "(ssso)");
145         if (r < 0)
146                 return sd_bus_error_set_errno(error, r);
147
148         HASHMAP_FOREACH(machine, m->machines, i) {
149                 _cleanup_free_ char *p = NULL;
150
151                 p = machine_bus_path(machine);
152                 if (!p)
153                         return -ENOMEM;
154
155                 r = sd_bus_message_append(reply, "(ssso)",
156                                           machine->name,
157                                           strempty(machine_class_to_string(machine->class)),
158                                           machine->service,
159                                           p);
160                 if (r < 0)
161                         return sd_bus_error_set_errno(error, r);
162         }
163
164         r = sd_bus_message_close_container(reply);
165         if (r < 0)
166                 return sd_bus_error_set_errno(error, r);
167
168         return sd_bus_send(bus, reply, NULL);
169 }
170
171 static int method_create_or_register_machine(Manager *manager, sd_bus_message *message, Machine **_m, sd_bus_error *error) {
172         const char *name, *service, *class, *root_directory;
173         MachineClass c;
174         uint32_t leader;
175         sd_id128_t id;
176         const void *v;
177         Machine *m;
178         size_t n;
179         int r;
180
181         assert(manager);
182         assert(message);
183         assert(_m);
184
185         r = sd_bus_message_read(message, "s", &name);
186         if (r < 0)
187                 return r;
188         if (!valid_machine_name(name))
189                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid machine name");
190
191         r = sd_bus_message_read_array(message, 'y', &v, &n);
192         if (r < 0)
193                 return r;
194         if (n == 0)
195                 id = SD_ID128_NULL;
196         else if (n == 16)
197                 memcpy(&id, v, n);
198         else
199                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid machine ID parameter");
200
201         r = sd_bus_message_read(message, "ssus", &service, &class, &leader, &root_directory);
202         if (r < 0)
203                 return r;
204
205         if (isempty(class))
206                 c = _MACHINE_CLASS_INVALID;
207         else {
208                 c = machine_class_from_string(class);
209                 if (c < 0)
210                         return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid machine class parameter");
211         }
212
213         if (leader == 1)
214                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid leader PID");
215
216         if (!isempty(root_directory) && !path_is_absolute(root_directory))
217                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Root directory must be empty or an absolute path");
218
219         if (leader == 0) {
220                 _cleanup_bus_creds_unref_ sd_bus_creds *creds = NULL;
221
222                 r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_PID, &creds);
223                 if (r < 0)
224                         return r;
225
226                 assert_cc(sizeof(uint32_t) == sizeof(pid_t));
227
228                 r = sd_bus_creds_get_pid(creds, (pid_t*) &leader);
229                 if (r < 0)
230                         return r;
231         }
232
233         if (hashmap_get(manager->machines, name))
234                 return sd_bus_error_setf(error, BUS_ERROR_MACHINE_EXISTS, "Machine '%s' already exists", name);
235
236         r = manager_add_machine(manager, name, &m);
237         if (r < 0)
238                 return r;
239
240         m->leader = leader;
241         m->class = c;
242         m->id = id;
243
244         if (!isempty(service)) {
245                 m->service = strdup(service);
246                 if (!m->service) {
247                         r = -ENOMEM;
248                         goto fail;
249                 }
250         }
251
252         if (!isempty(root_directory)) {
253                 m->root_directory = strdup(root_directory);
254                 if (!m->root_directory) {
255                         r = -ENOMEM;
256                         goto fail;
257                 }
258         }
259
260         *_m = m;
261
262         return 1;
263
264 fail:
265         machine_add_to_gc_queue(m);
266         return r;
267 }
268
269 static int method_create_machine(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
270         Manager *manager = userdata;
271         Machine *m = NULL;
272         int r;
273
274         r = method_create_or_register_machine(manager, message, &m, error);
275         if (r < 0)
276                 return r;
277
278         r = sd_bus_message_enter_container(message, 'a', "(sv)");
279         if (r < 0)
280                 goto fail;
281
282         r = machine_start(m, message, error);
283         if (r < 0)
284                 goto fail;
285
286         m->create_message = sd_bus_message_ref(message);
287         return 1;
288
289 fail:
290         machine_add_to_gc_queue(m);
291         return r;
292 }
293
294 static int method_register_machine(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
295         Manager *manager = userdata;
296         _cleanup_free_ char *p = NULL;
297         Machine *m = NULL;
298         int r;
299
300         r = method_create_or_register_machine(manager, message, &m, error);
301         if (r < 0)
302                 return r;
303
304         r = cg_pid_get_unit(m->leader, &m->unit);
305         if (r < 0) {
306                 r = sd_bus_error_set_errnof(error, r, "Failed to determine unit of process "PID_FMT" : %s", m->leader, strerror(-r));
307                 goto fail;
308         }
309
310         r = machine_start(m, NULL, error);
311         if (r < 0)
312                 goto fail;
313
314         p = machine_bus_path(m);
315         if (!p) {
316                 r = -ENOMEM;
317                 goto fail;
318         }
319
320         return sd_bus_reply_method_return(message, "o", p);
321
322 fail:
323         machine_add_to_gc_queue(m);
324         return r;
325 }
326
327 static int method_terminate_machine(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
328         Manager *m = userdata;
329         Machine *machine;
330         const char *name;
331         int r;
332
333         assert(bus);
334         assert(message);
335         assert(m);
336
337         r = sd_bus_message_read(message, "s", &name);
338         if (r < 0)
339                 return sd_bus_error_set_errno(error, r);
340
341         machine = hashmap_get(m->machines, name);
342         if (!machine)
343                 return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
344
345         return bus_machine_method_terminate(bus, message, machine, error);
346 }
347
348 static int method_kill_machine(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
349         Manager *m = userdata;
350         Machine *machine;
351         const char *name;
352         int r;
353
354         assert(bus);
355         assert(message);
356         assert(m);
357
358         r = sd_bus_message_read(message, "s", &name);
359         if (r < 0)
360                 return sd_bus_error_set_errno(error, r);
361
362         machine = hashmap_get(m->machines, name);
363         if (!machine)
364                 return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
365
366         return bus_machine_method_kill(bus, message, machine, error);
367 }
368
369 static int method_get_machine_addresses(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
370         Manager *m = userdata;
371         Machine *machine;
372         const char *name;
373         int r;
374
375         assert(bus);
376         assert(message);
377         assert(m);
378
379         r = sd_bus_message_read(message, "s", &name);
380         if (r < 0)
381                 return sd_bus_error_set_errno(error, r);
382
383         machine = hashmap_get(m->machines, name);
384         if (!machine)
385                 return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
386
387         return bus_machine_method_get_addresses(bus, message, machine, error);
388 }
389
390 const sd_bus_vtable manager_vtable[] = {
391         SD_BUS_VTABLE_START(0),
392         SD_BUS_METHOD("GetMachine", "s", "o", method_get_machine, SD_BUS_VTABLE_UNPRIVILEGED),
393         SD_BUS_METHOD("GetMachineByPID", "u", "o", method_get_machine_by_pid, SD_BUS_VTABLE_UNPRIVILEGED),
394         SD_BUS_METHOD("ListMachines", NULL, "a(ssso)", method_list_machines, SD_BUS_VTABLE_UNPRIVILEGED),
395         SD_BUS_METHOD("CreateMachine", "sayssusa(sv)", "o", method_create_machine, 0),
396         SD_BUS_METHOD("RegisterMachine", "sayssus", "o", method_register_machine, 0),
397         SD_BUS_METHOD("KillMachine", "ssi", NULL, method_kill_machine, SD_BUS_VTABLE_CAPABILITY(CAP_KILL)),
398         SD_BUS_METHOD("TerminateMachine", "s", NULL, method_terminate_machine, SD_BUS_VTABLE_CAPABILITY(CAP_KILL)),
399         SD_BUS_METHOD("GetMachineAddresses", "s", "a(yay)", method_get_machine_addresses, SD_BUS_VTABLE_UNPRIVILEGED),
400         SD_BUS_SIGNAL("MachineNew", "so", 0),
401         SD_BUS_SIGNAL("MachineRemoved", "so", 0),
402         SD_BUS_VTABLE_END
403 };
404
405 int match_job_removed(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
406         const char *path, *result, *unit;
407         Manager *m = userdata;
408         Machine *machine;
409         uint32_t id;
410         int r;
411
412         assert(bus);
413         assert(message);
414         assert(m);
415
416         r = sd_bus_message_read(message, "uoss", &id, &path, &unit, &result);
417         if (r < 0) {
418                 bus_log_parse_error(r);
419                 return r;
420         }
421
422         machine = hashmap_get(m->machine_units, unit);
423         if (!machine)
424                 return 0;
425
426         if (streq_ptr(path, machine->scope_job)) {
427                 free(machine->scope_job);
428                 machine->scope_job = NULL;
429
430                 if (machine->started) {
431                         if (streq(result, "done"))
432                                 machine_send_create_reply(machine, NULL);
433                         else {
434                                 _cleanup_bus_error_free_ sd_bus_error e = SD_BUS_ERROR_NULL;
435
436                                 sd_bus_error_setf(&e, BUS_ERROR_JOB_FAILED, "Start job for unit %s failed with '%s'", unit, result);
437
438                                 machine_send_create_reply(machine, &e);
439                         }
440                 } else
441                         machine_save(machine);
442         }
443
444         machine_add_to_gc_queue(machine);
445         return 0;
446 }
447
448 int match_properties_changed(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
449         _cleanup_free_ char *unit = NULL;
450         Manager *m = userdata;
451         Machine *machine;
452         const char *path;
453         int r;
454
455         assert(bus);
456         assert(message);
457         assert(m);
458
459         path = sd_bus_message_get_path(message);
460         if (!path)
461                 return 0;
462
463         r = unit_name_from_dbus_path(path, &unit);
464         if (r < 0)
465                 return r;
466
467         machine = hashmap_get(m->machine_units, unit);
468         if (machine)
469                 machine_add_to_gc_queue(machine);
470
471         return 0;
472 }
473
474 int match_unit_removed(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
475         const char *path, *unit;
476         Manager *m = userdata;
477         Machine *machine;
478         int r;
479
480         assert(bus);
481         assert(message);
482         assert(m);
483
484         r = sd_bus_message_read(message, "so", &unit, &path);
485         if (r < 0) {
486                 bus_log_parse_error(r);
487                 return r;
488         }
489
490         machine = hashmap_get(m->machine_units, unit);
491         if (machine)
492                 machine_add_to_gc_queue(machine);
493
494         return 0;
495 }
496
497 int match_reloading(sd_bus *bus, sd_bus_message *message, void *userdata, sd_bus_error *error) {
498         Manager *m = userdata;
499         Machine *machine;
500         Iterator i;
501         int b, r;
502
503         assert(bus);
504
505         r = sd_bus_message_read(message, "b", &b);
506         if (r < 0) {
507                 bus_log_parse_error(r);
508                 return r;
509         }
510         if (b)
511                 return 0;
512
513         /* systemd finished reloading, let's recheck all our machines */
514         log_debug("System manager has been reloaded, rechecking machines...");
515
516         HASHMAP_FOREACH(machine, m->machines, i)
517                 machine_add_to_gc_queue(machine);
518
519         return 0;
520 }
521
522 int manager_start_scope(
523                 Manager *manager,
524                 const char *scope,
525                 pid_t pid,
526                 const char *slice,
527                 const char *description,
528                 sd_bus_message *more_properties,
529                 sd_bus_error *error,
530                 char **job) {
531
532         _cleanup_bus_message_unref_ sd_bus_message *m = NULL, *reply = NULL;
533         int r;
534
535         assert(manager);
536         assert(scope);
537         assert(pid > 1);
538
539         r = sd_bus_message_new_method_call(
540                         manager->bus,
541                         &m,
542                         "org.freedesktop.systemd1",
543                         "/org/freedesktop/systemd1",
544                         "org.freedesktop.systemd1.Manager",
545                         "StartTransientUnit");
546         if (r < 0)
547                 return r;
548
549         r = sd_bus_message_append(m, "ss", strempty(scope), "fail");
550         if (r < 0)
551                 return r;
552
553         r = sd_bus_message_open_container(m, 'a', "(sv)");
554         if (r < 0)
555                 return r;
556
557         if (!isempty(slice)) {
558                 r = sd_bus_message_append(m, "(sv)", "Slice", "s", slice);
559                 if (r < 0)
560                         return r;
561         }
562
563         if (!isempty(description)) {
564                 r = sd_bus_message_append(m, "(sv)", "Description", "s", description);
565                 if (r < 0)
566                         return r;
567         }
568
569         r = sd_bus_message_append(m, "(sv)", "PIDs", "au", 1, pid);
570         if (r < 0)
571                 return r;
572
573         if (more_properties) {
574                 r = sd_bus_message_copy(m, more_properties, true);
575                 if (r < 0)
576                         return r;
577         }
578
579         r = sd_bus_message_close_container(m);
580         if (r < 0)
581                 return r;
582
583         r = sd_bus_message_append(m, "a(sa(sv))", 0);
584         if (r < 0)
585                 return r;
586
587         r = sd_bus_call(manager->bus, m, 0, error, &reply);
588         if (r < 0)
589                 return r;
590
591         if (job) {
592                 const char *j;
593                 char *copy;
594
595                 r = sd_bus_message_read(reply, "o", &j);
596                 if (r < 0)
597                         return r;
598
599                 copy = strdup(j);
600                 if (!copy)
601                         return -ENOMEM;
602
603                 *job = copy;
604         }
605
606         return 1;
607 }
608
609 int manager_stop_unit(Manager *manager, const char *unit, sd_bus_error *error, char **job) {
610         _cleanup_bus_message_unref_ sd_bus_message *reply = NULL;
611         int r;
612
613         assert(manager);
614         assert(unit);
615
616         r = sd_bus_call_method(
617                         manager->bus,
618                         "org.freedesktop.systemd1",
619                         "/org/freedesktop/systemd1",
620                         "org.freedesktop.systemd1.Manager",
621                         "StopUnit",
622                         error,
623                         &reply,
624                         "ss", unit, "fail");
625         if (r < 0) {
626                 if (sd_bus_error_has_name(error, BUS_ERROR_NO_SUCH_UNIT) ||
627                     sd_bus_error_has_name(error, BUS_ERROR_LOAD_FAILED)) {
628
629                         if (job)
630                                 *job = NULL;
631
632                         sd_bus_error_free(error);
633                         return 0;
634                 }
635
636                 return r;
637         }
638
639         if (job) {
640                 const char *j;
641                 char *copy;
642
643                 r = sd_bus_message_read(reply, "o", &j);
644                 if (r < 0)
645                         return r;
646
647                 copy = strdup(j);
648                 if (!copy)
649                         return -ENOMEM;
650
651                 *job = copy;
652         }
653
654         return 1;
655 }
656
657 int manager_kill_unit(Manager *manager, const char *unit, int signo, sd_bus_error *error) {
658         assert(manager);
659         assert(unit);
660
661         return sd_bus_call_method(
662                         manager->bus,
663                         "org.freedesktop.systemd1",
664                         "/org/freedesktop/systemd1",
665                         "org.freedesktop.systemd1.Manager",
666                         "KillUnit",
667                         error,
668                         NULL,
669                         "ssi", unit, "all", signo);
670 }
671
672 int manager_unit_is_active(Manager *manager, const char *unit) {
673         _cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
674         _cleanup_bus_message_unref_ sd_bus_message *reply = NULL;
675         _cleanup_free_ char *path = NULL;
676         const char *state;
677         int r;
678
679         assert(manager);
680         assert(unit);
681
682         path = unit_dbus_path_from_name(unit);
683         if (!path)
684                 return -ENOMEM;
685
686         r = sd_bus_get_property(
687                         manager->bus,
688                         "org.freedesktop.systemd1",
689                         path,
690                         "org.freedesktop.systemd1.Unit",
691                         "ActiveState",
692                         &error,
693                         &reply,
694                         "s");
695         if (r < 0) {
696                 if (sd_bus_error_has_name(&error, SD_BUS_ERROR_NO_REPLY) ||
697                     sd_bus_error_has_name(&error, SD_BUS_ERROR_DISCONNECTED))
698                         return true;
699
700                 if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ||
701                     sd_bus_error_has_name(&error, BUS_ERROR_LOAD_FAILED))
702                         return false;
703
704                 return r;
705         }
706
707         r = sd_bus_message_read(reply, "s", &state);
708         if (r < 0)
709                 return -EINVAL;
710
711         return !streq(state, "inactive") && !streq(state, "failed");
712 }
713
714 int manager_job_is_active(Manager *manager, const char *path) {
715         _cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
716         _cleanup_bus_message_unref_ sd_bus_message *reply = NULL;
717         int r;
718
719         assert(manager);
720         assert(path);
721
722         r = sd_bus_get_property(
723                         manager->bus,
724                         "org.freedesktop.systemd1",
725                         path,
726                         "org.freedesktop.systemd1.Job",
727                         "State",
728                         &error,
729                         &reply,
730                         "s");
731         if (r < 0) {
732                 if (sd_bus_error_has_name(&error, SD_BUS_ERROR_NO_REPLY) ||
733                     sd_bus_error_has_name(&error, SD_BUS_ERROR_DISCONNECTED))
734                         return true;
735
736                 if (sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_OBJECT))
737                         return false;
738
739                 return r;
740         }
741
742         /* We don't actually care about the state really. The fact
743          * that we could read the job state is enough for us */
744
745         return true;
746 }
747
748 int manager_get_machine_by_pid(Manager *m, pid_t pid, Machine **machine) {
749         _cleanup_free_ char *unit = NULL;
750         Machine *mm;
751         int r;
752
753         assert(m);
754         assert(pid >= 1);
755         assert(machine);
756
757         r = cg_pid_get_unit(pid, &unit);
758         if (r < 0)
759                 mm = hashmap_get(m->machine_leaders, UINT_TO_PTR(pid));
760         else
761                 mm = hashmap_get(m->machine_units, unit);
762
763         if (!mm)
764                 return 0;
765
766         *machine = mm;
767         return 1;
768 }
769
770 int manager_add_machine(Manager *m, const char *name, Machine **_machine) {
771         Machine *machine;
772
773         assert(m);
774         assert(name);
775
776         machine = hashmap_get(m->machines, name);
777         if (!machine) {
778                 machine = machine_new(m, name);
779                 if (!machine)
780                         return -ENOMEM;
781         }
782
783         if (_machine)
784                 *_machine = machine;
785
786         return 0;
787 }