Commit 5e7dccad3621f6e2b572f309cf830a2c902cae80
Committed by
James Bottomley
1 parent
f6dd337ee4
Exists in
master
and in
7 other branches
[SCSI] scsi_dh: add EMC Clariion device handler
This adds support for EMC Clariions. This patch has the features that currently exists in mainline and advanced features from Ed's patches. Signed-off-by: Chandra Seetharaman <sekharan@us.ibm.com> Signed-off-by: Mike Christie <michaelc@cs.wisc.edu> Signed-off-by: James Bottomley <James.Bottomley@HansenPartnership.com>
Showing 3 changed files with 506 additions and 0 deletions Side-by-side Diff
drivers/scsi/device_handler/Kconfig
... | ... | @@ -24,4 +24,10 @@ |
24 | 24 | If you have a HP/COMPAQ MSA device that requires START_STOP to |
25 | 25 | be sent to start it and cannot upgrade the firmware then select y. |
26 | 26 | Otherwise, say N. |
27 | + | |
28 | +config SCSI_DH_EMC | |
29 | + tristate "EMC CLARiiON Device Handler" | |
30 | + depends on SCSI_DH | |
31 | + help | |
32 | + If you have a EMC CLARiiON select y. Otherwise, say N. |
drivers/scsi/device_handler/Makefile
drivers/scsi/device_handler/scsi_dh_emc.c
1 | +/* | |
2 | + * Target driver for EMC CLARiiON AX/CX-series hardware. | |
3 | + * Based on code from Lars Marowsky-Bree <lmb@suse.de> | |
4 | + * and Ed Goggin <egoggin@emc.com>. | |
5 | + * | |
6 | + * Copyright (C) 2006 Red Hat, Inc. All rights reserved. | |
7 | + * Copyright (C) 2006 Mike Christie | |
8 | + * | |
9 | + * This program is free software; you can redistribute it and/or modify | |
10 | + * it under the terms of the GNU General Public License as published by | |
11 | + * the Free Software Foundation; either version 2, or (at your option) | |
12 | + * any later version. | |
13 | + * | |
14 | + * This program is distributed in the hope that it will be useful, | |
15 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
16 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
17 | + * GNU General Public License for more details. | |
18 | + * | |
19 | + * You should have received a copy of the GNU General Public License | |
20 | + * along with this program; see the file COPYING. If not, write to | |
21 | + * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. | |
22 | + */ | |
23 | +#include <scsi/scsi.h> | |
24 | +#include <scsi/scsi_eh.h> | |
25 | +#include <scsi/scsi_dh.h> | |
26 | +#include <scsi/scsi_device.h> | |
27 | + | |
28 | +#define CLARIION_NAME "emc_clariion" | |
29 | + | |
30 | +#define CLARIION_TRESPASS_PAGE 0x22 | |
31 | +#define CLARIION_BUFFER_SIZE 0x80 | |
32 | +#define CLARIION_TIMEOUT (60 * HZ) | |
33 | +#define CLARIION_RETRIES 3 | |
34 | +#define CLARIION_UNBOUND_LU -1 | |
35 | + | |
36 | +static unsigned char long_trespass[] = { | |
37 | + 0, 0, 0, 0, | |
38 | + CLARIION_TRESPASS_PAGE, /* Page code */ | |
39 | + 0x09, /* Page length - 2 */ | |
40 | + 0x81, /* Trespass code + Honor reservation bit */ | |
41 | + 0xff, 0xff, /* Trespass target */ | |
42 | + 0, 0, 0, 0, 0, 0 /* Reserved bytes / unknown */ | |
43 | +}; | |
44 | + | |
45 | +static unsigned char long_trespass_hr[] = { | |
46 | + 0, 0, 0, 0, | |
47 | + CLARIION_TRESPASS_PAGE, /* Page code */ | |
48 | + 0x09, /* Page length - 2 */ | |
49 | + 0x01, /* Trespass code + Honor reservation bit */ | |
50 | + 0xff, 0xff, /* Trespass target */ | |
51 | + 0, 0, 0, 0, 0, 0 /* Reserved bytes / unknown */ | |
52 | +}; | |
53 | + | |
54 | +static unsigned char short_trespass[] = { | |
55 | + 0, 0, 0, 0, | |
56 | + CLARIION_TRESPASS_PAGE, /* Page code */ | |
57 | + 0x02, /* Page length - 2 */ | |
58 | + 0x81, /* Trespass code + Honor reservation bit */ | |
59 | + 0xff, /* Trespass target */ | |
60 | +}; | |
61 | + | |
62 | +static unsigned char short_trespass_hr[] = { | |
63 | + 0, 0, 0, 0, | |
64 | + CLARIION_TRESPASS_PAGE, /* Page code */ | |
65 | + 0x02, /* Page length - 2 */ | |
66 | + 0x01, /* Trespass code + Honor reservation bit */ | |
67 | + 0xff, /* Trespass target */ | |
68 | +}; | |
69 | + | |
70 | +struct clariion_dh_data { | |
71 | + /* | |
72 | + * Use short trespass command (FC-series) or the long version | |
73 | + * (default for AX/CX CLARiiON arrays). | |
74 | + */ | |
75 | + unsigned short_trespass; | |
76 | + /* | |
77 | + * Whether or not (default) to honor SCSI reservations when | |
78 | + * initiating a switch-over. | |
79 | + */ | |
80 | + unsigned hr; | |
81 | + /* I/O buffer for both MODE_SELECT and INQUIRY commands. */ | |
82 | + char buffer[CLARIION_BUFFER_SIZE]; | |
83 | + /* | |
84 | + * SCSI sense buffer for commands -- assumes serial issuance | |
85 | + * and completion sequence of all commands for same multipath. | |
86 | + */ | |
87 | + unsigned char sense[SCSI_SENSE_BUFFERSIZE]; | |
88 | + /* which SP (A=0,B=1,UNBOUND=-1) is dflt SP for path's mapped dev */ | |
89 | + int default_sp; | |
90 | + /* which SP (A=0,B=1,UNBOUND=-1) is active for path's mapped dev */ | |
91 | + int current_sp; | |
92 | +}; | |
93 | + | |
94 | +static inline struct clariion_dh_data | |
95 | + *get_clariion_data(struct scsi_device *sdev) | |
96 | +{ | |
97 | + struct scsi_dh_data *scsi_dh_data = sdev->scsi_dh_data; | |
98 | + BUG_ON(scsi_dh_data == NULL); | |
99 | + return ((struct clariion_dh_data *) scsi_dh_data->buf); | |
100 | +} | |
101 | + | |
102 | +/* | |
103 | + * Parse MODE_SELECT cmd reply. | |
104 | + */ | |
105 | +static int trespass_endio(struct scsi_device *sdev, int result) | |
106 | +{ | |
107 | + int err = SCSI_DH_OK; | |
108 | + struct scsi_sense_hdr sshdr; | |
109 | + struct clariion_dh_data *csdev = get_clariion_data(sdev); | |
110 | + char *sense = csdev->sense; | |
111 | + | |
112 | + if (status_byte(result) == CHECK_CONDITION && | |
113 | + scsi_normalize_sense(sense, SCSI_SENSE_BUFFERSIZE, &sshdr)) { | |
114 | + sdev_printk(KERN_ERR, sdev, "Found valid sense data 0x%2x, " | |
115 | + "0x%2x, 0x%2x while sending CLARiiON trespass " | |
116 | + "command.\n", sshdr.sense_key, sshdr.asc, | |
117 | + sshdr.ascq); | |
118 | + | |
119 | + if ((sshdr.sense_key == 0x05) && (sshdr.asc == 0x04) && | |
120 | + (sshdr.ascq == 0x00)) { | |
121 | + /* | |
122 | + * Array based copy in progress -- do not send | |
123 | + * mode_select or copy will be aborted mid-stream. | |
124 | + */ | |
125 | + sdev_printk(KERN_INFO, sdev, "Array Based Copy in " | |
126 | + "progress while sending CLARiiON trespass " | |
127 | + "command.\n"); | |
128 | + err = SCSI_DH_DEV_TEMP_BUSY; | |
129 | + } else if ((sshdr.sense_key == 0x02) && (sshdr.asc == 0x04) && | |
130 | + (sshdr.ascq == 0x03)) { | |
131 | + /* | |
132 | + * LUN Not Ready - Manual Intervention Required | |
133 | + * indicates in-progress ucode upgrade (NDU). | |
134 | + */ | |
135 | + sdev_printk(KERN_INFO, sdev, "Detected in-progress " | |
136 | + "ucode upgrade NDU operation while sending " | |
137 | + "CLARiiON trespass command.\n"); | |
138 | + err = SCSI_DH_DEV_TEMP_BUSY; | |
139 | + } else | |
140 | + err = SCSI_DH_DEV_FAILED; | |
141 | + } else if (result) { | |
142 | + sdev_printk(KERN_ERR, sdev, "Error 0x%x while sending " | |
143 | + "CLARiiON trespass command.\n", result); | |
144 | + err = SCSI_DH_IO; | |
145 | + } | |
146 | + | |
147 | + return err; | |
148 | +} | |
149 | + | |
150 | +static int parse_sp_info_reply(struct scsi_device *sdev, int result, | |
151 | + int *default_sp, int *current_sp, int *new_current_sp) | |
152 | +{ | |
153 | + int err = SCSI_DH_OK; | |
154 | + struct clariion_dh_data *csdev = get_clariion_data(sdev); | |
155 | + | |
156 | + if (result == 0) { | |
157 | + /* check for in-progress ucode upgrade (NDU) */ | |
158 | + if (csdev->buffer[48] != 0) { | |
159 | + sdev_printk(KERN_NOTICE, sdev, "Detected in-progress " | |
160 | + "ucode upgrade NDU operation while finding " | |
161 | + "current active SP."); | |
162 | + err = SCSI_DH_DEV_TEMP_BUSY; | |
163 | + } else { | |
164 | + *default_sp = csdev->buffer[5]; | |
165 | + | |
166 | + if (csdev->buffer[4] == 2) | |
167 | + /* SP for path is current */ | |
168 | + *current_sp = csdev->buffer[8]; | |
169 | + else { | |
170 | + if (csdev->buffer[4] == 1) | |
171 | + /* SP for this path is NOT current */ | |
172 | + if (csdev->buffer[8] == 0) | |
173 | + *current_sp = 1; | |
174 | + else | |
175 | + *current_sp = 0; | |
176 | + else | |
177 | + /* unbound LU or LUNZ */ | |
178 | + *current_sp = CLARIION_UNBOUND_LU; | |
179 | + } | |
180 | + *new_current_sp = csdev->buffer[8]; | |
181 | + } | |
182 | + } else { | |
183 | + struct scsi_sense_hdr sshdr; | |
184 | + | |
185 | + err = SCSI_DH_IO; | |
186 | + | |
187 | + if (scsi_normalize_sense(csdev->sense, SCSI_SENSE_BUFFERSIZE, | |
188 | + &sshdr)) | |
189 | + sdev_printk(KERN_ERR, sdev, "Found valid sense data " | |
190 | + "0x%2x, 0x%2x, 0x%2x while finding current " | |
191 | + "active SP.", sshdr.sense_key, sshdr.asc, | |
192 | + sshdr.ascq); | |
193 | + else | |
194 | + sdev_printk(KERN_ERR, sdev, "Error 0x%x finding " | |
195 | + "current active SP.", result); | |
196 | + } | |
197 | + | |
198 | + return err; | |
199 | +} | |
200 | + | |
201 | +static int sp_info_endio(struct scsi_device *sdev, int result, | |
202 | + int mode_select_sent, int *done) | |
203 | +{ | |
204 | + struct clariion_dh_data *csdev = get_clariion_data(sdev); | |
205 | + int err_flags, default_sp, current_sp, new_current_sp; | |
206 | + | |
207 | + err_flags = parse_sp_info_reply(sdev, result, &default_sp, | |
208 | + ¤t_sp, &new_current_sp); | |
209 | + | |
210 | + if (err_flags != SCSI_DH_OK) | |
211 | + goto done; | |
212 | + | |
213 | + if (mode_select_sent) { | |
214 | + csdev->default_sp = default_sp; | |
215 | + csdev->current_sp = current_sp; | |
216 | + } else { | |
217 | + /* | |
218 | + * Issue the actual module_selec request IFF either | |
219 | + * (1) we do not know the identity of the current SP OR | |
220 | + * (2) what we think we know is actually correct. | |
221 | + */ | |
222 | + if ((current_sp != CLARIION_UNBOUND_LU) && | |
223 | + (new_current_sp != current_sp)) { | |
224 | + | |
225 | + csdev->default_sp = default_sp; | |
226 | + csdev->current_sp = current_sp; | |
227 | + | |
228 | + sdev_printk(KERN_INFO, sdev, "Ignoring path group " | |
229 | + "switch-over command for CLARiiON SP%s since " | |
230 | + " mapped device is already initialized.", | |
231 | + current_sp ? "B" : "A"); | |
232 | + if (done) | |
233 | + *done = 1; /* as good as doing it */ | |
234 | + } | |
235 | + } | |
236 | +done: | |
237 | + return err_flags; | |
238 | +} | |
239 | + | |
240 | +/* | |
241 | +* Get block request for REQ_BLOCK_PC command issued to path. Currently | |
242 | +* limited to MODE_SELECT (trespass) and INQUIRY (VPD page 0xC0) commands. | |
243 | +* | |
244 | +* Uses data and sense buffers in hardware handler context structure and | |
245 | +* assumes serial servicing of commands, both issuance and completion. | |
246 | +*/ | |
247 | +static struct request *get_req(struct scsi_device *sdev, int cmd) | |
248 | +{ | |
249 | + struct clariion_dh_data *csdev = get_clariion_data(sdev); | |
250 | + struct request *rq; | |
251 | + unsigned char *page22; | |
252 | + int len = 0; | |
253 | + | |
254 | + rq = blk_get_request(sdev->request_queue, | |
255 | + (cmd == MODE_SELECT) ? WRITE : READ, GFP_ATOMIC); | |
256 | + if (!rq) { | |
257 | + sdev_printk(KERN_INFO, sdev, "get_req: blk_get_request failed"); | |
258 | + return NULL; | |
259 | + } | |
260 | + | |
261 | + memset(&rq->cmd, 0, BLK_MAX_CDB); | |
262 | + rq->cmd[0] = cmd; | |
263 | + rq->cmd_len = COMMAND_SIZE(rq->cmd[0]); | |
264 | + | |
265 | + switch (cmd) { | |
266 | + case MODE_SELECT: | |
267 | + if (csdev->short_trespass) { | |
268 | + page22 = csdev->hr ? short_trespass_hr : short_trespass; | |
269 | + len = sizeof(short_trespass); | |
270 | + } else { | |
271 | + page22 = csdev->hr ? long_trespass_hr : long_trespass; | |
272 | + len = sizeof(long_trespass); | |
273 | + } | |
274 | + /* | |
275 | + * Can't DMA from kernel BSS -- must copy selected trespass | |
276 | + * command mode page contents to context buffer which is | |
277 | + * allocated by kmalloc. | |
278 | + */ | |
279 | + BUG_ON((len > CLARIION_BUFFER_SIZE)); | |
280 | + memcpy(csdev->buffer, page22, len); | |
281 | + rq->cmd_flags |= REQ_RW; | |
282 | + rq->cmd[1] = 0x10; | |
283 | + break; | |
284 | + case INQUIRY: | |
285 | + rq->cmd[1] = 0x1; | |
286 | + rq->cmd[2] = 0xC0; | |
287 | + len = CLARIION_BUFFER_SIZE; | |
288 | + memset(csdev->buffer, 0, CLARIION_BUFFER_SIZE); | |
289 | + break; | |
290 | + default: | |
291 | + BUG_ON(1); | |
292 | + break; | |
293 | + } | |
294 | + | |
295 | + rq->cmd[4] = len; | |
296 | + rq->cmd_type = REQ_TYPE_BLOCK_PC; | |
297 | + rq->cmd_flags |= REQ_FAILFAST; | |
298 | + rq->timeout = CLARIION_TIMEOUT; | |
299 | + rq->retries = CLARIION_RETRIES; | |
300 | + | |
301 | + rq->sense = csdev->sense; | |
302 | + memset(rq->sense, 0, SCSI_SENSE_BUFFERSIZE); | |
303 | + rq->sense_len = 0; | |
304 | + | |
305 | + if (blk_rq_map_kern(sdev->request_queue, rq, csdev->buffer, | |
306 | + len, GFP_ATOMIC)) { | |
307 | + __blk_put_request(rq->q, rq); | |
308 | + return NULL; | |
309 | + } | |
310 | + | |
311 | + return rq; | |
312 | +} | |
313 | + | |
314 | +static int send_cmd(struct scsi_device *sdev, int cmd) | |
315 | +{ | |
316 | + struct request *rq = get_req(sdev, cmd); | |
317 | + | |
318 | + if (!rq) | |
319 | + return SCSI_DH_RES_TEMP_UNAVAIL; | |
320 | + | |
321 | + return blk_execute_rq(sdev->request_queue, NULL, rq, 1); | |
322 | +} | |
323 | + | |
324 | +static int clariion_activate(struct scsi_device *sdev) | |
325 | +{ | |
326 | + int result, done = 0; | |
327 | + | |
328 | + result = send_cmd(sdev, INQUIRY); | |
329 | + result = sp_info_endio(sdev, result, 0, &done); | |
330 | + if (result || done) | |
331 | + goto done; | |
332 | + | |
333 | + result = send_cmd(sdev, MODE_SELECT); | |
334 | + result = trespass_endio(sdev, result); | |
335 | + if (result) | |
336 | + goto done; | |
337 | + | |
338 | + result = send_cmd(sdev, INQUIRY); | |
339 | + result = sp_info_endio(sdev, result, 1, NULL); | |
340 | +done: | |
341 | + return result; | |
342 | +} | |
343 | + | |
344 | +static int clariion_check_sense(struct scsi_device *sdev, | |
345 | + struct scsi_sense_hdr *sense_hdr) | |
346 | +{ | |
347 | + switch (sense_hdr->sense_key) { | |
348 | + case NOT_READY: | |
349 | + if (sense_hdr->asc == 0x04 && sense_hdr->ascq == 0x03) | |
350 | + /* | |
351 | + * LUN Not Ready - Manual Intervention Required | |
352 | + * indicates this is a passive path. | |
353 | + * | |
354 | + * FIXME: However, if this is seen and EVPD C0 | |
355 | + * indicates that this is due to a NDU in | |
356 | + * progress, we should set FAIL_PATH too. | |
357 | + * This indicates we might have to do a SCSI | |
358 | + * inquiry in the end_io path. Ugh. | |
359 | + * | |
360 | + * Can return FAILED only when we want the error | |
361 | + * recovery process to kick in. | |
362 | + */ | |
363 | + return SUCCESS; | |
364 | + break; | |
365 | + case ILLEGAL_REQUEST: | |
366 | + if (sense_hdr->asc == 0x25 && sense_hdr->ascq == 0x01) | |
367 | + /* | |
368 | + * An array based copy is in progress. Do not | |
369 | + * fail the path, do not bypass to another PG, | |
370 | + * do not retry. Fail the IO immediately. | |
371 | + * (Actually this is the same conclusion as in | |
372 | + * the default handler, but lets make sure.) | |
373 | + * | |
374 | + * Can return FAILED only when we want the error | |
375 | + * recovery process to kick in. | |
376 | + */ | |
377 | + return SUCCESS; | |
378 | + break; | |
379 | + case UNIT_ATTENTION: | |
380 | + if (sense_hdr->asc == 0x29 && sense_hdr->ascq == 0x00) | |
381 | + /* | |
382 | + * Unit Attention Code. This is the first IO | |
383 | + * to the new path, so just retry. | |
384 | + */ | |
385 | + return NEEDS_RETRY; | |
386 | + break; | |
387 | + } | |
388 | + | |
389 | + /* success just means we do not care what scsi-ml does */ | |
390 | + return SUCCESS; | |
391 | +} | |
392 | + | |
393 | +static const struct { | |
394 | + char *vendor; | |
395 | + char *model; | |
396 | +} clariion_dev_list[] = { | |
397 | + {"DGC", "RAID"}, | |
398 | + {"DGC", "DISK"}, | |
399 | + {NULL, NULL}, | |
400 | +}; | |
401 | + | |
402 | +static int clariion_bus_notify(struct notifier_block *, unsigned long, void *); | |
403 | + | |
404 | +static struct scsi_device_handler clariion_dh = { | |
405 | + .name = CLARIION_NAME, | |
406 | + .module = THIS_MODULE, | |
407 | + .nb.notifier_call = clariion_bus_notify, | |
408 | + .check_sense = clariion_check_sense, | |
409 | + .activate = clariion_activate, | |
410 | +}; | |
411 | + | |
412 | +/* | |
413 | + * TODO: need some interface so we can set trespass values | |
414 | + */ | |
415 | +static int clariion_bus_notify(struct notifier_block *nb, | |
416 | + unsigned long action, void *data) | |
417 | +{ | |
418 | + struct device *dev = data; | |
419 | + struct scsi_device *sdev = to_scsi_device(dev); | |
420 | + struct scsi_dh_data *scsi_dh_data; | |
421 | + struct clariion_dh_data *h; | |
422 | + int i, found = 0; | |
423 | + unsigned long flags; | |
424 | + | |
425 | + if (action == BUS_NOTIFY_ADD_DEVICE) { | |
426 | + for (i = 0; clariion_dev_list[i].vendor; i++) { | |
427 | + if (!strncmp(sdev->vendor, clariion_dev_list[i].vendor, | |
428 | + strlen(clariion_dev_list[i].vendor)) && | |
429 | + !strncmp(sdev->model, clariion_dev_list[i].model, | |
430 | + strlen(clariion_dev_list[i].model))) { | |
431 | + found = 1; | |
432 | + break; | |
433 | + } | |
434 | + } | |
435 | + if (!found) | |
436 | + goto out; | |
437 | + | |
438 | + scsi_dh_data = kzalloc(sizeof(struct scsi_device_handler *) | |
439 | + + sizeof(*h) , GFP_KERNEL); | |
440 | + if (!scsi_dh_data) { | |
441 | + sdev_printk(KERN_ERR, sdev, "Attach failed %s.\n", | |
442 | + CLARIION_NAME); | |
443 | + goto out; | |
444 | + } | |
445 | + | |
446 | + scsi_dh_data->scsi_dh = &clariion_dh; | |
447 | + h = (struct clariion_dh_data *) scsi_dh_data->buf; | |
448 | + h->default_sp = CLARIION_UNBOUND_LU; | |
449 | + h->current_sp = CLARIION_UNBOUND_LU; | |
450 | + | |
451 | + spin_lock_irqsave(sdev->request_queue->queue_lock, flags); | |
452 | + sdev->scsi_dh_data = scsi_dh_data; | |
453 | + spin_unlock_irqrestore(sdev->request_queue->queue_lock, flags); | |
454 | + | |
455 | + sdev_printk(KERN_NOTICE, sdev, "Attached %s.\n", CLARIION_NAME); | |
456 | + try_module_get(THIS_MODULE); | |
457 | + | |
458 | + } else if (action == BUS_NOTIFY_DEL_DEVICE) { | |
459 | + if (sdev->scsi_dh_data == NULL || | |
460 | + sdev->scsi_dh_data->scsi_dh != &clariion_dh) | |
461 | + goto out; | |
462 | + | |
463 | + spin_lock_irqsave(sdev->request_queue->queue_lock, flags); | |
464 | + scsi_dh_data = sdev->scsi_dh_data; | |
465 | + sdev->scsi_dh_data = NULL; | |
466 | + spin_unlock_irqrestore(sdev->request_queue->queue_lock, flags); | |
467 | + | |
468 | + sdev_printk(KERN_NOTICE, sdev, "Dettached %s.\n", | |
469 | + CLARIION_NAME); | |
470 | + | |
471 | + kfree(scsi_dh_data); | |
472 | + module_put(THIS_MODULE); | |
473 | + } | |
474 | + | |
475 | +out: | |
476 | + return 0; | |
477 | +} | |
478 | + | |
479 | +static int __init clariion_init(void) | |
480 | +{ | |
481 | + int r; | |
482 | + | |
483 | + r = scsi_register_device_handler(&clariion_dh); | |
484 | + if (r != 0) | |
485 | + printk(KERN_ERR "Failed to register scsi device handler."); | |
486 | + return r; | |
487 | +} | |
488 | + | |
489 | +static void __exit clariion_exit(void) | |
490 | +{ | |
491 | + scsi_unregister_device_handler(&clariion_dh); | |
492 | +} | |
493 | + | |
494 | +module_init(clariion_init); | |
495 | +module_exit(clariion_exit); | |
496 | + | |
497 | +MODULE_DESCRIPTION("EMC CX/AX/FC-family driver"); | |
498 | +MODULE_AUTHOR("Mike Christie <michaelc@cs.wisc.edu>, Chandra Seetharaman <sekharan@us.ibm.com>"); | |
499 | +MODULE_LICENSE("GPL"); |