Skip to content

Commit da24991

Browse files
committed
Add more type annotations
Signed-off-by: Gabor Igloi <[email protected]>
1 parent 4791466 commit da24991

File tree

1 file changed

+188
-163
lines changed

1 file changed

+188
-163
lines changed

main.ml

Lines changed: 188 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,101 @@ let pvs_version = "3.0"
108108
let supported_api_versions = [pvs_version; "5.0"]
109109
let api_max = List.fold_left ~f:max supported_api_versions ~init:""
110110

111+
let id = fun x -> x
112+
113+
type compat_in = R.t -> R.t
114+
(** A function that changes the input to make it compatible with an older
115+
script *)
116+
117+
type compat_out = R.t -> R.t
118+
(** A function that changes the output of an older script to make it
119+
compatible with the new interface and ensure it is unmarshalled without
120+
error. *)
121+
122+
module Compat(V : sig val version : string option ref end) : sig
123+
(** Module for making the inputs and outputs compatible with the old PVS
124+
version of the storage scripts. *)
125+
126+
type device_config = (Core.String.t, string) Core.List.Assoc.t
127+
128+
val compat_out_volume : compat_out
129+
(** Add the missing [sharable] field to the Dict in [rpc], to ensure the
130+
volume in the output match the new volume record type and is successfully
131+
parsed by rpclib. *)
132+
133+
val compat_out_volumes : compat_out
134+
(** Add the missing [sharable] field to the Dicts in [rpc], to ensure the
135+
volumes in the output match the new volume record type and are
136+
successfully parsed by rpclib. *)
137+
138+
val compat_uri : device_config -> (compat_in, Storage_interface.Exception.exnty) Deferred.Result.t
139+
(** For the old PVS version, adds the uri parameter to the call from
140+
device_config, for newer versions, removes the uri key from device_config *)
141+
142+
val sr_create : device_config -> (device_config * compat_in * compat_out, Storage_interface.Exception.exnty) Deferred.Result.t
143+
(** Compatiblity for the old PVS version of SR.create, which had signature
144+
[uri -> name -> desc -> config -> unit] *)
145+
end = struct
146+
147+
type device_config = (Core.String.t, string) Core.List.Assoc.t
148+
type compat_in = R.t -> R.t
149+
type compat_out = R.t -> R.t
150+
151+
let remove field rpc =
152+
match !V.version, rpc with
153+
| Some v, R.Dict d when v = pvs_version ->
154+
R.Dict (List.filter ~f:(fun (k,_) -> k <> field) d)
155+
| _ -> rpc
156+
157+
let with_pvs_version f rpc = if !V.version = Some pvs_version then f rpc else rpc
158+
159+
let add_param_to_input params =
160+
with_pvs_version (function
161+
(* Currently all parameters must be named. In this case, rpclib
162+
currently puts them into a Dict. *)
163+
| R.Dict d -> R.Dict (List.rev_append params d)
164+
| rpc -> rpc)
165+
166+
let add_fields_to_dict fields =
167+
function R.Dict d -> R.Dict (List.rev_append fields d) | rpc -> rpc
168+
169+
let add_fields_to_record_output fields =
170+
with_pvs_version (function
171+
| R.Dict _ as d -> add_fields_to_dict fields d
172+
| rpc -> rpc)
173+
174+
let add_fields_to_record_list_output fields =
175+
with_pvs_version (function
176+
| R.Enum l -> R.Enum (List.map ~f:(add_fields_to_dict fields) l)
177+
| rpc -> rpc)
178+
179+
let compat_out_volume =
180+
add_fields_to_record_output ["sharable", R.Bool false]
181+
182+
let compat_out_volumes =
183+
add_fields_to_record_list_output ["sharable", R.Bool false]
184+
185+
let compat_uri device_config =
186+
if !V.version = Some pvs_version then
187+
match List.Assoc.find ~equal:String.equal device_config "uri" with
188+
| None ->
189+
return (Error (missing_uri ()))
190+
| Some uri ->
191+
return (Ok (add_param_to_input ["uri", R.String uri]))
192+
else
193+
return (Ok id)
194+
195+
let sr_create device_config =
196+
compat_uri device_config >>>= fun compat_in ->
197+
let compat_out rpc =
198+
(* The PVS version will return nothing *)
199+
if rpc = R.Null then
200+
Rpcmarshal.marshal Xapi_storage.Control.typ_of_configuration device_config
201+
else rpc
202+
in
203+
return (Ok (device_config, compat_in, compat_out))
204+
end
205+
111206
let check_plugin_version_compatible query_result =
112207
let Xapi_storage.Plugin.{ name; required_api_version; _ } = query_result in
113208
if required_api_version <> api_max then
@@ -197,8 +292,6 @@ module Script = struct
197292
find ()
198293
end
199294

200-
let id = fun x -> x
201-
202295
(** Call the script named after the RPC method in the [script_dir]
203296
directory. The arguments (not the whole JSON-RPC call) are passed as JSON
204297
to its stdin, and stdout is returned. In case of a non-zero exit code,
@@ -212,101 +305,102 @@ let id = fun x -> x
212305
output.
213306
This function either returns a successful RPC response, or raises
214307
Fork_exec_error with a suitable SMAPIv2 error if the call failed. *)
215-
let fork_exec_rpc ~script_dir ?missing ?(compat_in=id) ?(compat_out=id) =
216-
let invoke_script call script_name =
217-
Process.create ~prog:script_name ~args:["--json"] ()
218-
>>= function
219-
| Error e ->
220-
error "%s failed: %s" script_name (Error.to_string_hum e);
221-
return (Error(backend_error "SCRIPT_FAILED" [ script_name; Error.to_string_hum e ]))
222-
| Ok p ->
223-
(* Send the request as json on stdin *)
224-
let w = Process.stdin p in
225-
(* We pass just the args, not the complete JSON-RPC call.
226-
Currently the Python code generated by rpclib requires all params to
227-
be named - they will be converted into a name->value Python dict.
228-
Rpclib currently puts all named params into a dict, so we expect
229-
params to be a single Dict, if all the params are named. *)
230-
(match call.R.params with
231-
| [R.Dict _ as d] ->
232-
return (Ok d)
233-
| _ -> return (Error (backend_error "INCORRECT_PARAMETERS" [ script_name; "All the call parameters should be named and should be in a RPC Dict" ]))
234-
) >>>= fun args ->
235-
let args = compat_in args in
236-
Writer.write w (Jsonrpc.to_string args);
237-
Writer.close w
238-
>>= fun () ->
239-
Process.collect_output_and_wait p
240-
>>= fun output ->
241-
begin match output.Process.Output.exit_status with
242-
| Error (`Exit_non_zero code) ->
243-
(* Expect an exception and backtrace on stdout *)
244-
begin match Or_error.try_with (fun () -> Jsonrpc.of_string output.Process.Output.stdout) with
245-
| Error _ ->
246-
error "%s failed and printed bad error json: %s" script_name output.Process.Output.stdout;
247-
error "%s failed, stderr: %s" script_name output.Process.Output.stderr;
248-
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "non-zero exit and bad json on stdout"; string_of_int code; output.Process.Output.stdout; output.Process.Output.stdout ]))
249-
| Ok response ->
250-
begin match Or_error.try_with (fun () -> error_of_rpc response) with
251-
| Error _ ->
252-
error "%s failed and printed bad error json: %s" script_name output.Process.Output.stdout;
253-
error "%s failed, stderr: %s" script_name output.Process.Output.stderr;
254-
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "non-zero exit and bad json on stdout"; string_of_int code; output.Process.Output.stdout; output.Process.Output.stdout ]))
255-
| Ok x -> return (Error(backend_backtrace_error x.code x.params x.backtrace))
256-
end
257-
end
258-
| Error (`Signal signal) ->
259-
error "%s caught a signal and failed" script_name;
260-
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "signalled"; Signal.to_string signal; output.Process.Output.stdout; output.Process.Output.stdout ]))
261-
| Ok () ->
262-
263-
(* Parse the json on stdout. We get back a JSON-RPC
264-
value from the scripts, not a complete JSON-RPC response *)
265-
begin match Or_error.try_with (fun () -> Jsonrpc.of_string output.Process.Output.stdout) with
266-
| Error _ ->
267-
error "%s succeeded but printed bad json: %s" script_name output.Process.Output.stdout;
268-
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "bad json on stdout"; output.Process.Output.stdout ]))
269-
| Ok response ->
270-
info "%s succeeded: %s" script_name output.Process.Output.stdout;
271-
let response = compat_out response in
272-
let response = R.success response in
273-
return (Ok response)
274-
end
275-
end
276-
in
308+
let fork_exec_rpc : script_dir:string -> ?missing:R.t -> ?compat_in:compat_in -> ?compat_out:compat_out -> R.call -> R.response Deferred.t =
309+
fun ~script_dir ?missing ?(compat_in=id) ?(compat_out=id) ->
310+
let invoke_script call script_name : (R.response, Storage_interface.Exception.exnty) Deferred.Result.t =
311+
Process.create ~prog:script_name ~args:["--json"] ()
312+
>>= function
313+
| Error e ->
314+
error "%s failed: %s" script_name (Error.to_string_hum e);
315+
return (Error(backend_error "SCRIPT_FAILED" [ script_name; Error.to_string_hum e ]))
316+
| Ok p ->
317+
(* Send the request as json on stdin *)
318+
let w = Process.stdin p in
319+
(* We pass just the args, not the complete JSON-RPC call.
320+
Currently the Python code generated by rpclib requires all params to
321+
be named - they will be converted into a name->value Python dict.
322+
Rpclib currently puts all named params into a dict, so we expect
323+
params to be a single Dict, if all the params are named. *)
324+
(match call.R.params with
325+
| [R.Dict _ as d] ->
326+
return (Ok d)
327+
| _ -> return (Error (backend_error "INCORRECT_PARAMETERS" [ script_name; "All the call parameters should be named and should be in a RPC Dict" ]))
328+
) >>>= fun args ->
329+
let args = compat_in args in
330+
Writer.write w (Jsonrpc.to_string args);
331+
Writer.close w
332+
>>= fun () ->
333+
Process.collect_output_and_wait p
334+
>>= fun output ->
335+
begin match output.Process.Output.exit_status with
336+
| Error (`Exit_non_zero code) ->
337+
(* Expect an exception and backtrace on stdout *)
338+
begin match Or_error.try_with (fun () -> Jsonrpc.of_string output.Process.Output.stdout) with
339+
| Error _ ->
340+
error "%s failed and printed bad error json: %s" script_name output.Process.Output.stdout;
341+
error "%s failed, stderr: %s" script_name output.Process.Output.stderr;
342+
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "non-zero exit and bad json on stdout"; string_of_int code; output.Process.Output.stdout; output.Process.Output.stdout ]))
343+
| Ok response ->
344+
begin match Or_error.try_with (fun () -> error_of_rpc response) with
345+
| Error _ ->
346+
error "%s failed and printed bad error json: %s" script_name output.Process.Output.stdout;
347+
error "%s failed, stderr: %s" script_name output.Process.Output.stderr;
348+
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "non-zero exit and bad json on stdout"; string_of_int code; output.Process.Output.stdout; output.Process.Output.stdout ]))
349+
| Ok x -> return (Error(backend_backtrace_error x.code x.params x.backtrace))
350+
end
351+
end
352+
| Error (`Signal signal) ->
353+
error "%s caught a signal and failed" script_name;
354+
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "signalled"; Signal.to_string signal; output.Process.Output.stdout; output.Process.Output.stdout ]))
355+
| Ok () ->
356+
357+
(* Parse the json on stdout. We get back a JSON-RPC
358+
value from the scripts, not a complete JSON-RPC response *)
359+
begin match Or_error.try_with (fun () -> Jsonrpc.of_string output.Process.Output.stdout) with
360+
| Error _ ->
361+
error "%s succeeded but printed bad json: %s" script_name output.Process.Output.stdout;
362+
return (Error (backend_error "SCRIPT_FAILED" [ script_name; "bad json on stdout"; output.Process.Output.stdout ]))
363+
| Ok response ->
364+
info "%s succeeded: %s" script_name output.Process.Output.stdout;
365+
let response = compat_out response in
366+
let response = R.success response in
367+
return (Ok response)
368+
end
369+
end
370+
in
277371

278-
let script_rpc call =
279-
info "%s" (Jsonrpc.string_of_call call);
280-
Script.path ~script_dir ~script_name:call.R.name >>= function
281-
| Error (`missing path) ->
282-
error "%s is not a file" path;
283-
(match missing with
284-
| None ->
285-
return (Error(backend_error "SCRIPT_MISSING" [ path; "Check whether the file exists and has correct permissions" ]))
286-
| Some m ->
287-
warn "Deprecated: script '%s' is missing, treating as no-op. Update your plugin!" path;
288-
return (Ok (R.success m)))
289-
| Error (`not_executable (path, exn)) ->
290-
error "%s is not executable" path;
291-
return (Error (backend_error "SCRIPT_NOT_EXECUTABLE" [ path; Exn.to_string exn ]))
292-
| Ok path -> invoke_script call path
293-
in
372+
let script_rpc call : (R.response, Storage_interface.Exception.exnty) Deferred.Result.t =
373+
info "%s" (Jsonrpc.string_of_call call);
374+
Script.path ~script_dir ~script_name:call.R.name >>= function
375+
| Error (`missing path) ->
376+
error "%s is not a file" path;
377+
(match missing with
378+
| None ->
379+
return (Error(backend_error "SCRIPT_MISSING" [ path; "Check whether the file exists and has correct permissions" ]))
380+
| Some m ->
381+
warn "Deprecated: script '%s' is missing, treating as no-op. Update your plugin!" path;
382+
return (Ok (R.success m)))
383+
| Error (`not_executable (path, exn)) ->
384+
error "%s is not executable" path;
385+
return (Error (backend_error "SCRIPT_NOT_EXECUTABLE" [ path; Exn.to_string exn ]))
386+
| Ok path -> invoke_script call path
387+
in
294388

295-
(* The Errors we return from this function and the special error format
296-
returned by the scripts are not included in the error types of the various
297-
SMAPIv3 interfaces, therefore we have to propagate them as exceptions
298-
instead of returning an RPC call with an error, because rpclib would fail
299-
to unmarshal that error.
300-
Therefore we either return a successful RPC response, or raise
301-
Fork_exec_error with a suitable SMAPIv2 error if the call failed. *)
302-
let rpc call =
303-
script_rpc call >>= fun result ->
304-
Result.map_error ~f:(fun e -> Fork_exec_error e) result
305-
|> Result.ok_exn
306-
|> return
307-
in
389+
(* The Errors we return from this function and the special error format
390+
returned by the scripts are not included in the error types of the various
391+
SMAPIv3 interfaces, therefore we have to propagate them as exceptions
392+
instead of returning an RPC call with an error, because rpclib would fail
393+
to unmarshal that error.
394+
Therefore we either return a successful RPC response, or raise
395+
Fork_exec_error with a suitable SMAPIv2 error if the call failed. *)
396+
let rpc : R.call -> R.response Deferred.t = fun call ->
397+
script_rpc call >>= fun result ->
398+
Result.map_error ~f:(fun e -> Fork_exec_error e) result
399+
|> Result.ok_exn
400+
|> return
401+
in
308402

309-
rpc
403+
rpc
310404

311405
module Attached_SRs = struct
312406
type state = {
@@ -421,75 +515,6 @@ let vdi_of_volume x =
421515
persistent = true;
422516
}
423517

424-
module Compat(V : sig val version : string option ref end) = struct
425-
(** Module for making the inputs and outputs compatible with the old PVS
426-
version of the storage scripts. *)
427-
428-
let remove field rpc =
429-
match !V.version, rpc with
430-
| Some v, R.Dict d when v = pvs_version ->
431-
R.Dict (List.filter ~f:(fun (k,_) -> k <> field) d)
432-
| _ -> rpc
433-
434-
let with_pvs_version f rpc = if !V.version = Some pvs_version then f rpc else rpc
435-
436-
let add_param_to_input params =
437-
with_pvs_version (function
438-
(* Currently all parameters must be named. In this case, rpclib
439-
currently puts them into a Dict. *)
440-
| R.Dict d -> R.Dict (List.rev_append params d)
441-
| rpc -> rpc)
442-
443-
let add_fields_to_dict fields =
444-
function R.Dict d -> R.Dict (List.rev_append fields d) | rpc -> rpc
445-
446-
let add_fields_to_record_output fields =
447-
with_pvs_version (function
448-
| R.Dict _ as d -> add_fields_to_dict fields d
449-
| rpc -> rpc)
450-
451-
let add_fields_to_record_list_output fields =
452-
with_pvs_version (function
453-
| R.Enum l -> R.Enum (List.map ~f:(add_fields_to_dict fields) l)
454-
| rpc -> rpc)
455-
456-
(** Add the missing [sharable] field to the Dict in [rpc], to ensure the
457-
volume in the output match the new volume record type and is successfully
458-
parsed by rpclib. *)
459-
let compat_out_volume =
460-
add_fields_to_record_output ["sharable", R.Bool false]
461-
462-
(** Add the missing [sharable] field to the Dicts in [rpc], to ensure the
463-
volumes in the output match the new volume record type and are
464-
successfully parsed by rpclib. *)
465-
let compat_out_volumes =
466-
add_fields_to_record_list_output ["sharable", R.Bool false]
467-
468-
(** For the old PVS version, adds the uri parameter to the call from
469-
device_config, for newer versions, removes the uri key from device_config *)
470-
let compat_uri device_config =
471-
if !V.version = Some pvs_version then
472-
match List.Assoc.find ~equal:String.equal device_config "uri" with
473-
| None ->
474-
return (Error (missing_uri ()))
475-
| Some uri ->
476-
return (Ok (add_param_to_input ["uri", R.String uri]))
477-
else
478-
return (Ok id)
479-
480-
(** Compatiblity for the old PVS version of SR.create, which had signature
481-
[uri -> name -> desc -> config -> unit] *)
482-
let sr_create device_config =
483-
compat_uri device_config >>>= fun compat_in ->
484-
let compat_out rpc =
485-
(* The PVS version will return nothing *)
486-
if rpc = R.Null then
487-
Rpcmarshal.marshal Xapi_storage.Control.typ_of_configuration device_config
488-
else rpc
489-
in
490-
return (Ok (device_config, compat_in, compat_out))
491-
end
492-
493518
let choose_datapath ?(persistent = true) response =
494519
(* We can only use a URI with a valid scheme, since we use the scheme
495520
to name the datapath plugin. *)

0 commit comments

Comments
 (0)