chiark / gitweb /
[PATCH] add scsi_id "extra" program from Patrick Mansfield <patmans@us.ibm.com>
[elogind.git] / extras / multipath / main.c
1 /*
2  * Soft:        Description here...
3  *
4  * Version:     $Id: main.h,v 0.0.1 2003/09/18 15:13:38 cvaroqui Exp $
5  *
6  * Author:      Copyright (C) 2003 Christophe Varoqui
7  *
8  *              This program is distributed in the hope that it will be useful,
9  *              but WITHOUT ANY WARRANTY; without even the implied warranty of
10  *              MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11  *              See the GNU General Public License for more details.
12  *
13  *              This program is free software; you can redistribute it and/or
14  *              modify it under the terms of the GNU General Public License
15  *              as published by the Free Software Foundation; either version
16  *              2 of the License, or (at your option) any later version.
17  */
18
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <unistd.h>
22 #include <fcntl.h>
23 #include <sys/types.h>
24 #include <sys/stat.h>
25 #include <linux/kdev_t.h>
26 #include <errno.h>
27 #include <string.h>
28 #include <sys/ioctl.h>
29 #include <libsysfs.h>
30 #include <libdevmapper.h>
31 #include "main.h"
32
33 static int
34 do_inq(int sg_fd, int cmddt, int evpd, unsigned int pg_op,
35        void *resp, int mx_resp_len, int noisy)
36 {
37         int res;
38         unsigned char inqCmdBlk[INQUIRY_CMDLEN] =
39             { INQUIRY_CMD, 0, 0, 0, 0, 0 };
40         unsigned char sense_b[SENSE_BUFF_LEN];
41         struct sg_io_hdr io_hdr;
42
43         if (cmddt)
44                 inqCmdBlk[1] |= 2;
45         if (evpd)
46                 inqCmdBlk[1] |= 1;
47         inqCmdBlk[2] = (unsigned char) pg_op;
48         inqCmdBlk[4] = (unsigned char) mx_resp_len;
49         memset(&io_hdr, 0, sizeof (struct sg_io_hdr));
50         io_hdr.interface_id = 'S';
51         io_hdr.cmd_len = sizeof (inqCmdBlk);
52         io_hdr.mx_sb_len = sizeof (sense_b);
53         io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
54         io_hdr.dxfer_len = mx_resp_len;
55         io_hdr.dxferp = resp;
56         io_hdr.cmdp = inqCmdBlk;
57         io_hdr.sbp = sense_b;
58         io_hdr.timeout = DEF_TIMEOUT;
59
60         if (ioctl(sg_fd, SG_IO, &io_hdr) < 0) {
61                 perror("SG_IO (inquiry) error");
62                 return -1;
63         }
64         res = sg_err_category3(&io_hdr);
65         switch (res) {
66         case SG_ERR_CAT_CLEAN:
67         case SG_ERR_CAT_RECOVERED:
68                 return 0;
69         default:
70                 return -1;
71         }
72 }
73
74 static int
75 do_tur(int fd)
76 {
77         unsigned char turCmdBlk[TUR_CMD_LEN] = { 0x00, 0, 0, 0, 0, 0 };
78         struct sg_io_hdr io_hdr;
79         unsigned char sense_buffer[32];
80
81         memset(&io_hdr, 0, sizeof (struct sg_io_hdr));
82         io_hdr.interface_id = 'S';
83         io_hdr.cmd_len = sizeof (turCmdBlk);
84         io_hdr.mx_sb_len = sizeof (sense_buffer);
85         io_hdr.dxfer_direction = SG_DXFER_NONE;
86         io_hdr.cmdp = turCmdBlk;
87         io_hdr.sbp = sense_buffer;
88         io_hdr.timeout = 20000;
89         io_hdr.pack_id = 0;
90         if (ioctl(fd, SG_IO, &io_hdr) < 0) {
91                 close(fd);
92                 return 0;
93         }
94         if (io_hdr.info & SG_INFO_OK_MASK) {
95                 return 0;
96         }
97         return 1;
98 }
99
100 static void
101 sprint_wwid(char * buff, const char * str)
102 {
103         int i;
104         const char *p;
105         char *cursor;
106         unsigned char c;
107
108         p = str;
109         cursor = buff;
110         for (i = 0; i <= WWID_SIZE / 2 - 1; i++) {
111                 c = *p++;
112                 sprintf(cursor, "%.2x", (int) (unsigned char) c);
113                 cursor += 2;
114         }
115         buff[WWID_SIZE - 1] = '\0';
116 }
117
118 static int
119 get_lun_strings(int fd, struct path * mypath)
120 {
121         char buff[36];
122
123         if (0 == do_inq(fd, 0, 0, 0, buff, 36, 1)) {
124                 memcpy(mypath->vendor_id, &buff[8], 8);
125                 memcpy(mypath->product_id, &buff[16], 16);
126                 memcpy(mypath->rev, &buff[32], 4);
127                 return 1;
128         }
129         return 0;
130 }
131
132 /*
133 static int
134 get_serial (int fd, char * str)
135 {
136         char buff[MX_ALLOC_LEN + 1];
137         int len;
138
139         if (0 == do_inq(fd, 0, 1, 0x80, buff, MX_ALLOC_LEN, 0)) {
140                 len = buff[3];
141                 if (len > 0) {
142                         memcpy(str, buff + 4, len);
143                         buff[len] = '\0';
144                 }
145                 return 1;
146         }
147         return 0;
148 }
149 */
150
151 /* hardware vendor specifics : add support for new models below */
152 static int
153 get_storageworks_wwid(int fd, char *str)
154 {
155         char buff[64];
156
157         if (0 == do_inq(fd, 0, 1, 0x83, buff, sizeof (buff), 1)) {
158                 sprint_wwid(str, &buff[8]);
159                 return 1;
160         }
161         return 0;
162 }
163
164 /* White list switch */
165 static int
166 get_unique_id(int fd, struct path * mypath)
167 {
168         if (strncmp(mypath->product_id, "HSV110 (C)COMPAQ", 16) == 0 ||
169             strncmp(mypath->product_id, "HSG80           ", 16) == 0) {
170                 get_storageworks_wwid(fd, mypath->wwid);
171                 return 0;
172         }
173
174         return 1;
175 }
176
177 static void
178 basename(char * str1, char * str2)
179 {
180         char *p = str1 + (strlen(str1) - 1);
181
182         while (*--p != '/')
183                 continue;
184         strcpy(str2, ++p);
185 }
186
187 static int
188 get_all_paths_sysfs(struct env * conf, struct path * all_paths)
189 {
190         int k=0;
191         int sg_fd;
192         struct sysfs_directory * sdir;
193         struct sysfs_directory * devp;
194         struct sysfs_dlink * linkp;
195         char buff[FILE_NAME_SIZE];
196
197         char block_path[FILE_NAME_SIZE];
198
199         sprintf(block_path, "%s/block", conf->sysfs_path);
200         sdir = sysfs_open_directory(block_path);
201         sysfs_read_directory(sdir);
202         devp = sdir->subdirs;
203         while (devp != NULL) {
204                 sysfs_read_directory(devp);
205                 linkp = devp->links;
206                 while (linkp != NULL) {
207                         if (!strncmp(linkp->name, "device", 6))
208                                 break;
209                         linkp = linkp->next;
210                 }
211                 if (linkp == NULL) {
212                         devp = devp->next;
213                         continue;
214                 }
215
216                 basename(devp->path, buff);
217                 sprintf(all_paths[k].sg_dev, "/dev/%s", buff);
218                 strcpy(all_paths[k].dev, all_paths[k].sg_dev);
219                 if ((sg_fd = open(all_paths[k].sg_dev, O_RDONLY)) < 0) {
220                         devp = devp->next;
221                         continue;
222                 }
223                 get_lun_strings(sg_fd, &all_paths[k]);
224                 get_unique_id(sg_fd, &all_paths[k]);
225                 all_paths[k].state = do_tur(sg_fd);
226                 close(sg_fd);
227                 basename(linkp->target->path, buff);
228                 sscanf(buff, "%i:%i:%i:%i",
229                         &all_paths[k].sg_id.host_no,
230                         &all_paths[k].sg_id.channel,
231                         &all_paths[k].sg_id.scsi_id,
232                         &all_paths[k].sg_id.lun);
233                 k++;
234                 devp = devp->next;
235         }
236         sysfs_close_directory(sdir);
237
238         return 0;
239 }
240
241 static int
242 get_all_paths_nosysfs(struct env * conf, struct path * all_paths,
243                       struct scsi_dev * all_scsi_ids)
244 {
245         int k, i, sg_fd;
246         char buff[FILE_NAME_SIZE];
247         char file_name[FILE_NAME_SIZE];
248
249         for (k = 0; k < conf->max_devs; k++) {
250                 strcpy(file_name, "/dev/sg");
251                 sprintf(buff, "%d", k);
252                 strncat(file_name, buff, FILE_NAME_SIZE);
253                 strcpy(all_paths[k].sg_dev, file_name);
254                 if ((sg_fd = open(file_name, O_RDONLY)) < 0)
255                         continue;
256                 get_lun_strings(sg_fd, &all_paths[k]);
257                 get_unique_id(sg_fd, &all_paths[k]);
258                 all_paths[k].state = do_tur(sg_fd);
259                 if (0 > ioctl(sg_fd, SG_GET_SCSI_ID, &(all_paths[k].sg_id)))
260                         printf("device %s failed on sg ioctl, skip\n",
261                                file_name);
262
263                 close(sg_fd);
264
265                 for (i = 0; i < conf->max_devs; i++) {
266                         if ((all_paths[k].sg_id.host_no ==
267                              all_scsi_ids[i].host_no)
268                             && (all_paths[k].sg_id.scsi_id ==
269                                 (all_scsi_ids[i].scsi_id.dev_id & 0xff))
270                             && (all_paths[k].sg_id.lun ==
271                                 ((all_scsi_ids[i].scsi_id.dev_id >> 8) & 0xff))
272                             && (all_paths[k].sg_id.channel ==
273                                 ((all_scsi_ids[i].scsi_id.
274                                   dev_id >> 16) & 0xff))) {
275                                 strcpy(all_paths[k].dev, all_scsi_ids[i].dev);
276                                 break;
277                         }
278                 }
279         }
280         return 0;
281 }
282
283 static int
284 get_all_scsi_ids(struct env * conf, struct scsi_dev * all_scsi_ids)
285 {
286         int k, big, little, res, host_no, fd;
287         char buff[64];
288         char fname[FILE_NAME_SIZE];
289         struct scsi_idlun my_scsi_id;
290
291         for (k = 0; k < conf->max_devs; k++) {
292                 strcpy(fname, "/dev/sd");
293                 if (k < 26) {
294                         buff[0] = 'a' + (char) k;
295                         buff[1] = '\0';
296                         strcat(fname, buff);
297                 } else if (k <= 255) {  /* assumes sequence goes x,y,z,aa,ab,ac etc */
298                         big = k / 26;
299                         little = k - (26 * big);
300                         big = big - 1;
301
302                         buff[0] = 'a' + (char) big;
303                         buff[1] = 'a' + (char) little;
304                         buff[2] = '\0';
305                         strcat(fname, buff);
306                 } else
307                         strcat(fname, "xxxx");
308
309                 if ((fd = open(fname, O_RDONLY)) < 0)
310                         continue;
311
312                 res = ioctl(fd, SCSI_IOCTL_GET_IDLUN, &my_scsi_id);
313                 if (res < 0) {
314                         close(fd);
315                         printf("Could not get scsi idlun\n");
316                         continue;
317                 }
318
319                 res = ioctl(fd, SCSI_IOCTL_GET_BUS_NUMBER, &host_no);
320                 if (res < 0) {
321                         close(fd);
322                         printf("Could not get host_no\n");
323                         continue;
324                 }
325
326                 close(fd);
327
328                 strcpy(all_scsi_ids[k].dev, fname);
329                 all_scsi_ids[k].scsi_id = my_scsi_id;
330                 all_scsi_ids[k].host_no = host_no;
331         }
332         return 0;
333 }
334
335 /* print_path style */
336 #define ALL     0
337 #define NOWWID  1
338
339 static void
340 print_path(struct path * all_paths, int k, int style)
341 {
342         if (style != NOWWID)
343                 printf("%s ", all_paths[k].wwid);
344         else
345                 printf(" \\_");
346         printf("(%i %i %i %i) ",
347                all_paths[k].sg_id.host_no,
348                all_paths[k].sg_id.channel,
349                all_paths[k].sg_id.scsi_id, all_paths[k].sg_id.lun);
350         printf("%s ", all_paths[k].sg_dev);
351         printf("op:%i ", all_paths[k].state);
352         printf("%s ", all_paths[k].dev);
353         printf("[%.16s]\n", all_paths[k].product_id);
354 }
355
356 static void
357 print_all_path(struct env * conf, struct path * all_paths)
358 {
359         int k;
360         char empty_buff[WWID_SIZE];
361
362         memset(empty_buff, 0, WWID_SIZE);
363         for (k = 0; k < conf->max_devs; k++) {
364                 if (memcmp(empty_buff, all_paths[k].wwid, WWID_SIZE) == 0)
365                         continue;
366                 print_path(all_paths, k, ALL);
367         }
368 }
369
370 static void
371 print_all_mp(struct path * all_paths, struct multipath * mp, int nmp)
372 {
373         int k, i;
374
375         for (k = 0; k <= nmp; k++) {
376                 printf("%s\n", mp[k].wwid);
377                 for (i = 0; i <= mp[k].npaths; i++)
378                         print_path(all_paths, PINDEX(k,i), NOWWID);
379         }
380 }
381
382 static int
383 coalesce_paths(struct env * conf, struct multipath * mp,
384                struct path * all_paths)
385 {
386         int k, i, nmp, np, already_done;
387         char empty_buff[WWID_SIZE];
388
389         nmp = -1;
390         already_done = 0;
391         memset(empty_buff, 0, WWID_SIZE);
392
393         for (k = 0; k < conf->max_devs - 1; k++) {
394                 /* skip this path if no unique id has been found */
395                 if (memcmp(empty_buff, all_paths[k].wwid, WWID_SIZE) == 0)
396                         continue;
397                 np = 0;
398
399                 for (i = 0; i <= nmp; i++) {
400                         if (0 == strcmp(mp[i].wwid, all_paths[k].wwid))
401                                 already_done = 1;
402                 }
403
404                 if (already_done) {
405                         already_done = 0;
406                         continue;
407                 }
408
409                 nmp++;
410                 strcpy(mp[nmp].wwid, all_paths[k].wwid);
411                 PINDEX(nmp,np) = k;
412
413                 for (i = k + 1; i < conf->max_devs; i++) {
414                         if (0 == strcmp(all_paths[k].wwid, all_paths[i].wwid)) {
415                                 np++;
416                                 PINDEX(nmp,np) = i;
417                                 mp[nmp].npaths = np;
418                         }
419                 }
420         }
421         return nmp;
422 }
423
424 static long
425 get_disk_size (struct env * conf, char * dev) {
426         long size;
427         int fd;
428         char attr_path[FILE_NAME_SIZE];
429         char buff[FILE_NAME_SIZE];
430         char basedev[FILE_NAME_SIZE];
431
432         if(conf->with_sysfs) {
433                 basename(dev, basedev);
434                 sprintf(attr_path, "%s/block/%s/size",
435                         conf->sysfs_path, basedev);
436                 if (0 > sysfs_read_attribute_value(attr_path, buff,
437                                          FILE_NAME_SIZE * sizeof(char)))
438                         return -1;
439                 size = atoi(buff);
440                 return size;
441         } else {
442                 if ((fd = open(dev, O_RDONLY)) < 0)
443                         return -1;
444                 if(!ioctl(fd, BLKGETSIZE, &size))
445                         return size;
446         }
447         return -1;
448 }
449
450 static int
451 make_dm_node(char * str)
452 {
453         int r = 0;
454         dev_t dev;
455         char buff[FILE_NAME_SIZE];
456         int major, minor;
457         struct dm_names * names;
458         unsigned next = 0;
459         struct dm_task *dmt;
460
461         if (!(dmt = dm_task_create(DM_DEVICE_LIST)))
462                 return 0;
463
464         if (!dm_task_run(dmt))
465                 goto out;
466
467         if (!(names = dm_task_get_names(dmt)))
468                 goto out;
469
470         if (!names->dev) {
471                 r = 1;
472                 goto out;
473         }
474
475         do {
476                 if (0 == strcmp(names->name, str))
477                         break;
478                 next = names->next;
479                 names = (void *) names + next;
480         } while (next);
481
482         major = (int) MAJOR(names->dev);
483         minor = (int) MINOR(names->dev);
484
485         dev = major << sizeof(dev_t);
486         dev = dev | minor;
487         sprintf(buff, "/dev/mapper/%s", str);
488         unlink(buff);
489         mknod(buff, 0600 | S_IFBLK, dev);
490
491         out:
492         dm_task_destroy(dmt);
493         return r;
494
495 }
496
497 /* future use ?
498 static int
499 del_map(char * str) {
500         struct dm_task *dmt;
501
502         if (!(dmt = dm_task_create(DM_DEVICE_REMOVE)))
503                 return 0;
504         if (!dm_task_set_name(dmt, str))
505                 goto delout;
506         if (!dm_task_run(dmt))
507                 goto delout;
508
509         printf("Deleted device map : %s\n", str);
510
511         delout:
512         dm_task_destroy(dmt);
513         return 1;
514 }
515 */
516
517 static int
518 add_map(struct env * conf, struct path * all_paths,
519         struct multipath * mp, int index, int op)
520 {
521         char params[255];
522         char * params_p;
523         struct dm_task *dmt;
524         int i, np;
525         long size = -1;
526
527         if (!(dmt = dm_task_create(op)))
528                 return 0;
529
530         if (!dm_task_set_name(dmt, mp[index].wwid))
531                 goto addout;
532         params_p = &params[0];
533
534         np = 0;
535         for (i=0; i<=mp[index].npaths; i++) {
536                 if ((1 == all_paths[PINDEX(index,i)].state) &&
537                         (0 == all_paths[PINDEX(index,i)].sg_id.scsi_type))
538                         np++;
539         }
540         if (np == 0)
541                 goto addout;
542         params_p += sprintf(params_p, "%i 32", np);
543         
544         for (i=0; i<=mp[index].npaths; i++) {
545                 if (( 0 == all_paths[PINDEX(index,i)].state) ||
546                         (0 != all_paths[PINDEX(index,i)].sg_id.scsi_type))
547                         continue;
548                 if (size < 0)
549                         size = get_disk_size(conf, all_paths[PINDEX(index,0)].dev);
550                 params_p += sprintf(params_p, " %s %i",
551                         all_paths[PINDEX(index,i)].dev, 0);
552         }
553
554         if (size < 0)
555                 goto addout;
556
557         if (!conf->quiet) {
558                 if (op == DM_DEVICE_RELOAD)
559                         printf("U|");
560                 if (op == DM_DEVICE_CREATE)
561                         printf("N|");
562                 printf("%s : 0 %li %s %s\n",
563                         mp[index].wwid, size, DM_TARGET, params);
564         }
565
566         if (!dm_task_add_target(dmt, 0, size, DM_TARGET, params))
567                 goto addout;
568
569         if (!dm_task_run(dmt))
570                 goto addout;
571
572
573         make_dm_node(mp[index].wwid);
574
575         addout:
576         dm_task_destroy(dmt);
577         return 1;
578 }
579
580 /*
581 static int
582 get_table(const char * str)
583 {
584         int r = 0;
585         struct dm_task *dmt;
586         void *next = NULL;
587         uint64_t start, length;
588         char *target_type = NULL;
589         char *params;
590
591         if (!(dmt = dm_task_create(DM_DEVICE_TABLE)))
592                 return 0;
593
594         if (!dm_task_set_name(dmt, str))
595                 goto out;
596
597         if (!dm_task_run(dmt))
598                 goto out;
599
600         do {
601                 next = dm_get_next_target(dmt, next, &start, &length,
602                                           &target_type, &params);
603                 if (target_type) {
604                         printf("%" PRIu64 " %" PRIu64 " %s %s\n",
605                                start, length, target_type, params);
606                 }
607         } while (next);
608
609         r = 1;
610
611       out:
612         dm_task_destroy(dmt);
613         return r;
614
615 }
616 */
617
618 static int
619 map_present(char * str)
620 {
621         int r = 0;
622         struct dm_task *dmt;
623         struct dm_names *names;
624         unsigned next = 0;
625
626         if (!(dmt = dm_task_create(DM_DEVICE_LIST)))
627                 return 0;
628
629         if (!dm_task_run(dmt))
630                 goto out;
631
632         if (!(names = dm_task_get_names(dmt)))
633                 goto out;
634
635         if (!names->dev) {
636                 goto out;
637         }
638
639         do {
640                 if (0 == strcmp(names->name, str))
641                         r = 1;
642                 next = names->next;
643                 names = (void *) names + next;
644         } while (next);
645
646         out:
647         dm_task_destroy(dmt);
648         return r;
649 }
650
651 static void
652 usage(char * progname)
653 {
654         fprintf(stderr, VERSION_STRING);
655         fprintf(stderr, "Usage: %s [-v|-q] [-d] [-m max_devs]\n", progname);
656         fprintf(stderr, "\t-v\t\tverbose, print all paths and multipaths\n");
657         fprintf(stderr, "\t-q\t\tquiet, no output at all\n");
658         fprintf(stderr, "\t-d\t\tdry run, do not create or update devmaps\n");
659         fprintf(stderr, "\t-m max_devs\tscan {max_devs} devices at most\n");
660         exit(1);
661 }
662
663 int
664 main(int argc, char *argv[])
665 {
666         struct multipath * mp;
667         struct path * all_paths;
668         struct scsi_dev * all_scsi_ids;
669         struct env conf;
670         int i, k, nmp;
671
672         /* Default behaviour */
673         conf.max_devs = MAX_DEVS;
674         conf.dry_run = 0;       /* 1 == Do not Create/Update devmaps */
675         conf.verbose = 0;       /* 1 == Print all_paths and mp */
676         conf.quiet = 0;         /* 1 == Do not even print devmaps */
677         conf.with_sysfs = 0;    /* Default to compat / suboptimal behaviour */
678
679         /* kindly provided by libsysfs */
680         if (0 == sysfs_get_mnt_path(conf.sysfs_path, FILE_NAME_SIZE))
681                 conf.with_sysfs = 1;
682
683         for (i = 1; i < argc; ++i) {
684                 if (0 == strcmp("-v", argv[i])) {
685                         if (conf.quiet == 1)
686                                 usage(argv[0]);
687                         conf.verbose = 1;
688                 } else if (0 == strcmp("-m", argv[i])) {
689                         conf.max_devs = atoi(argv[++i]);
690                         if (conf.max_devs < 2)
691                                 usage(argv[0]);
692                 } else if (0 == strcmp("-q", argv[i])) {
693                         if (conf.verbose == 1)
694                                 usage(argv[0]);
695                         conf.quiet = 1;
696                 } else if (0 == strcmp("-d", argv[i]))
697                         conf.dry_run = 1;
698                 else if (*argv[i] == '-') {
699                         fprintf(stderr, "Unknown switch: %s\n", argv[i]);
700                         usage(argv[0]);
701                 } else if (*argv[i] != '-') {
702                         fprintf(stderr, "Unknown argument\n");
703                         usage(argv[0]);
704                 }
705
706         }
707
708         /* dynamic allocations */
709         mp = malloc(conf.max_devs * sizeof(struct multipath));
710         all_paths = malloc(conf.max_devs * sizeof(struct path));
711         all_scsi_ids = malloc(conf.max_devs * sizeof(struct scsi_dev));
712         if (mp == NULL || all_paths == NULL || all_scsi_ids == NULL)
713                 exit(1);
714
715         if (!conf.with_sysfs) {
716                 get_all_scsi_ids(&conf, all_scsi_ids);
717                 get_all_paths_nosysfs(&conf, all_paths, all_scsi_ids);
718         } else {
719                 get_all_paths_sysfs(&conf, all_paths);
720         }
721         nmp = coalesce_paths(&conf, mp, all_paths);
722
723         if (conf.verbose) {
724                 print_all_path(&conf, all_paths);
725                 printf("\n");
726                 print_all_mp(all_paths, mp, nmp);
727                 printf("\n");
728         }
729
730         if (conf.dry_run)
731                 exit(0);
732
733         for (k=0; k<=nmp; k++) {
734                 if (map_present(mp[k].wwid)) {
735                         add_map(&conf, all_paths, mp, k, DM_DEVICE_RELOAD);
736                 } else {
737                         add_map(&conf, all_paths, mp, k, DM_DEVICE_CREATE);
738                 }
739         }
740         exit(0);
741 }