xref: /illumos-gate/usr/src/cmd/nvmeadm/nvmeadm_wdc.c (revision e00bdde3c6d406f40f53f3025defadc22f7ec31a)
1 /*
2  * This file and its contents are supplied under the terms of the
3  * Common Development and Distribution License ("CDDL"), version 1.0.
4  * You may only use this file in accordance with the terms of version
5  * 1.0 of the CDDL.
6  *
7  * A full copy of the text of the CDDL should have accompanied this
8  * source.  A copy of the CDDL is also available via the Internet at
9  * http://www.illumos.org/license/CDDL.
10  */
11 
12 /*
13  * Copyright 2024 Oxide Computer Company
14  */
15 
16 /*
17  * WDC vendor-specific commands
18  */
19 
20 #include <err.h>
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <fcntl.h>
24 #include <unistd.h>
25 #include <sys/sysmacros.h>
26 #include <stdbool.h>
27 #include <endian.h>
28 #include <sys/nvme/wdc.h>
29 
30 #include "nvmeadm.h"
31 
32 /*
33  * This is the default chunk size that we'll read the e6 log in. This generally
34  * should fit within the maximum transfer size for a device. If we wanted to
35  * improve this, we could expose what the kernel's maximum transfer size is for
36  * a device and then use that as a larger upper bound. Currently the value is 64
37  * KiB.
38  */
39 #define	E6_BUFSIZE	0x10000
40 
41 typedef struct nvmeadm_e6_dump {
42 	const char *e6_output;
43 } nvmeadm_e6_dump_t;
44 
45 typedef struct nvmeadm_wdc_resize {
46 	bool wr_query;
47 	uint32_t wr_set;
48 } nvmeadm_wdc_resize_t;
49 
50 void
51 usage_wdc_e6dump(const char *c_name)
52 {
53 	(void) fprintf(stderr, "%s -o output <ctl>\n\n"
54 	    "  Dump WDC e6 diagnostic log from a device.\n", c_name);
55 }
56 
57 void
58 optparse_wdc_e6dump(nvme_process_arg_t *npa)
59 {
60 	int c;
61 	nvmeadm_e6_dump_t *e6;
62 
63 	if ((e6 = calloc(1, sizeof (nvmeadm_e6_dump_t))) == NULL) {
64 		err(-1, "failed to allocate memory for e6 options structure");
65 	}
66 
67 	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":o:")) != -1) {
68 		switch (c) {
69 		case 'o':
70 			e6->e6_output = optarg;
71 			break;
72 		case '?':
73 			errx(-1, "unknown option: -%c", optopt);
74 		case ':':
75 			errx(-1, "option -%c requires an argument", optopt);
76 		}
77 	}
78 
79 	if (e6->e6_output == NULL) {
80 		errx(-1, "missing required e6dump output file, specify with "
81 		    "-o");
82 	}
83 
84 	npa->npa_cmd_arg = e6;
85 }
86 
87 static void
88 wdc_e6_read(const nvme_process_arg_t *npa, nvme_wdc_e6_req_t *req,
89     uint64_t off, void *buf, size_t len)
90 {
91 	if (!nvme_wdc_e6_req_set_offset(req, off)) {
92 		nvmeadm_fatal(npa, "failed to set e6 request offset to 0x%"
93 		    PRIx64, off);
94 	}
95 
96 	if (!nvme_wdc_e6_req_set_output(req, buf, len)) {
97 		nvmeadm_fatal(npa, "failed to set e6 request output buffer");
98 	}
99 
100 	if (!nvme_wdc_e6_req_exec(req)) {
101 		nvmeadm_fatal(npa, "failed to issue e6 request for %zu bytes "
102 		    "at offset 0x%" PRIx64, len, off);
103 	}
104 }
105 
106 /*
107  * Write out e6 data to a file. Because our read from the device has already
108  * been constrained by size, we don't bother further chunking up the write out
109  * to a file.
110  */
111 static void
112 wdc_e6_write(int fd, const void *buf, size_t len)
113 {
114 	size_t off = 0;
115 
116 	while (len > 0) {
117 		void *boff = (void *)((uintptr_t)buf + off);
118 		ssize_t ret = write(fd, boff, len);
119 		if (ret < 0) {
120 			/*
121 			 * We explicitly allow a signal that interrupts us to
122 			 * lead to a failure assuming someone has more likely
123 			 * than not issued a SIGINT or similar.
124 			 */
125 			err(-1, "failed to write e6 data to output file");
126 		}
127 
128 		len -= (size_t)ret;
129 		off += (size_t)ret;
130 	}
131 }
132 
133 int
134 do_wdc_e6dump(const nvme_process_arg_t *npa)
135 {
136 	int ofd;
137 	nvmeadm_e6_dump_t *e6 = npa->npa_cmd_arg;
138 	nvme_vuc_disc_t *vuc;
139 	void *buf;
140 	nvme_wdc_e6_req_t *req;
141 	const wdc_e6_header_t *header;
142 	uint64_t len, off;
143 
144 	vuc = nvmeadm_vuc_init(npa, npa->npa_cmd->c_name);
145 
146 	ofd = open(e6->e6_output, O_RDWR | O_CREAT | O_TRUNC, 0644);
147 	if (ofd < 0) {
148 		err(-1, "failed to open file %s", e6->e6_output);
149 	}
150 
151 	if ((buf = calloc(1, E6_BUFSIZE)) == NULL) {
152 		err(-1, "failed to allocate 0x%x bytes for E6 transfer buffer",
153 		    E6_BUFSIZE);
154 	}
155 
156 	if (!nvme_wdc_e6_req_init(npa->npa_ctrl, &req)) {
157 		nvmeadm_fatal(npa, "failed to initialize e6 request");
158 	}
159 
160 	/*
161 	 * Begin by reading the header to determine the actual size. Note, as
162 	 * far as we can tell, the size of the header is included in the size we
163 	 * get.
164 	 */
165 	wdc_e6_read(npa, req, 0, buf, sizeof (wdc_e6_header_t));
166 	header = buf;
167 	len = be32toh(header->e6_size_be);
168 
169 	if (len == UINT32_MAX) {
170 		errx(-1, "e6 header size 0x%" PRIx64 " looks like an invalid "
171 		    "PCI read, aborting", len);
172 	}
173 
174 	if ((len % 4) != 0) {
175 		warnx("e6 header size 0x%zx is not 4 byte aligned, but "
176 		    "firmware claims it always will be, rounding up", len);
177 		len = P2ROUNDUP(len, 4);
178 	}
179 
180 	if (len < sizeof (wdc_e6_header_t)) {
181 		errx(-1, "e6 header size is too small, 0x%zx bytes does not "
182 		    "even cover the header", len);
183 	}
184 	wdc_e6_write(ofd, buf, sizeof (wdc_e6_header_t));
185 
186 	/*
187 	 * Account for the fact that we already read the header.
188 	 */
189 	off = sizeof (wdc_e6_header_t);
190 	len -= off;
191 	while (len > 0) {
192 		uint32_t toread = MIN(len, E6_BUFSIZE);
193 		wdc_e6_read(npa, req, off, buf, toread);
194 		wdc_e6_write(ofd, buf, toread);
195 
196 		off += toread;
197 		len -= toread;
198 	}
199 
200 	nvme_wdc_e6_req_fini(req);
201 	VERIFY0(close(ofd));
202 	nvmeadm_vuc_fini(npa, vuc);
203 
204 	return (0);
205 }
206 
207 void
208 usage_wdc_resize(const char *c_name)
209 {
210 	(void) fprintf(stderr, "%s -s size | -g <ctl>\n\n"
211 	    "  Resize a device to a new overall capacity in GB (not GiB) or "
212 	    "get its\n  current size. Resizing will cause all data and "
213 	    "namespaces to be lost.\n",
214 	    c_name);
215 }
216 
217 void
218 optparse_wdc_resize(nvme_process_arg_t *npa)
219 {
220 	int c;
221 	nvmeadm_wdc_resize_t *resize;
222 
223 	if ((resize = calloc(1, sizeof (nvmeadm_wdc_resize_t))) == NULL) {
224 		err(-1, "failed to allocate memory for resize options "
225 		    "structure");
226 	}
227 
228 	while ((c = getopt(npa->npa_argc, npa->npa_argv, ":gs:")) != -1) {
229 		const char *err;
230 
231 		switch (c) {
232 		case 'g':
233 			resize->wr_query = true;
234 			break;
235 		case 's':
236 			/*
237 			 * The size to set is in GB (not GiB). While WDC
238 			 * recommends specific size points depending on the
239 			 * drives initial capacity, we allow the user to set
240 			 * what they expect and will allow the command to
241 			 * succeed or fail as per the controller's whims. It
242 			 * would be better if we looked at the device and
243 			 * determined its underlying capacity and figured out
244 			 * what points made sense, but it's not clear on the
245 			 * best way to do that across a few different
246 			 * generations of WDC products.
247 			 */
248 			resize->wr_set = (uint32_t)strtonumx(optarg, 1,
249 			    UINT16_MAX, &err, 0);
250 			if (err != NULL) {
251 				errx(-1, "failed to parse resize size %s:"
252 				    "value is %s", optarg, err);
253 			}
254 			break;
255 		case '?':
256 			errx(-1, "unknown option: -%c", optopt);
257 		case ':':
258 			errx(-1, "option -%c requires an argument", optopt);
259 		}
260 	}
261 
262 	if (resize->wr_query && resize->wr_set != 0) {
263 		errx(-1, "only one of -g and -s may be specified");
264 	}
265 
266 	if (!resize->wr_query && resize->wr_set == 0) {
267 		errx(-1, "one of -g and -s must be specified");
268 	}
269 
270 	npa->npa_cmd_arg = resize;
271 }
272 
273 int
274 do_wdc_resize(const nvme_process_arg_t *npa)
275 {
276 	nvmeadm_wdc_resize_t *resize = npa->npa_cmd_arg;
277 	nvme_vuc_disc_t *vuc;
278 
279 	vuc = nvmeadm_vuc_init(npa, npa->npa_cmd->c_name);
280 
281 	/*
282 	 * The VUC for this generally recommends exclusive access. If this
283 	 * becomes problematic for folks issuing this query, then we should
284 	 * break the query into a separate VUC entry that we should discover
285 	 * instead.
286 	 */
287 	if (resize->wr_query) {
288 		uint32_t val;
289 
290 		if (!nvme_wdc_resize_get(npa->npa_ctrl, &val)) {
291 			nvmeadm_fatal(npa, "failed to query current WDC "
292 			    "device capacity");
293 		}
294 
295 		(void) printf("%u\n", val);
296 		nvmeadm_vuc_fini(npa, vuc);
297 		return (0);
298 	}
299 
300 	if (!nvme_wdc_resize_set(npa->npa_ctrl, resize->wr_set)) {
301 		nvmeadm_fatal(npa, "failed to resize device to %u",
302 		    resize->wr_set);
303 	}
304 
305 	(void) printf("%s resized to %u GB\n", npa->npa_name, resize->wr_set);
306 	nvmeadm_vuc_fini(npa, vuc);
307 
308 	return (0);
309 }
310